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.
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. There is no ambient access to system resources, and there is 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_thread_limit(S, 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 working out of the box. 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_thread_stack_size 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 there is 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. send enqueues onto the POOLED run-queue, send-off onto SOLO; each pool has its own worker thread but the per-state eval lock still serializes one action at a time across both pools, so multi-agent dispatch is still serialized within one state. Each worker counts against thread_limit (the embedder thread does not), so send throws MTH001 if the host hasn't granted a thread budget; embedders that want both pools alive concurrently must raise the limit to >= 2. send-via is intentionally deferred (no public Executor type). One small pool-routing deviation: send-off inside a dosync posts onto POOLED for the post-commit drain (JVM canon would dispatch via the action's original pool). This is invisible while both pools run under the per-state eval lock; the deviation will matter once SOLO yields the lock for blocking IO. See STM for the full surface, including the public C-API perimeter (mino_send, mino_send_off, mino_await, mino_await_for, mino_agent_error, mino_restart_agent).
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.
Auto-promoting math operators
Plain + / - / * / inc / dec auto-promote to MINO_BIGINT on long overflow. JVM Clojure raises ArithmeticException for the unprimed forms and reserves the prime variants (+' / -' / etc.) for the auto-promoting behaviour; mino keeps only one form and chooses the safe one. (* Long/MIN_VALUE -1) on the JVM throws; on mino it returns 9223372036854775808N.
Why. mino's stated goal is correctness over throughput. Long overflow is a frequent source of subtle production bugs in JVM Clojure and forces every numeric library to choose between throwing and primed-everywhere. Auto-promotion makes the obvious form mathematically right; the embedded use case rarely needs the wraparound shape, and when it does the unchecked-* family (unchecked-+, unchecked--, unchecked-*, unchecked-inc, unchecked-dec, unchecked-divide-int) is the explicit opt-in. Same surface as Clojure, sharper default.
One float tier (double)
mino has a single 64-bit IEEE 754 float tier (MINO_FLOAT). (float 0.1) and (double 0.1) return the same value, (= (float 0.5) (double 0.5)) is true, and float? / double? are aliases. JVM Clojure exposes both java.lang.Float and java.lang.Double as distinct types; cross-type equality is false there even when the values are numerically equal.
Why. A 32-bit float tier exists in JVM Clojure mostly because Java's primitive set forces it; the values exist on the heap as boxed java.lang.Float objects, and cross-tier comparison is a frequent source of bugs ((= 0.1 (float 0.1)) is false on the JVM). mino picks one float tier and uses it consistently. The C-level embedding API exposes double for both reading and writing, so there's no representational gap to span.
Regex patterns are strings
mino's regex literal #"..." parses to a MINO_STRING whose contents are the pattern source. re-find, re-matches, re-seq, re-pattern, and the clojure.string regex consumers compile from that string at the call site. There is no separate java.util.regex.Pattern-equivalent value type.
Consequences. Two regex values produced from the same source string compare equal under = (they're the same string). On the JVM, (= #"x" #"x") is false because Pattern instances rely on identity. The mino behaviour is convenient (regex patterns can be used as map keys, deduplicated in sets) but does diverge from Clojure's Pattern semantics.
Permissive function arity
Calling a fixed-arity function with too few or too many arguments does not throw in mino. Missing positional parameters bind to nil, and trailing arguments are silently ignored: ((fn [x] x) 1 2 3) returns 1, and ((fn [x y] [x y])) returns [nil nil]. JVM Clojure raises clojure.lang.ArityException in both cases.
Why. The embedded use case favours robustness over strictness; permissive arity makes host-supplied callbacks easier to slot in without exact shape negotiation. Variadic & rest parameters are still respected, and a future strict mode is on the long-term roadmap.
What is in scope for future versions
One queued item remains on the roadmap:
- 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.
Items that shipped recently: regex literal escapes; the *out* / *err* / *in* print pipeline; REPL specials and clojure.repl / clojure.stacktrace; clojure.core.protocols and clojure.datafy; auto-promoting + / - / * / inc / dec plus the unchecked-* family; real defrecord / deftype / reify / instance?; bundled stdlib + per-group install hooks; clojure.template + clojure.instant; *data-readers* reader hook; clojure.spec.alpha + clojure.core.specs.alpha; the host-thread capability and metadata surface; real OS-thread future / promise / thread backed by pthread_create; blocking <!! / >!! / alts!! parking across threads; the embed-distinctive thread pool, factory, and stack-size surface; real MINO_VOLATILE backing volatile! / vswap! / vreset! for stateful transducers; iteration (Clojure 1.11); the clojure.core.async namespace wrap with merge and into under their canon names; the chunked-seq family (MINO_CHUNKED_CONS value type, chunked-seq?, chunk-first, chunk-rest, chunk-next, chunk-cons, chunk-buffer, chunk-append, chunk) with map/filter/take/keep/keep-indexed/map-indexed propagating chunkedness end-to-end and source-side auto-chunking on (seq vec) and lazy range; cross-type compare over the canon order (nil < false < true < numbers < strings < symbols < keywords); and a minimal clojure.test.check port (generators, properties, quick-check; shrinking deferred) backing s/gen and s/exercise.
The remaining items above (no JVM interop, simpler STM underneath, no proxy / definterface) are stable design choices, not deferrals.