(rontolisp) docs

Semantics

Every Clojure form lowers, at file-read time, to the Common Lisp core forms the rest of rontolisp already consumes; no backend learns a Clojure name. This page is the map of those lowerings and the forms refused by name.

Names and calls

An identifier lowers to a symbol behind the c% prefix, so Foo and foo stay apart and no name reaches a core-form case label. defn is a defun called directly; a head-position call to a parameter, a let/loop binding or a def'd variable holding a real function is a funcall of the value cell, so (defn call-it [f x] (f x)) runs; a variable that may hold a collection goes through the prelude dispatcher instead (rontolisp::%clojure-call: functions through apply, sets/maps/vectors/keywords/symbols through their lookup, like IFn; a keyword or symbol takes one or two arguments and signals the arity error otherwise); calling a declared-but-never-defined name signals the oracle's Attempting to call unbound fn (in the REPL the call stays direct, since a later input may define it). def is a top-level setq -- inside a body it still sets the global when the body runs.

defn with several arities is one defun per arity plus a dispatch defun picking by argument count (a single variadic clause takes any count past its fixed parameters); any other count signals. fn with several arities is one lambda dispatching the same way, and a named fn binds itself for self-calls; #(...) reads as the oracle's (fn* [p1__N# ...] (body)), a lambda over the parameters up to the highest %N used plus a rest parameter for %&, its body forms wrapped as one call. declare names what is defined below, so a definition may use one.

Namespaces and files

Every namespace has its own vars. A definition belongs to the current namespace (user until an ns or in-ns switches it); a name resolves to a local, then to the current namespace's own var, then to a referred one, and alias/name or full.name/name reaches another namespace's var -- a private one (defn-, ^:private) is refused, like the oracle's compiler. A var of user lowers to c%name, any other to c%ns/name, which is what a Common Lisp file loading the program calls.

A require, use or ns clause naming a namespace the program has not declared loads its file: my-app.core is my_app/core.clj, read from the first source root holding it -- the directory the entry file's own namespace names (src for src/demo/main.clj declaring demo.main, the file's directory without an ns), then the :paths of the nearest deps.edn at or above the entry file (["src"] when it names none), or src under the working directory when there is no deps.edn. A file lowers once per program: its definitions stay ahead of the form that required it, while its other top-level forms run when the require runs -- including a require inside a function body, which loads when the body runs. A second require loads nothing; :reload runs the file again (def resets, defonce keeps its root) and :reload-all re-runs its dependencies first, like the oracle. A file without an ns form defines into the requiring namespace. use and :refer :all bring in every public var; a file no root holds, a cycle of requires and a refer of a missing or private var are errors in the oracle's words.

*ns* is the current namespace as a value, switched by ns and in-ns where they run and read when the reading code runs, so a function answers its caller's namespace. While a required file runs, *ns* is rebound (the file's ns switches it, and the requiring namespace is back afterwards), *file* holds the file's path below its root (my_app/core.clj) and *source-path* its name; the entry file's *file* is its absolute path. the-ns, find-ns and ns-name reach a namespace by name.

# deps.edn holds {:paths ["src"]}; demo.main and demo.main-test require demo.lib
rontolisp src/demo/main.clj          # roots: src (its ns), src (deps.edn)
rontolisp test/demo/main_test.clj    # roots: test (its ns), src (deps.edn)

Binding

