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.

See Coming from Clojure

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

See Coming from Clojure

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.

See Coming from Clojure

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.

See Coming from Clojure

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

See Coming from Clojure

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

See Coming from Clojure

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.

See Coming from Clojure

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

See Coming from Clojure

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.

See Coming from Clojure

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.

See Coming from Clojure

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.

See Coming from Clojure

Since v0.422.0

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

The remaining items above (no JVM interop, simpler STM underneath, no proxy / definterface) are stable design choices, not deferrals.