Coming from Clojure
mino aspires to become a proper Clojure dialect. If you know Clojure, most mino code will look familiar. This page is the higher-level tour of where mino differs and why; for an item-by-item table of supported / differs / absent forms, see the compatibility matrix, and for the longer-form rationale behind each divergence, see intentional divergences.
Rules of thumb
- Pure language syntax follows Clojure.
- Host boundary operations use mino-native explicit forms.
- Reader desugaring is preferred over evaluator special cases.
- Macro and library implementations are preferred over new C runtime features.
- Least-surprising behavior for REPL users.
- Differences are intentional, documented, and tied to embeddability.
What works the same
Most daily Clojure idioms work as expected:
- Persistent vectors, maps, sets, and lists with structural sharing
- Immutable values everywhere
fn,defn,let,loop/recurwith vector binding forms- Destructuring in
let,fn,loop(vector positional, map:keys/:strs/:syms/:or/:as, nested) - Multi-arity functions:
(fn ([x] ...) ([x y] ...)) - Named anonymous functions:
(fn name [x] ...) - Macros, quasiquote,
gensym - Lazy sequences (
lazy-seq,map,filter,take,range, etc.) - Atoms with
swap!andreset! - Refs and STM:
ref,dosync,alter,commute,ensure,ref-set,io!,in-transaction?, plusadd-watch/set-validator!on refs (single-version optimistic locking; see STM for the deviations from JVM canon) - Threading macros (
->,->>,as->,cond->,cond->>,some->,some->>) try/catch/finally/throwwith-openfor resource managementapply,partial,comp,complement- Regular expressions via
re-find,re-matches,re-seq - Full test framework:
deftest,is,testing - Callable keywords:
(:k m)as map lookup #()anonymous function shorthand#_discard reader macroex-info,ex-data,ex-messagereducedfor early termination inreduce- Multi-collection
map:(map + [1 2] [3 4])works setconstructor:(set coll)- Variadic
comp:(comp f g h ...) identical?for pointer identity- Value metadata:
meta,with-meta,vary-meta,alter-meta!,^{:key val}/^:key/^Typereader syntax - Protocols:
defprotocol,extend-type,extend-protocol,satisfies? - Multi-binding
foranddoseqwith:when,:while, and:let iteration(Clojure 1.11) for stateful pull iterators built from a step function- Transducers:
transduce,intowith xform,sequence,eduction,completing,cat,halt-when,ensure-reduced - Attribute maps in
defnanddefmacroare accepted and skipped - Forward declarations:
declareand(def name)without a value - Bootstrap aliases:
fn*,let*,loop*are accepted as their unstarred equivalents update-valsandupdate-keysfor transforming map values or keysmin-keyandmax-keyfor finding elements by keyed comparisoncasewith literal matching, multi-match lists, and default clausescommentandwhen-firstmacrosclojure.setnamespace:union,intersection,difference,select,project,rename,rename-keys,map-invert,join,index,subset?,superset?clojure.stringnamespace:lower-case,upper-case,capitalize,reverse,blank?,starts-with?,ends-with?,escape,replace,trim,triml,trimr,trim-newline,split-lines,join,includes?random-samplefor probabilistic filteringbounded-countfor counting with an upper limit on lazy sequenceswhilemacro for imperative loopsdistinct?for checking argument uniqueness- Type predicates:
sorted?,associative?,reversible?,counted?,any?
Namespaces
Namespaces are first-class. Each namespace owns its own root binding table, so (ns a) (def x 1) and (ns b) (def x 2) are independent. clojure.core is the bundled-core namespace; every other namespace's root env chains to it via a parent pointer, so unqualified if, map, let keep working without an explicit refer.
(ns myapp.core
(:require [clojure.string :as str]))
(str/blank? "") ;=> trueThe full ns surface is here: :require (with :as, :as-alias, :refer, :refer :all, :only, :exclude, :rename, and prefix lists), :use, and :refer-clojure. Vars are first-class. (def x 1) returns #'<ns>/x, intern, find-var, var-get, var-set, alter-var-root, and with-redefs all work, and ^:private is enforced on cross-namespace qualified access.
Module resolution uses a host-supplied resolver. The default standalone resolver searches .cljc, .clj, and .cljs in that order. A loaded file's first (ns ...) form must declare the requested module name (dash and underscore are equivalent), so accidental misnaming fails loud rather than silently. Isolation between runtimes still gives you full per-state isolation when you want it.
Concurrency
mino provides two concurrency models. Cooperative async runs by default; host-granted threading layers on top.
core.async
clojure.core.async channels and go blocks work as expected:
(require '[clojure.core.async :as a :refer [chan go >! <!!]])
(let [ch (chan 10)]
(go (>! ch 42))
(println (<!! ch))) ;=> 42Supported under the clojure.core.async ns: chan, put!, take!, close!, go, go-loop, <!, >!, <!!, >!!, alts!, alts!!, timeout, pipe, merge, into, mult/tap, pub/sub, mix/admix, pipeline, pipeline-async. Channels support transducers and exception handlers.
Differences from the JVM implementation:
goblocks use cooperative scheduling on the calling thread (no fiber pool).<!!/>!!/alts!!park across OS threads when the host has granted them. See the host-threads contract.alt!macro is not implemented (usealts!)- Parks in
catch/finallybodies are not supported - Nested parks in function call arguments require explicit
letbindings
Futures, promises, threads
future, promise, deliver, thread, future-cancel, future-done?, future-cancelled?, realized?, and future? back onto real OS threads via pthread_create (CreateThread on Windows). deref parks via pthread_cond_wait; the blocking core.async ops park on the same condition variables when the runtime thread limit is greater than 1.
thread is a stable alias for future-call; both share the same worker pool. Embedders raise the per-state limit via mino_set_thread_limit(S, n); the standalone ./mino binary grants cpu_count by default, so REPL and script users see the canonical surface working out of the box. Embedders that want sandboxed scripts withhold the grant.
When the limit is <= 1, the same forms throw :mino/unsupported with a message naming the policy. agent ships and constructors work, but send / send-off throw MTH001 when their pool's worker can't spawn under the granted limit. pmap is not provided. See the host-threads contract for the embed-distinctive pool / factory / stack-size knobs and the multi-tenant pool example.
Host interop
mino uses the same .method, .-field, new, and Type/static syntax. The host registers capabilities through a type-oriented registry with a default-deny policy:
;; Familiar syntax, capability-gated dispatch
(def c (new Counter))
(.inc c)
(.-value c) ;=> 1
(Math/add 3 4) ;=> 7
;; Explicit forms also available
(host/call c :inc)
(host/get c :value)
(host/static-call :Math :add 3 4)The host decides which types and methods each runtime gets. There is no ambient access to system resources. See the Embedding Guide for details.
Reader syntax
Most reader macros work as in Clojure. A few are absent:
| Syntax | Status |
|---|---|
#(inc %) | Same |
#'var | Same |
#_ form | Same |
^{:key val} / ^:key / ^Type | Same |
#?(:clj ... :default ...) | Same (active dialect keys are :mino and :clj) |
#?@(...) | Same (splice reader conditional) |
'() / `(~x ~@xs) / @atom | Same |
2r1010 / 0xFF / 8r77 | Same (radix and hex integer literals) |
#"regex" | Same (body bytes pass to the regex engine verbatim; \d reaches the engine as backslash and d) |
::keyword / ::alias/keyword | Same (auto-resolved at read time) |
#:foo{:b 1} / #::{:b 1} / #::alias{...} | Same (namespaced map literals) |
Data structures
Core data structures match Clojure semantics:
- Vectors, maps, sets, and lists are persistent and immutable with structural sharing (Bagwell tries for vectors, HAMT for maps and sets)
- Cross-type sequential equality:
(= '(1 2) [1 2])istrue conj,assoc,dissoc,get,nth,intowork as expected- Sets support
contains?,conj,disj - Collections as callable functions:
({:a 1} :a)returns1,([1 2 3] 0)returns1,(#{:a :b} :a)returns:a. Works in higher-order contexts like(map :name coll)and(filter #{:a} coll) peekandpopfor stack abstraction on vectors (from end) and lists (from front)findreturns[key val]ornilemptyreturns an empty collection of the same typerseqfor reverse-order vector traversalsorted-mapandsorted-setwith persistent red-black tree (LLRB), maintaining key ordering with structural sharing
Differences:
array-mapis an alias forhash-map(HAMT is used at all sizes)
Characters
Character literals (\A, \space, \uNNNN, literal UTF-8 like \☃) parse to a distinct character type holding a Unicode codepoint. char? returns true for chars and only for chars; string? returns false. (int \A) is 65 and (str \A) is "A". Chars hash and compare distinctly from single-character strings, so they can live cleanly as map keys or set members.
Sequences
Lazy sequences work the same way:
(take 5 (map inc (range))) ;=> (1 2 3 4 5)rest on vectors, maps, sets, and strings returns a lazy cons chain (elements produced on demand), matching the expected behavior for large collections.
Strings are sequences of characters: (seq "abc") returns (\a \b \c), (first "abc") returns \a, and (get "ab" 0) returns \a. The walk is codepoint-counted so multi-byte characters like \☃ count as one position. subs indexes by codepoint as well.
Transducers work as expected:
(into [] (comp (map inc) (filter even?)) [1 2 3 4 5])
;=> [2 4 6]
(transduce (map inc) + 0 [1 2 3])
;=> 9Chunked sequences ship as a real value type with the canon API surface (chunked-seq?, chunk-first, chunk-rest, chunk-next, chunk-cons, chunk-buffer, chunk-append, chunk). map, filter, take, keep, keep-indexed, and map-indexed propagate chunkedness end-to-end. Vector seqs and lazy range auto-chunk into 32-element chunks, so (chunked-seq? (seq [1 2 3])) returns true and a (reduce + (map inc (filter odd? (range 1e6))))-style pipeline runs end-to-end chunked without per-element cons-cell allocation.
Numbers
mino has the full Clojure numeric tower: 64-bit Long, 64-bit IEEE 754 Double, arbitrary-precision BigInt, exact Ratio, and arbitrary-precision BigDec. The bignum tier is backed by vendored MIT-licensed imath.
- Ratio literals (
1/2) parse to a realRatiovalue.ratio?istruefor non-integer ratios; an exact integer ratio reduces to its long or bigint value. - BigInt literals (
42N) parse toMINO_BIGINT. Cross-tier equality matches Clojure:(= 1 1N)istrue,(= 1.0 1)isfalse. - BigDec literals (
1.5M) parse toMINO_BIGDEC.(with-precision n)controls division rounding. - Plain
+/-/*/inc/decauto-promote to bigint on long overflow rather than throwing. This matches the semantics of Clojure's prime variants (+'/-'/*'/inc'/dec') and is a deliberate divergence from JVM Clojure where the unprimed forms raiseArithmeticException. For wraparound semantics use theunchecked-*family. Mixed-type tower dispatch (long × bigint, ratio × bigdec, etc.) follows the standard promotion order. mod/rem/quotpreserve the higher operand tier:(mod 10 3N)is1N,(mod 10 3.0M)is1.0M,(mod 3 1/2)is0N(the ratio result collapses to bigint when integer-valued).</<=/>/>=require numeric operands and short-circuit tofalsewhen any operand is##NaN, matching Clojure's unordered semantics.- Float arithmetic follows IEEE 754. mino has only one float tier (double);
(float x)and(double x)return values that compare equal.
Error handling
try/catch/throw work as expected, but mino improves on the JVM approach: throw accepts any value, and catch always receives a structured diagnostic map with stable keys like :mino/kind, :mino/code, and :mino/message. The original thrown value is accessible via ex-data:
(try
(count 42)
(catch e
(println (:mino/kind e)) ;; :eval/type
(println (:mino/code e)) ;; "MTY001"
(println (:mino/message e)) ;; "count: expected a collection, got int"
))Errors render with source snippets in the REPL, similar to Rust and Elm. Every error has a searchable code. See the Error Diagnostics guide for the full story.
ex-info, ex-data, and ex-message work transparently with both diagnostic maps and user-thrown values. finally and with-open work as expected:
(with-open [f (open "data.txt")]
(read-all f))Intentionally absent
These are design decisions, not missing features. See intentional divergences for the full rationale behind each:
- JVM interop surface.
Class/forName,bean,gen-class,..,*warn-on-reflection*, and host-array literals all assume a JVM. mino's host-method syntax ((.next obj),Type/static) goes through a capability registry the embedder controls. - Records and types.
proxyanddefinterfacedo not exist (no JVM classes to materialize).defrecord,deftype,reify, andinstance?ship as real value types. See the records section above. - Host-thread primitives are grant-gated.
future/promise/deliver/threadthrow:mino/unsupporteduntil the host callsmino_set_thread_limit(S, n>1). Standalone./minograntscpu_countby default, so REPL/script users see the canon surface without configuration.pmapstays absent. - Async agents.
agent/send/send-off/await/await-for/shutdown-agentsall work.sendenqueues onto the POOLED run-queue,send-offonto SOLO; each pool has its own worker thread but the per-state eval lock still serializes one action at a time across both pools. Each worker counts againstthread_limit(the embedder thread does not), sosendthrowsMTH001if the host hasn't granted a thread budget. Public C-API:mino_send,mino_send_off,mino_await,mino_await_for,mino_agent_error,mino_restart_agent.send-viais intentionally deferred (no public Executor type). See STM.
Recent additions
The v0.409 – v0.419 series closed the last canon-parity gaps and added an Erlang-inspired bit-syntax surface:
- Lazy-seq namespace scoping. A
(lazy-seq ...)form now captures the namespace where it was written and restores it during realization. Library helpers referenced from inside the lazy body resolve correctly even when the seq is forced from a foreign namespace. - Print dynvars. The remaining
*print-readably*,*print-meta*,*print-dup*,*print-namespace-maps*, and*flush-on-newline*dynvars are wired end-to-end with state-cached resolution. - Full math-context rounding modes.
(with-precision N :rounding mode (/ a b))accepts every JVM rounding mode::down,:up,:floor,:ceiling,:half-up,:half-down,:half-even(banker's),:unnecessary(throws when rounding occurs). - JVM Class/Member statics.
Long/MAX_VALUE,Math/sqrt,Double/parseDouble,java.util.UUID/randomUUID,java.util.List/of, and the rest of the JVM-Clojure preamble surface resolve without any custom interop wiring. Embedded-host capabilities (wall-clock time, env, exit) route through the canonical JVM names. - inst? / inst-ms / #inst literal. The
#inst "..."reader literal produces a component-map carrying:mino/instant truemeta;(inst? v)detects the marker and(inst-ms v)returns epoch millis without host-Date dependencies. - MINO_BYTES + bit syntax. First-class immutable binary-data type (
byte-array,bytes?,bitstring?) plus an Erlang-inspired bit- syntax surface (bits,bits-get,subbits,let-bits,#bytes "..."reader literal). See the dedicated Bytes and Bit Syntax page. This is one place where mino outshines JVM Clojure for the embedded-runtime niche. *clojure-version*+ AOT-compiler dynvars (*compile-path*,*source-path*,*compile-files*,*warn-on-reflection*,*unchecked-math*) for shape parity with JVM Clojure.
The v0.401 – v0.407 series tightened mino against JVM-Clojure-canon ports:
- Strict function arity is verified end-to-end
(both tree-walker and bytecode VM throw
MAR001/MAR002on missing or extra args). The diagnostic carries:mino/kind = :eval/arityso structured catch dispatch works. (is (thrown-with-msg? <re> body))and the JVM-shaped(thrown-with-msg? <Class> <re> body)inclojure.test. The class symbol is documentation-only; the regex matches the thrown value's message (mino lacks a class hierarchy).*print-length*and*print-level*dynvars.pr/prn/print/println/pr-strconsult them at top-level entry; collection printers truncate with...or collapse with#per JVM semantics.pcalls,pvalues, andalt!(the macro overalts!). Round out the parallel surface alongside the existingpmap; all share the(mino-thread-limit) <= 1sequential fallback.hash-combine,*math-context*, andwith-precision. Boost-style 32-bit hash mixer; bigdec division with(with-precision N (/ ... ...))using:half-uprounding (other rounding modes throwMHO002— implementing them properly is queued).unchecked-longon a bigint outside signed long range wraps modulo 2^64 instead of clamping through double. Matches JVM'sNumber.longValue()onBigInteger.clojure.test.checkquick-check now walks rose trees on failure and reports:shrunkwith the minimal counter-example.clojure.core.reducers/foldparallel branch. Vectors larger than the chunk hint partition into thread-budget-many chunks reduced concurrently and combined viacombinef.
Quick reference
| Feature | Status |
|---|---|
(ns ...) / require | Same |
(:key map) / #(inc %) | Same |
(.method obj) / Type/static | Same (capability-gated) |
(ex-info ...) / try/catch | Same |
core.async channels / go | Same (cooperative scheduling; <!! / >!! / alts!! park across threads when granted) |
(dosync ...) / (ref ...) / alter / commute | Same surface (single-version optimistic locking; see STM) |
(future ...) / (promise) / (thread ...) | Same when host grants threads (default in standalone) |
defmulti / defmethod | Supported |
defrecord / deftype / reify / instance? | Same |
1/2 / 42N / 1.5M | Real Ratio / BigInt / BigDec |
Plain + / - / * on long overflow | Same. Auto-promotes to BigInt. |
unchecked-+ / unchecked-- / unchecked-* | Same |