let is a let* (Clojure's let is sequential); letfn is one labels over pre-scanned entries, so siblings call each other; loop/recur is a labels self call, constant-stack on every backend, with sequential inits. recur also reaches a named or anonymous fn, a defn clause, a letfn entry or a lazy-seq body of arity 0 (each multi-arity clause its own target); a wrong count is a named refusal. Parameters and bindings destructure: a vector pattern binds positionally through the seq view (& the rest as a seq, itself a pattern; :as the whole), a map pattern through the table-aware read (:keys/:syms/:strs, explicit locals, :as, :or defaults) -- in let, loop and fn/defn parameters alike; nested patterns recurse. Malformed shapes are named refusals.

Macros

defmacro defines a compile-time expander, stored beside the lowering: each call site expands datum to datum while lowering -- the argument forms travel quoted into one application of the lowered body, the answer decodes back to a datum and lowers like any other form -- so every backend, and the interpreter's own eval of a macro call, runs expanded code. Parameters bind unevaluated forms (& rest, destructuring and several arities like defn; a docstring and an attr map are skipped); &form/&env are refused. A body sees the core builtins and the clojure.lisp library, not the program's own definitions. The definition also registers a runtime table entry of the same expander, answers nil, and works session-wide; a call above its definition is an error, a macro has no function value, and a later def/defn of the same name wins back the call sites. A defmacro of a core name (with-out-str, when-not, inc, declare, ...) shadows it from its definition on, like the oracle's form-by-form compile: a call site above the definition, and a syntax-quote in a macro defined above it, keep the core meaning. clojure.core/name always names the core var, whatever the program defines under that name. A special form, or a head the reader spells (deref for @x, with-meta) cannot name a macro. A fn macro is allowed: it captures fn call sites, never #(...) (read as the special form fn*).

`form builds a form as data over the mangled namespace: a symbol naming a var the defining namespace sees qualifies with that var's namespace like the oracle (a special form stays bare; a core name spells clojure.core/name, any other unresolved spelling the defining namespace), ~ inserts its form's value, ~@ splices a sequence into the enclosing list, vector, map or set, and each x# binds one (gensym "x") per syntax-quote -- one symbol per expansion, the same at every occurrence within it. An unquote outside any syntax-quote is an error, as is a splice outside a sequence. macroexpand-1 expands once and macroexpand to the fixpoint, each answering the expansion as the mangled data itself, so = against a quoted form holds and printing spells the oracle's lowercase; gensym answers a fresh uninterned symbol per evaluation.

Threading and flow

->/->> insert the value second/last, a bare name or keyword calling/reading with it; as-> rebinds its name step by step (nested lets, so shadowing matches the oracle); doto answers its (unchanged) target; cond->/cond->> thread only on truthy tests; some->/some->> stop at nil but not at false. if/when/cond/do/and/or are the core forms: every test treats nil and the false object as falsey, and cond keeps the lenient reading (an odd trailing arm is the default). list* folds cons over the seq view.

Iteration

doseq iterates the seq view for side effects and answers nil: one loop per binding pair nested left to right, the body an implicit do. Each loop steps through a lazy input one element at a time, so a :while stops an infinite one. dotimes binds 0 below its count the same way and answers nil; the count runs through truncate first, so 2.5 counts 0 1 and a non-number signals, like the oracle's intCast. for answers its body over every combination: realized at once, a strict list, while every collection it steps over is strict (an empty one is nil, where the oracle prints ()), and a lazy seq from the first lazy collection on, realized as it is consumed -- so first/take realize only what they answer, and an infinite collection ends behind them. Each pair takes any collection the seq view takes, and patterns destructure like let. The :when/:while/:let modifiers trail their binding in order: :when skips the element, :while ends its level (an outer level's ends the whole form), :let binds sequentially; any other keyword is refused. dorun walks a collection to its end for effect (a lazy one realizes) and answers nil, doall answers the collection itself.

Collections

A vector literal is a vector call; a map literal an equal hash table, never mutated in place -- every verb builds a fresh one, so persistence holds observably; a set literal the same table with each member stored under itself, wrapped so verbs tell a set from a map. Keys find each other by =: a vector, list, map or set key is stored under the first = key of its kind the program stored, so an equal table finds it. A vector is never mutated either: assoc, update, assoc-in and update-in on one copy it whole with the index replaced, the index equal to the count appending. A keyword is its spelling wrapped as (:C%KEYWORD name): data compared by equal, and in call position ((:k m), with an optional default) or as a function value the map lookup. Arrays are general: (make-array Class dim...) builds a general array ignoring the class, read through aget, written through aset, measured through alength (the book's interop.clj shape; only the Clojure spellings are new, so all four backends).

The seq family runs over list views of every collection: lists pass through untouched, vectors and strings coerce, maps contribute one two-vector per entry and sets one member per element (both in the table's walk order, unspecified); nil and false are empty; anything else signals like the oracle. Strict collections coerce up front, while a lazy seq (lazy-seq, lazy-cat, repeat, cycle, iterate, repeatedly) realizes one element at a time through the same view: take steps through it and terminates on infinite seqs, drop/first/rest/next/seq realize through it, and cons/concat/map/filter, remove, keep, keep-indexed, map-indexed, distinct, interpose, partition and interleave answer lazy again when any input is lazy (strict lists otherwise). A lazy-seq body runs at most once per seq object; printing realizes a lazy seq like the oracle (an empty one prints (), an infinite one without end). There is no chunking. count/empty?/= reach maps and sets (= deeply and structurally); get takes an optional default and reads maps, sets, vectors, strings and nil.

A lazy input reaches every seq verb. The verbs that walk the whole collection (count, last, sort, apply, reverse, set, frequencies, reduce, into) realize it first -- an infinite one never answers, like the oracle's -- and the ones that stop early (second, nth, some, every?, take-while, drop-while, zipmap, positional destructuring, doseq/for) step through it, so an infinite input still answers.

State and dynamic scope

Reader metadata (^:private, ^:dynamic, ^{...} attr maps, type hints) on a name or a local parses and drops: it never affects dispatch, except that ^:dynamic on a def/defonce/defn name marks the var rebindable -- a ^:dynamic defn keeps its direct definition but its calls go through the var, so binding reaches them. Only binding rebinds through it. defn- is a private-by-convention defn; def takes a docstring and an attr map like defn; defonce is def unless bound, so a reload keeps the root.

Value metadata is real: with-meta answers a copy carrying the map, meta reads it, vary-meta updates it, and reader metadata on a vector, map or set literal attaches like with-meta. = ignores it. Two deviations: a value derived from a copy (assoc, conj, ...) starts without metadata, where the oracle keeps it, and a symbol carries none (its with-meta answers the symbol).

#'x ((var x)) answers the var of a program definition, one object per name that prints #'ns/x, derefs and invokes through its root. Its metadata is what the newest definition above the #' recorded: a def/defn/defn-/defmacro gives :arglists, the docstring as :doc, the name's metadata and attr map (evaluated where the definition stands, so ^{:test (fn [] ...)} works), :line/:column/:file, :name and :ns; test calls its :test fn. A local is no var. A name no program definition claims, or a clojure.core/ spelling, is the core var (#'clojure.core/inc): its root is the core value, a macro's root signals, and its metadata is :name, :ns and a macro's :macro. A core var with no value here (#'all-ns) is refused.

A ref is the atom cell with a transaction discipline: dosync opens the extent (single-threaded, so no retries and no isolation), alter/commute apply through the :validator (a failed one signals and writes nothing), ref-set replaces through it, and ensure answers the ref -- every verb requiring the extent. An agent is the same cell updated by send/send-off, which apply at once (there is no thread pool, so async ordering is out) and answer the agent; *agent* is bound while one runs. await rendezvous and shutdown-agents answer nil. future/delay/force/promise/deliver stay refused by name, and so does proxy-super (proxy methods take no super handle).

binding rebinds ^:dynamic vars and the clojure.core specials with dynamic extent; anything else is refused. *out*/*in*/*err* are *standard-output*/ *standard-input*/*error-output*; the flags hold the oracle's values under clojure -M (*print-length* nil, *assert* true, *data-readers* {}, *command-line-args* the program's arguments, *clojure-version* 1.12.6, ...), and the printer honours *print-length*, *print-level*, *print-readably*, *print-meta* and *print-namespace-maps* (a map whose keys share a namespace prints #:a{:b 1}), assert reads *assert* where it expands, and the others are plain values. *ns*, *file* and *source-path* follow the load (above), *repl* is false, and *1/*2/*3/*e are nil outside the REPL. with-in-str binds *in* to a string reader, which read-line, read and (.read *in*) take from. defstruct holds its key vector behind the name; struct/struct-map build fresh maps over it. with-out-str binds *standard-output* to a string stream (never a literal with-output-to-string) and answers what printed; time reports Elapsed time: N msecs (a double count, like the oracle's) and answers its value. with-open binds and closes in reverse order through unwind-protect, calling the close method (Java closeables need the JVM, like all interop); (. stream write x) prints through princ on every backend, (.readLine stream) reads through read-line (nil past the end, like the oracle) and (.read stream) answers the next character's code (-1 past the end).

Protocols, records and types

A defprotocol declares methods; each method lowers to a dispatcher over the target's tag (the multimethod shape without the hierarchy search: an exact tag match, then the Object row). extend-protocol/extend-type/extend add rows under a target's tag; satisfies? tests membership. Extend targets are the kinds class answers (String, Number, Boolean, Keyword, Symbol, Character, Map, Vector, Set, List/Seq, plus nil and Object as the miss default) and known record/deftype names; anything else is a named refusal. A miss with no Object row signals, like the oracle. Each method takes one parameter vector (several arities stay refused). A protocol declared :extend-via-metadata true also finds a method in the target's metadata under the namespace-qualified method symbol -- after an implementation in a defrecord/deftype/reify body and before the extension rows, like the oracle (see defprotocol).

A defrecord value is a map with a type tag: the entry table every map uses, wrapped as (:C%RECORD tag fields table class), so the map verbs read through it (get/contains?/keys/vals/count/seq/select-keys read the entries; assoc/update/conj/merge rebuild the table and keep the tag; dissoc keeps the record while every declared field is still present and drops to a plain map otherwise, like the oracle). = compares two records by tag plus entries and never equals a plain map, like the oracle. A deftype shares the shape with an opaque tag: reads miss, writers and seq/count/empty? signal, and = is identity, like the oracle. reify answers one fresh tag per evaluation with a row per method in each protocol's table. Constructors are mangled functions: ->Type positionally, map->Type from a map (records only -- the oracle defines none for deftypes); (Type. ...)/(new Type ...) rewrite to ->Type. instance? of a record/deftype name tests the tag; (.-field x) reads the field table (missing fields signal, like the oracle). Inline method bodies see the fields as locals (an explicit parameter shadows its field, like the oracle); type hints (^String, ^H) parse and drop, never affecting dispatch.

A deftype field marked ^:unsynchronized-mutable or ^:volatile-mutable is assignable: (set! field value) inside the type's own inline methods writes it and answers the value, and a later read (in this call or after another method's write) sees the new value. Such a field is private to the methods (.-field misses it), a closure created in a method (fn, #(), letfn, reify, lazy-seq, for, dosync) copies it at creation, and defrecord refuses the markers, all like the oracle. ClojureScript's ^:mutable is no marker. set! of a local, a parameter or an immutable field is the oracle's Cannot assign to non-mutable: ...; of a non-dynamic global it signals Can't change/establish root binding of: ... with set at run time.

Reading

read-string reads the first datum of a string and read one datum from a reader, at run time on every backend, answering what a quote of the same text answers: the same numbers, strings, characters, keywords (::kw in the calling namespace) and collections, metadata dropped, #_ discarding. A record literal builds the record of a class the program defines; #= read-time evaluation, reader conditionals and tagged literals are refused like in source. A reader is a clojure.java.io/reader, *in*, or a java.io.PushbackReader/BufferedReader over one or over a java.io.StringReader, which is a stream on every backend; read leaves it right after the datum. str of a collection quotes the strings inside it, like the oracle's, so what spit writes reads back. eval and load-string stay absent: no compiler runs at run time.

Not yet

Each refusal names the missing design, never unknown name:

RefusedMessage shapeWhy
end-less rangeinfinite range is not supported: range needs an endan infinite seq cannot be spelled strictly -- spell it with iterate
transient, persistent!, assoc!, dissoc!, conj!, disj!transients are not supported yet: ...no transient runtime behind the tables
definterface, gen-class, gen-interfaceprotocols are not supported yet: ...no interface generation on any backend
multi-arity protocol methodsmulti-arity protocol methods are not supported yet: ...one parameter vector per method
set! of a core var that is no special (inc), of a host fieldset! of a var is not supported yet: ..., set! of a host field is not supported yet: ...no var to assign; the java: surface has no field write
future, future-done?/future-cancelled?, delay/force, promise/deliverby nameno thread pool, lazy memo cells or blocking rendezvous on any backend
proxy-super outside a proxy methodproxy-super outside a proxy methoda proxy-super calls the superclass implementation on the method's this
proxy with a second class, a duplicate method, a final superclass... is a class, not an interface, proxy defines method ... twice, proxy cannot extend final class ...one superclass only, one body per method name, no final superclass
toString/equals/hashCode in an interface-only proxyproxy cannot override ... yetjava:proxy keeps Object's three, so the body would never run (a class proxy runs it)
a variadic-only static member, instance method (Class/.m) or constructor (Class/new) as a value... is variadic and has no value formno rest-spread reaches java:static, java:call or java:new
&form/&env in defmacro parametersby namemacros receive no compilation environment
::alias/kw with an unknown aliasInvalid token: ...only required aliases, the file's own ns and known namespaces resolve
--no-gc buildsby namethat backend has no pairs, symbols or closures
file-seq, clojure.java.io (except reader)file-seq / unknown name: clojure.java.io/...no directory walks; only reader resolves, opening a file-stream reader

Errors and positions

A lowering error names the innermost form's position (file:line:column when the file is known); the reader's errors are prefixed the same way.