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.
The macro-family special forms are reachable by their fully qualified names too: (clojure.core/let ...), (clojure.core/when ...), and (clojure.core/fn ...) evaluate identically to the bare forms. Syntax-quote qualifies those forms to clojure.core/X the way Clojure does, while the true special forms (if, do, recur, quote) stay bare.
(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.- Parks in
catch/finallybodies are not supported - Parks nested in plain function-call arguments are lifted automatically (left-to-right order preserved); parks under a macro call or inside map/set literals still need 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_option(S, MINO_OPT_THREAD_LIMIT, n). The standalone ./mino binary grants cpu_count by default, so REPL and script users see the canonical surface working without configuration. 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. 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. 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/decthrow on long overflow, matching JVM Clojure's unprimed contract. The primed variants (+'/-'/*'/inc'/dec') auto-promote to bigint. 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 two float tiers: 64-bit
doubleand 32-bitfloat.(float x)returns a 32-bit value;float?matches both tiers whiledouble?matches only the 64-bit tier.
Error handling
try/catch/throw work as expected, but mino differs from 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_option(S, MINO_OPT_THREAD_LIMIT, n>1). Standalone./minograntscpu_countby default, so REPL/script users see the canon surface without configuration. - 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.
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. Throws (use +' to auto-promote). |
unchecked-+ / unchecked-- / unchecked-* | Same |
Divergences: jvm-statics
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.
Divergences: metadata
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.
Divergences: ordering
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
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
Divergences: pprint
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
Divergences: reader-conditionals
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.
Divergences: spec
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.
Divergences: types
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