Intentional divergences from Clojure
mino aims to be the Clojure dialect at embedded scale. Every divergence on this page is a deliberate design decision, not a missing feature waiting for a contributor. Each entry names what is different, why, and what mino offers in its place.
For an item-by-item rundown of which Clojure functions and macros are supported, differ, or are absent, see the compatibility matrix.
Coverage: 90.9% of the Clojure 1.12.4 surface. 89 vars are intentionally absent (JVM-bound) and 2 are genuine gaps.
No JVM interop surface
mino is written in ANSI C, not on the JVM. Class/forName, bean, gen-class, .., set! on instance fields, host-array literals (int-array, to-array, etc.), Java class type hints, and *warn-on-reflection* all assume a JVM that mino does not have.
What mino keeps: the surface syntax for calling host methods. (.next obj), (.-field obj), (Type/static-call ...), and (new Type ...) all work, but they dispatch through a capability registry the embedder controls. Each method, getter, and constructor is opted in by the host. No ambient access to system resources, and no reflection at all. See the Embedding Guide for the full host contract.
Host-grant-gated host threads
Threading is a per-state runtime capability the host grants, not a build-time feature. Each mino_state starts at thread_limit = 1 (single-threaded). Embedders raise the limit via mino_set_option(S, MINO_OPT_THREAD_LIMIT, n); while the limit is <= 1, future, promise, deliver, thread, and the blocking <!! / >!! / alts!! ops throw :mino/unsupported with a message naming the policy.
Standalone ./mino grants cpu_count right after mino_install_all, so REPL/script users see the canon surface without configuration. Embedders that want sandboxed scripts withhold the grant; embedders that want canon parity make the same call the standalone binary does.
Status. The full surface ships: real OS-thread future / promise / thread backed by pthread_create (CreateThread on Windows); deref parks via pthread_cond_wait; future-cancel, future-done?, future-cancelled?, realized?, future? round it out; blocking <!! / >!! / alts!! park across OS threads. The (mino-thread-limit) primitive exposes the current limit so library code can branch on it. ASan + UBSan + TSan-clean across the test suite.
Embed-distinctive value-add. mino_set_thread_pool lets the host hand mino an existing pool (Tokio runtime, libuv worker pool, ASIO io_context, custom pthread pool); workers from that pool service future spawns. The work item carries the state pointer, not the thread, so the same N-worker pool can service an unbounded number of isolated mino_state runtimes, multi-tenant by construction. mino_set_thread_factory hooks per-worker naming, affinity, priority for the spawn-per-future path; mino_set_option(S, MINO_OPT_THREAD_STACK_BYTES, n) tunes RSS for tight embedders. JVM Clojure cannot offer this because the JVM forces one global heap; mino's per-state isolation makes it natural. See examples/embed_multi_tenant_threads.c for a worked end-to-end demo.
Cooperative concurrency without threading. core.async channels and go blocks remain the inside-one-runtime story. go parking, channel composition, transducer-carrying channels, alts!, timeout, mult / tap, pub / sub, and pipeline all work without threads. Inside a go block <! / >! park the fiber; outside, the blocking variants pump the scheduler. The grant gates only the OS-thread shape, not the cooperative shape.
STM uses single-version optimistic locking
ref, dosync, alter, commute, ensure, ref-set, and io! all work as in Clojure, plus watches and validators on refs. The Clojure surface matches canon for any program that does not depend on the items below.
Underneath, mino is simpler than JVM Clojure. mino keeps one committed value per ref instead of the JVM MVCC history ring; ref-min-history, ref-max-history, and ref-history-count are stubs returning 0 / 10 / 0. A single global commit lock serializes commits in place of per-ref read/write locks, and no barging or mid-body retry. Long readers under sustained writer pressure may exhaust the 10000-retry cap rather than serve an older snapshot from history.
The trade-off is deliberate. mino's typical workload is a small ref set and a handful of worker threads, often single-threaded. The simpler machinery costs nothing on the single-threaded fast path and stays comprehensible at a glance. See the STM page for the full enumeration of deviations and the C API mirror.
Agents dispatch asynchronously through per-state worker threads (POOLED + SOLO). agent, send, send-off, await, await-for, agent-error, restart-agent, and shutdown-agents all ship. The per-state eval lock serializes one action at a time across both pools. send-via is intentionally deferred (no public Executor type). One pool-routing deviation: send-off inside a dosync posts onto POOLED for the post-commit drain rather than the action's original pool. See STM for the full surface and the C API perimeter.
No proxy, definterface
defrecord, deftype, reify, and instance? all ship as real value types. See the Coming from Clojure page for the canonical surface and the embed-distinctive C-side construction API.
proxy materializes an anonymous JVM object implementing host interfaces; definterface declares one. Both are JVM shapes that don't translate to mino's runtime.
Use defprotocol + extend-type. For one-off polymorphic values, mino has real reify. For static interface declaration, defprotocol is the analogue. definterface throws an informative error pointing at defprotocol.
Multimethods use the global hierarchy only
defmulti accepts a :hierarchy option in Clojure to dispatch against an explicit user-supplied hierarchy. mino's defmulti always dispatches through the global hierarchy.
Hierarchy-as-data still works: make-hierarchy, 3-arity derive / underive, and isa? against an explicit hierarchy all behave as in Clojure. What does not exist is binding such a hierarchy to a particular multimethod.
If user code needs scoped dispatch, the workaround is to keep the hierarchy keys disjoint between subsystems (prefix with namespace) so the global hierarchy stays uncontested.
Full divergence catalog
The entries below are generated from the clojure-census data. Every divergence is tracked with its affected vars, rationale, and behavioral expectation.
Ordering & comparison
How sort, compare, and ordered iteration work.
compare returns sign-only (-1, 0, 1)
Hickey-test for what compare's contract is: a three-way ordering function. Sign-normalized return values are simpler and avoid leaking representation-specific deltas.
Affected: clojure.core/compare
| Clojure: | (compare "z" "a") ;=> 25 |
| mino: | (compare "z" "a") ;=> 1 |
Behavior: Diverges as expected -- Same sign as JVM compare; magnitude collapsed to -1/0/1.
Since v0.1.0
sorted-map default comparator does not handle mixed types
Cross-type comparison was closed in v0.98; mixed- type sorted-map keys without an explicit comparator may throw rather than coerce.
Affected: clojure.core/sorted-map clojure.core/sorted-set
Since v0.98.0
Type-system representation
How type, class, and dispatch report a value's kind.
type returns a keyword, not a Java class
No JVM classes. mino's type system is keyword- tagged (:vector, :map, :list, :symbol, ...). type reports the concrete tag and, unlike class, honours :type metadata when present. Dispatch logic that pattern-matches on classes must use the keyword tags instead.
Affected: clojure.core/type clojure.core/class
| Clojure: | (type [1 2 3]) ;=> clojure.lang.PersistentVector |
| mino: | (type [1 2 3]) ;=> :vector |
Behavior: Diverges as expected -- Oracle returns a Class object; mino returns the analogous keyword tag.
Since v0.1.0
class is a concrete-tag function, not a JVM class
There are no JVM classes, so class is a real concrete-tag function rather than an alias of type: nil yields nil, a record yields the record's type symbol, and every other value yields its type keyword. class ignores :type metadata, which is what distinguishes it from type. Code that branches on a host Class must use the keyword/symbol tags instead.
Affected: clojure.core/class
| Clojure: | (class nil) ;=> nil, (class [1 2 3]) ;=> clojure.lang.PersistentVector |
| mino: | (class nil) ;=> nil, (class [1 2 3]) ;=> :vector |
Behavior: Diverges as expected -- nil yields nil (a true match); other values yield the analogous keyword or record-type symbol where the oracle returns a Class object.
Since v0.1.0
defrecord types are not Java classes
mino's records carry identity, field slots, and a type keyword -- no host-level class is generated. instance? on a record uses the type keyword as its discriminator.
Affected: clojure.core/defrecord clojure.core/instance?
Since v0.1.0
deftype is an alias for defrecord without map-like ops
deftype's volatile-field and interface-impl features depend on JVM semantics. mino provides deftype as a structural type carrier; the JVM- only knobs are no-ops.
Affected: clojure.core/deftype
Since v0.1.0
Protocol-dispatch implementation internals are absent
These are private-ish JVM dispatch internals: the protocol-fn cache, method-reset, impl/method lookup over host classes, and the iterator reduce helper. mino dispatches protocols through its own atom-keyed mechanism and does not expose the JVM internals.
Affected: clojure.core/-cache-protocol-fn clojure.core/-reset-methods clojure.core/find-protocol-impl clojure.core/find-protocol-method clojure.core.protocols/iterator-reduce!
Since v0.1.0
Numeric tower
Integer tiers, ratio promotion, float precision.
Single integer type -- no Long/Integer/Short distinction
mino has no JVM types; one integer representation suffices. Promotion within the integer tier never happens because there is only one tier.
Affected: clojure.core/integer? clojure.core/int? clojure.core/long
Since v0.1.0
Float-32 is a distinct numeric type
mino exposes a 32-bit float-tagged value for space-constrained embedded use; Clojure (JVM) collapses to Double.
Since v0.1.0
= on cross-type numbers follows Clojure (JVM), not JVM =
= remains a value-equality predicate; numeric equality across float / int is what Clojure (JVM) specifies, not JVM-Object-equality.
Affected: clojure.core/=
Since v0.1.0
s/double-in rejects NaN and infinity unless opted in
A plain range spec admitting NaN and infinity by default is a silent-surprise trap; mino defaults :NaN? and :infinite? to false so out-of-range non-finite doubles fail validation unless the spec opts in. Canon defaults both to true.
Affected: clojure.spec.alpha/double-in
| Clojure: | (s/valid? (s/double-in :min 0.0 :max 1.0) ##NaN) ;=> true |
| mino: | (s/valid? (s/double-in :min 0.0 :max 1.0) ##NaN) ;=> false |
Since unreleased
Reader behavior
Reader macros, literal forms, source-meta attachment.
Reader conditional :clj does not fire under mino
mino is not JVM Clojure; portable code must use :default or :mino to target it. Documented and intentional.
| Clojure: | #?(:clj :A :mino :B :default :C) ;=> :A |
| mino: | #?(:clj :A :mino :B :default :C) ;=> :B |
Since v0.1.0
Printer behavior
pr-str output for various values and dynamic-var states.
pprint is minimal -- no cl-format directives
mino's clojure.pprint supports pretty-printing basic forms; cl-format and table directives are not implemented. Targets the common case.
Affected: clojure.pprint/cl-format clojure.pprint/print-table
Since v0.1.0
CLI default for *print-namespace-maps* is true
mino's CLI alters the var-root to true on startup so user-facing prints collapse qualified-key maps. bb and JVM Clojure do the same; library use sees the documented false default.
Affected: clojure.core/*print-namespace-maps*
Since v0.422.0
print-dup / print-ctor are absent
print-dup emits host-class-aware, read-eval-able forms keyed on Java types. mino prints EDN-readable forms through the ordinary printer; the print-dup dispatch and its print-ctor helper are not provided.
Affected: clojure.core/print-dup clojure.core/print-ctor
Since v0.1.0
Collection semantics
Iteration order, equality, hash, structural sharing.
clojure.spec generators ship as separate primitives
mino's spec.alpha implements the documented surface; clojure.spec.gen.alpha is partially present and may diverge from clojure.test.check behavior on edge cases.
Since v0.1.0
transit reader/writer are not bundled
Transit is a JVM artifact in Clojure (JVM) distributions; mino does not bundle it. EDN remains the wire format.
Since v0.1.0
Sequence chunking is deterministic but not always 32
Lazy realization batch size is implementation- defined in Clojure (JVM); mino picks sizes that suit the embedded use case. Code that depends on exact chunk boundaries is non-portable in Clojure (JVM) too.
Since v0.1.0
Concurrency primitives
Atoms, refs, agents, futures, core.async behavior.
Atoms, refs, futures, agents have alpha-quality
mino runs single-threaded in the embedded host; concurrency primitives are present but the underlying execution model is cooperative, not preemptive.
Affected: clojure.core/future clojure.core/agent clojure.core/send clojure.core/send-off
Since v0.1.0
Agent ForkJoin-executor controls are absent
mino's agents run on a cooperative single-threaded model, so the JVM ForkJoin send-executor controls and agent-error inspection vars have no backing thread pool to configure.
Affected: clojure.core/agent-errors clojure.core/await1 clojure.core/clear-agent-errors clojure.core/set-agent-send-executor! clojure.core/set-agent-send-off-executor!
Since v0.1.0
reducers ForkJoin task/pool entry points are absent
clojure.core.reducers/fjtask and /pool front a JVM ForkJoinPool for parallel fold. mino has no ForkJoin runtime, so fold runs sequentially and the pool entry points are absent.
Affected: clojure.core.reducers/fjtask clojure.core.reducers/pool
Since v0.1.0
Error message shapes
Exception types, ex-data shapes, message phrasing.
ex-info / throw use mino-native exception type
No JVM Throwable. mino's exception type is its own runtime value with :message and :data fields; instance checks via clojure.lang.ExceptionInfo do not work.
Affected: clojure.core/ex-info clojure.core/ex-data clojure.core/ex-message clojure.core/ex-cause
Behavior: Skipped -- Exception type/message comparison is out of scope per the README's error-message exclusion.
Since v0.1.0
Error messages and ex-info keys may differ
Exception text is implementation-defined in Clojure (JVM). mino's messages aim to be informative but are not byte-for-byte JVM-Clojure-compatible. Use ex-data / ex-cause / category keys to dispatch.
Since v0.1.0
Throwable->map returns an empty :trace vector
mino error values carry no stack frames, so the canon map shape is preserved (:cause :data :via :trace) with :trace always []. Code that walks :via and :cause ports unchanged; frame-inspecting code finds an empty vector, not a missing key.
Affected: clojure.core/Throwable->map
| Clojure: | (:trace (Throwable->map e)) ;=> [[clazz method file line] ...] |
| mino: | (:trace (Throwable->map e)) ;=> [] |
Since unreleased
JVM-static value remap
JVM-only statics remapped to dialect-native equivalents.
Integer/MAX_VALUE etc. remapped or absent
mino exposes a curated subset of JVM-static-name values (Integer/toBinaryString, Long/toHexString, ...) for Clojure code that depends on them. Most are absent.
Since v0.1.0
No clojure.reflect -- JVM-only
Reflection is meaningless without a JVM. mino's introspection uses ns-publics + meta, not class inspection.
Since v0.1.0
proxy is not provided
proxy generates a JVM class -- no equivalent in mino. defrecord + protocols cover the same use case in a host-portable way.
Affected: clojure.core/proxy
Since v0.1.0
gen-class is not provided
gen-class emits JVM bytecode. mino has no Java interop layer.
Affected: clojure.core/gen-class
Since v0.1.0
clojure.java.io namespace is intentionally absent
mino's host I/O is exposed via mino-native primitives that do not match File / InputStream abstractions. clojure.java.io is excluded from parity comparison.
Since v0.1.0
clojure.repl is intentionally absent
Repl support is environment-bound and uses embedder-specific input/output. doc, source, apropos are exposed at the CLI rather than in a namespace.
Since v0.1.0
Primitive-array and gvec machinery is absent
mino has no JVM primitive arrays. The array constructors, mutators, coercions, and the gvec (vector-of) backing types front java[] storage that does not exist here; persistent vectors and maps cover the same use cases host-portably.
Affected: clojure.core/aclone clojure.core/amap clojure.core/areduce clojure.core/aset-boolean clojure.core/aset-byte clojure.core/aset-char clojure.core/aset-double clojure.core/aset-float clojure.core/aset-int clojure.core/aset-long clojure.core/aset-short clojure.core/booleans clojure.core/bytes clojure.core/chars clojure.core/shorts clojure.core/ints clojure.core/longs clojure.core/floats clojure.core/doubles clojure.core/make-array clojure.core/to-array-2d clojure.core/vector-of clojure.core/->ArrayChunk clojure.core/->Vec clojure.core/->VecNode clojure.core/->VecSeq clojure.core/EMPTY-NODE clojure.core/primitives-classnames
Since v0.1.0
proxy / struct class-generation helpers are absent
construct-proxy, the proxy-* helpers, gen-interface, and the struct family all generate or manipulate JVM classes and struct-maps. mino has no host class layer; defrecord plus protocols cover the same ground host-portably.
Affected: clojure.core/construct-proxy clojure.core/get-proxy-class clojure.core/init-proxy clojure.core/update-proxy clojure.core/proxy-call-with-super clojure.core/proxy-mappings clojure.core/proxy-name clojure.core/proxy-super clojure.core/gen-interface clojure.core/create-struct clojure.core/defstruct clojure.core/struct clojure.core/struct-map
Since v0.1.0
Classloader and compiler controls are absent
mino has no JVM classloader hierarchy or bytecode compiler, so the vars and functions that drive them have nothing to bind or call. Loading is handled by mino-native primitives instead.
Affected: clojure.core/*compiler-options* clojure.core/*fn-loader* clojure.core/*use-context-classloader* clojure.core/*reader-resolver* clojure.core/*allow-unresolved-vars* clojure.core/*verbose-defrecords* clojure.core/*suppress-read* clojure.core/add-classpath clojure.core/compile clojure.core/load clojure.core/load-reader clojure.core/with-loading-context clojure.core/definline
Since v0.1.0
Java host-interop bridges are absent
These vars bridge into the Java host -- class hierarchy queries, bean reflection, the .. and memfn interop sugar, host iterator/enumeration and JDK-stream adapters, and the class? predicate. Without a JVM there is nothing to bridge to.
Affected: clojure.core/bases clojure.core/supers clojure.core/cast clojure.core/class? clojure.core/bean clojure.core/enumeration-seq clojure.core/iterator-seq clojure.core/resultset-seq clojure.core/PrintWriter-on clojure.core/StackTraceElement->vec clojure.core/method-sig clojure.core/accessor clojure.core/.. clojure.core/memfn clojure.core/stream-into! clojure.core/stream-reduce! clojure.core/stream-seq! clojure.core/stream-transduce!
Since v0.1.0
Host symbol-name munging is absent
munge and namespace-munge encode Clojure names into JVM-legal class and member names. mino emits no host symbols, so the munging step has no purpose.
Affected: clojure.core/munge clojure.core/namespace-munge
Since v0.1.0
*read-eval* is absent
*read-eval* gates the JVM reader's #=() eval-on-read feature, which mino's reader does not provide; there is no host eval to guard, so the var is absent.
Affected: clojure.core/*read-eval*
Since v0.1.0
read-instant-calendar / -timestamp are absent
These instant readers materialise java.util.Calendar and java.sql.Timestamp host values. mino has neither type; read-instant-date covers the portable case.
Affected: clojure.instant/read-instant-calendar clojure.instant/read-instant-timestamp
Since v0.1.0
Namespace mechanics
ns, require, refer, alias, var resolution semantics.
Namespaced keyword aliasing uses runtime aliases
::alias/kw is resolved at read time against the current ns's aliases -- same as JVM Clojure, but mino's reader does not have access to the JVM classloader hierarchy.
Since v0.1.0
Metadata propagation
Where reader attaches meta and which ops preserve it.
Reader-tracked :file may be relative or absolute
mino's reader records the path as the embedder passed it; JVM Clojure normalizes to classpath- relative. Tools that compare :file values must accept either.
Since v0.1.0
Reader attaches {:line :column} to lists only
mino mirrors JVM Clojure: per-cons reader meta is accessible through meta; literal vectors/maps/sets receive no per-instance reader meta.
Since v0.422.0
pr and *clojure-version* do not carry :dynamic metadata
On the JVM these two are marked dynamic for host-historical reasons (pprint-era rebinding and a version map nobody rebinds). mino does not inherit the wart; neither var is rebindable and neither claims to be.
Affected: clojure.core/pr clojure.core/*clojure-version*
| Clojure: | (:dynamic (meta (resolve 'pr))) ;=> true |
| mino: | (:dynamic (meta (resolve 'pr))) ;=> nil |
Since v0.1.0
What is in scope for future versions
- ABI freeze at v1.0. Until then
src/mino.his labelled evolving and the numeric-tower type tags (MINO_BIGINT,MINO_RATIO,MINO_BIGDEC) sit under the same evolving-API umbrella.
The remaining items above (no JVM interop, simpler STM underneath, no proxy / definterface) are stable design choices, not deferrals.