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

What works the same

Most daily Clojure idioms work as expected:

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? "")  ;=> true

The 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)))  ;=> 42

Supported 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:

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:

SyntaxStatus
#(inc %)Same
#'varSame
#_ formSame
^{:key val} / ^:key / ^TypeSame
#?(:clj ... :default ...)Same (active dialect keys are :mino and :clj)
#?@(...)Same (splice reader conditional)
'() / `(~x ~@xs) / @atomSame
2r1010 / 0xFF / 8r77Same (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/keywordSame (auto-resolved at read time)
#:foo{:b 1} / #::{:b 1} / #::alias{...}Same (namespaced map literals)

Data structures

Core data structures match Clojure semantics:

Differences:

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])
;=> 9

Chunked 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.

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:

Recent additions

The v0.409 – v0.419 series closed the last canon-parity gaps and added an Erlang-inspired bit-syntax surface:

The v0.401 – v0.407 series tightened mino against JVM-Clojure-canon ports:

Quick reference

FeatureStatus
(ns ...) / requireSame
(:key map) / #(inc %)Same
(.method obj) / Type/staticSame (capability-gated)
(ex-info ...) / try/catchSame
core.async channels / goSame (cooperative scheduling; <!! / >!! / alts!! park across threads when granted)
(dosync ...) / (ref ...) / alter / commuteSame surface (single-version optimistic locking; see STM)
(future ...) / (promise) / (thread ...)Same when host grants threads (default in standalone)
defmulti / defmethodSupported
defrecord / deftype / reify / instance?Same
1/2 / 42N / 1.5MReal Ratio / BigInt / BigDec
Plain + / - / * on long overflowSame. Auto-promotes to BigInt.
unchecked-+ / unchecked-- / unchecked-*Same