Systems (asdf)
The asdf package provides a limited, API-compatible subset of ASDF, the
Common Lisp build facility: you describe a multi-file project once in a
NAME.asd file with asdf:defsystem,
and asdf:load-system loads the
files in dependency order — on every backend. Real ASDF is not ported (it
depends on CLOS, the condition system and the pathname API, none of which
exist here); instead, .asd files are parsed as plain data and the supported
defsystem subset drives the same machinery as load/require. A .asd
that stays inside the subset works unchanged.
| Operator | Purpose |
|---|---|
asdf:defsystem | Define a system: :depends-on, :serial, :components |
asdf:load-system | Load a system (dependencies first, files in order, idempotent) |
A complete project
app/
my-app.asd
package.lisp
main.lisp
run.lisp
registry/base/
base.asd
base.lisp
;; app/my-app.asd
(defsystem :my-app
:version "0.1.0"
:depends-on (:base)
:serial t
:components ((:file "package")
(:file "main")))
;; app/package.lisp
(defpackage :my-app (:use :cl) (:export :run))
;; app/main.lisp
(in-package :my-app)
(defun run () (print (base:double 21)))
;; app/run.lisp
(asdf:load-system :my-app)
(my-app:run)
Run or compile the entry file; the same directive works on all four backends:
rontolisp app/run.lisp --system-path registry/base # interpret
rontolisp app/run.lisp --system-path registry/base -o Prog.class # JVM
rontolisp app/run.lisp --system-path registry/base -o app.wasm # WASM
my-app.asd is found next to run.lisp; the :base dependency system is
found through --system-path. On the compile path the whole system (its
dependency first) is spliced into the program at compile time, exactly like
the compile-time load include, so the JVM and WASM compilers see every
defun natively.
The system search path
asdf:load-system looks for NAME.asd in, in order:
- the directory of the file doing the
load-system(likeload), - the directories given with
--system-path(several can be joined with the platform path separator, likePATH), - the directories in the
RONTOLISP_SOURCE_REGISTRYenvironment variable (same format).
A dependency system's .asd is searched starting from the depending system's
directory, so sibling systems in one registry directory find each other.
Downloading with quickload
To skip the manual download, ql:quickload
fetches a system (and its dependencies) from the real
Quicklisp distribution and then loads it through
exactly the machinery above:
$ rontolisp
CL-USER> (ql:quickload "split-sequence")
(split-sequence)
CL-USER> (split-sequence:split-sequence #\, "a,b,c")
("a" "b" "c")
The Quicklisp dist metadata drives the download (systems.txt for dependency
resolution, releases.txt for the tarball URLs); each release is extracted and
cached under ~/.rontolisp/quicklisp/ (override with RONTOLISP_QUICKLISP_HOME),
so a repeat quickload does no network I/O. The download runs at interpret time
or compile time (Java-side): a compiled program has the sources spliced in and
never fetches at runtime, so ql:quickload works on all four backends. Because
loading still goes through the asdf subset, the same limitations apply — a
downloaded library only loads if its sources stay inside the supported subset
below.
Adding a dist (Ultralisp)
Quicklisp's dist format is spoken by more than one distribution, and
Ultralisp — rebuilt every few minutes, so a library
lands in it the day it is published — is the usual second one. It is opt-in:
a program installs it with
ql-dist:install-dist, the
same call real Quicklisp takes,
$ rontolisp
CL-USER> (ql-dist:install-dist "http://dist.ultralisp.org/" :prompt nil)
"ultralisp"
CL-USER> (ql:quickload "circular-buffer")
(circular-buffer)
and an invocation with nowhere to put a form (rontolisp test SYSTEM, a build
script that must not edit the sources it compiles) names it on the command line
instead — --dist ultralisp, several comma-separated, or the
RONTOLISP_DISTS environment variable. Either channel also takes the URL of any
other Quicklisp-format distinfo.
The dists are searched in installation order, per system: ql:quickload
takes each system — and each dependency — from the first dist that lists it, so
adding one supplies the names Quicklisp does not have and changes where nothing
else comes from. Quicklisp is installed first unless it is named explicitly, so
--dist ultralisp,quicklisp is how Ultralisp's copy of a library both dists
carry wins. A dist's index is downloaded only when a lookup actually reaches it,
and each dist caches under its own ~/.rontolisp/<dist>/ (RONTOLISP_DIST_HOME
overrides the base; RONTOLISP_QUICKLISP_HOME still overrides the quicklisp
one). Because the index is then cached forever,
ql:update-dist is what makes a
fast-moving dist worth having:
$ rontolisp -e '(ql:update-dist "ultralisp")'
Everything else is unchanged: the download happens at interpret time or compile time, so an installed dist works the same on all four backends, and a downloaded library still has to stay inside the supported subset below.
What is (and is not) supported
-
.asdfiles are parsed as data:defsystem(bare orasdf:-qualified),in-package/defpackageforms (skipped),register-system-packagesforms (which record "this package lives in that system" — read when a package-inferred system turns adefpackagedependency into a system name, and otherwise inert, since a package is found through its owndefpackage), and top-leveldefparameters of pure literal/conditional values (evaluated into a parse-time environment) may appear.#+/#-feature conditionals work (evaluated against the target backend's features, see Data Types), a#.read-time-eval form is resolved where its value is used: in a clause that decides what gets loaded it is resolved against thosedefparameters (the(:file #.*string-file*)idiom), and an unresolvable one is an error naming the file and the clause; in ignored metadata (:long-description,:version,:perform, …) and at top level (an ASDF version guard) it is dropped unevaluated and unremarked. A:depends-onentry may be(:feature EXPR DEP), contributing its dependency only when the feature expression holds, or(:version NAME "1.2.3"), resolving to the plain dependency (the version constraint is not checked — the:versionoption is ignored metadata here, so there is nothing to check against). A top-level(defmethod perform ...)hook is tolerated and ignored (there is nooperatemachinery for it to run on; any other method name is an error), and a top-leveldefclasswhose superclasses are documentation component classes (ASDF'sdoc-file, or one declared earlier in the same file) declares its name as a component type whose entries participate in ordering but contribute no source, like:static-file—:doc-fileand:html-filework without adefclass. Any other top-level form is an error naming the file. -
A
.asdmay announce features: a top-level(eval-when (:load-toplevel :execute) (pushnew :my-feature *features*))(or a barepushnew/push) before adefsystemdeclares that feature for every system defined after it in the file — the same effect as writing:rontolisp-features (:my-feature)on those systems. The declaration reaches the system's own:if-feature/(:feature ...)clauses and the reading of its component files — carrying the announcement out of the.asd, which is the half the reader cannot do for itself. (A#+/#-in the same.asdsees the push too, but through the reader's own handling of a source's announcement — see Data Types.) It does not reach a dependency, which declares its own. Aneval-whenwhose situations are only(:compile-toplevel)is inert (ASDF loads a.asd, it never compiles one), and any other form inside theeval-whenis an error naming it. -
defsystemsupports the metadata options (ignored, except that a plain-string:versionis read back byasdf:component-version),:depends-on,:defsystem-depends-on(systems real ASDF loads while the.asdis read: located the same way, loaded before:depends-on, never a sideway dependency of the system, and a built-in shim named there announces its features to this system —("trivial-features")puts:unixand:little-endianin force for its clauses and component files),:serial,:pathname(a literal directory prefixed to every component, so a system whose sources live insrc/can name them bare) and:componentswith:file/:module/:static-fileentries; a component may carry:if-feature expr, which drops the component's files when the feature expression does not hold (how libraries gate CLOS-only files behind(:or :sbcl ...)) while keeping its place in the dependency order. The test-op wiring options are the one op with machinery behind them::in-order-to ((test-op (test-op ...)))and:perform (test-op (o c) ...)are recorded and driven byasdf:test-system(any other operation, a qualifiedtest-op :aftermethod, or a#.in the body stays tolerated and ignored — there is still no generaloperatemachinery). A:versionvalue may be any literal form including ASDF's(:read-file-form ...)indirection (never inspected — only a plain string is recorded). Anything else (:around-compile, ...) is an error naming the clause. -
:class :package-inferred-systemis supported — the style ningle, rove and array-operations use. Such a system has no:componentsat all: a sub-system name is a file path under the system's directory (my-lib/mainismain.lisp,my-lib/util/textisutil/text.lisp, both below:pathnamewhen the system has one), and that file's owndefpackagenames its dependencies — every package in:use,:mix,:reexport,:use-reexportand:mix-reexport, plus the first argument of each:import-from/:shadowing-import-from. A package name becomes a system name: what aregister-system-packagesform declared, otherwise the downcased package name itself (cland friends drop out). Only the forms up to the first package definition form of each file are read, so the common(in-package #:cl-user)header before thedefpackageis fine; a file with nodefpackage/uiop:define-packageat all is an error. No other:classis supported, and a package-inferred system that also lists:componentsis an error. -
Loading a system twice is a no-op; circular
:depends-onchains are detected and reported — including one written as a cycle between two sub-systems'defpackageforms. -
The compile path requires a literal, top-level
(asdf:load-system NAME); the interpreter also accepts a computed name at runtime. Both accept and ignore trailing keyword options (:verbose nil,:force t,:silent t), which real libraries pass when they load a system at runtime. A nested or computedload-system/ql:quickloadin a compiled program answersnilwhen the system was already spliced and signals otherwise — nothing can be loaded at run time there. -
The component metaobjects are real at run time.
asdf:find-systemanswers a memoized CLOS instance per system (eqacross calls) over real classes —asdf:component,asdf:child-component/asdf:parent-component,asdf:module,asdf:system,asdf:package-inferred-system,asdf:source-file,asdf:cl-source-file,asdf:static-file— sotypep,typecaseanddefmethodspecializers over them work on every backend. The readersasdf:component-name,asdf:component-version(the declared:versionstring, when it was written as a plain string),asdf:component-pathname,asdf:component-children(onecl-source-fileper component file, in load order),asdf:component-sideway-dependencies,asdf:component-parentandasdf:component-systemwalk the model;asdf:registered-systemslists every registered name, andasdf:*user-cache*is external andnil(there is no fasl cache). This is the component model rove's system-driven test runner reads. -
asdf:test-systemruns the recorded test-op wiring.asdf:test-systemloads the system, follows its:in-order-totest-op chain, and runs each recorded:perform (test-op (o c) ...)body with the component bound to the system metaobject. On the compile paths a literal top-level call splices the test systems too. -
Compiling tree-shakes the system. A function, variable or constant a loaded system defines but your program never reaches — following names through the source, including quoted symbols and whole string literals — is left out of the
.class/.wasm— classes, generic functions, methods, conditions and structures included (a method also leaves when no reachable code can create an instance it applies to). Compile with--no-prune(or--dynamic) to keep every definition; see Compiling to the JVM for the one consequence. -
The load-context variables hold the file being loaded, on every backend. While a file is being loaded,
*load-pathname*holds the pathloadwas called with and*load-truename*the path it resolved to; the enclosing file's values come back when it finishes, and outside a load both arenil— read at top level or from a function the load defined and your program calls later. A component is loaded by its resolved path, so both variables hold whatasdf:component-pathnameanswers for it, which is what lets a test framework correlate the definitions of a file with the system that owns it. This works the same on the compile backends: a spliced file's forms are compiled with its own load context, because nothing is being loaded at run time there for them to read otherwise.*compile-file-pathname*and*compile-file-truename*are alwaysnil— there is nocompile-filehere. Libraries use the(or *compile-file-truename* *load-truename*)idiom to find data files beside their own sources. That works at read time too: a#.form — the way that idiom is normally spelled, so the file is read while the source is being read — sees its own file's load context on every backend, so a library can bake in a data file that ships next to it.
Built-in shim systems
Some Quicklisp libraries depend on per-implementation portability layers that
cannot know rontolisp from their side. rontolisp ships those as built-in
ASDF systems: asdf:load-system/ql:quickload (and a :depends-on from a
real library) resolve the name to a bundled shim instead of downloading it.
| System | What the shim provides |
|---|---|
usocket | the socket API over rontolisp:tcp-* (see the TCP guide) |
trivial-gray-streams | the portable Gray-stream classes/generics (base-class hierarchy incl. the binary/input classes and trivial-gray-stream-mixin, the read/write/sequence generics, stream-file-position + its (setf ...) writer), adapting onto rontolisp's own protocol — what the stream-taking built-ins dispatch to for a CLOS instance stream (see Gray Streams) |
closer-mop | the class-introspection readers over the real class metaobjects (find-class / class-of answers): classp, class-slots, slot-definition-name/-initargs/-type/-readers/-initfunction, class-name, class-direct-superclasses, class-direct-slots, class-direct-subclasses, class-finalized-p, ensure-finalized. A legacy tag-symbol designator still answers (name declared-type) pairs. The flat closer-common-lisp re-export package (nickname c2cl: all of cl overlaid with these, closer-mop winning collisions) is always registered, so a (:use :closer-common-lisp) package works |
flexi-streams | pass-through streams (a flexi stream IS the underlying stream), plus the REAL in-memory octet pair: make-in-memory-input-stream over the vector-stream class, and make-in-memory-output-stream / get-output-stream-sequence (which resets the stream, as upstream does). See Gray Streams |
babel | the UTF-8 codec in both of babel's layers. The everyday one: babel:string-to-octets/octets-to-string (with :start/:end/:errorp), babel:string-size-in-octets (:max included), babel-encodings:*default-character-encoding* (:utf-8) and babel:list-character-encodings. The MAPPING layer underneath, for a library that decodes incrementally and has no whole octet vector to convert: babel-encodings:get-character-encoding, enc-max-units-per-char, lookup-mapping over babel:*string-vector-mappings*, and the code-point-counter / octet-counter / decoder / encoder it answers, plus the babel:unicode-char type. Malformed input signals babel's own conditions (babel:character-decoding-error and the leaves end-of-input-in-character, character-out-of-range, invalid-utf8-starter-byte, invalid-utf8-continuation-byte, overlong-utf8-sequence; babel:character-encoding-error on the way out), and babel-encodings:*suppress-character-coding-errors* — which every :errorp defaults from — replaces the signal with the substitution character. An encoding and a mapping are both just the encoding NAME here: real babel generates 40+ code pages from 20,000 lines of tables, while rontolisp has one character model (a character IS a Unicode code point, the wire form is UTF-8), so the shim implements that codec, treats the :latin-1/:us-ascii aliases as the code-point identity they are for the octets they can represent, and signals on any other :encoding rather than handing back mis-coded bytes |
float-features | single-float-bits/bits-single-float and the double variants over the IEEE 754 bit primitives (interpreter + JVM; the WASM numeric model cannot carry 64-bit bit patterns) |
bordeaux-threads (nicknames bt and bt2) | both API namespaces of the one shim. The locking subset — make-lock, acquire-lock, release-lock, with-lock-held, *supports-threads-p* — rides rontolisp:make-mutex and friends; thread creation — bt2:make-thread (with :initial-bindings), join-thread, threadp, thread-alive-p, destroy-thread — rides rontolisp:make-thread, a real virtual thread on the interpreter and the JVM. On the single-threaded WASM backends the thread entry points signal at call time and bt:*supports-threads-p* is nil. make-lock returns a reentrant lock (upstream's is not) — so make-recursive-lock and with-recursive-lock-held are the same object and the same expansion — acquire-lock's :wait-p is ignored — the acquisition always blocks — and an :initial-bindings value form must be a quote form or self-evaluating (anything else would need the new thread's dynamic environment and signals) |
uiop | ASDF's portability layer, registered as 15 sub-packages and 429 exports. See The uiop Package for what is implemented; every other export resolves and signals uiop:not-implemented-error naming the operation, so a library that merely names one still loads |
swank | a stub, and only so that a library depending on it can load: swank:create-server signals ("rontolisp cannot serve a remote REPL") and swank:stop-server is a nil no-op. Real swank is SLIME's server half, whose own .asd is a program the defsystem-as-data front-end cannot read -- without the stub, (ql:quickload "clack") fetches the SLIME tarball and dies on it |
mgl-pax-bootstrap | the mgl-pax package (nickname pax) as a stub, so a library documented with mgl-pax can load (trivial-utf-8, a uuid dependency, hard-depends on it; the real system's .asd declares :around-compile, a compile hook outside the defsystem-as-data subset). pax:define-package acts as defpackage, pax:defsection defines its section name as a nil variable and exports the section's (symbol locative) entries — mgl-pax's documented default, and how such libraries export their public API — and the PAX-World registration helpers are nil no-ops. No documentation is generated |
trivial-features | the platform-feature announcement upstream trivial-features exists to make portable, without the CFFI probing: naming it in :defsystem-depends-on (or :depends-on) declares :unix and :little-endian for the depending system's own clauses and component files, and pushes both onto *features* at run time. :unix because every backend's file/path/environment surface is POSIX-shaped and none is Windows; :little-endian because the only places a program can see machine layout (WASM linear memory, a reactor's :bytes boundary) are little-endian by the wasm spec; :64-bit because every backend has 64-bit fixnums and every pointer-shaped value is 8 bytes wherever pointers exist at all — cffi's types.lisp reads exactly this to give :size a base type. The CPU name :x86-64 stays absent — rontolisp has no instruction-set surface to describe — and so does :32-bit, which rontolisp is never. Upstream's own .asd cannot load here at all: it ends in (error "Sorry, your Lisp is not supported") for an implementation it does not recognize |
trivial-garbage (nickname tg) | GC finalizers as honest no-ops: tg:finalize registers nothing and returns the object, tg:cancel-finalization is a nil no-op. tg:make-weak-hash-table answers an ORDINARY hash table (:weakness and :weakness-matters are dropped, every other argument reaches make-hash-table) and tg:hash-table-weakness answers nil: weakness is not observable from within Common Lisp, so the only difference is that the table retains more. No backend exposes GC hooks — and Common Lisp gives finalizers no guarantee of ever running, so a conforming consumer must already work when they never fire. Practical consequence for dbd-postgres (its consumer): a leaked prepared statement lives until the connection closes; call dbi:disconnect explicitly |
cl+ssl | the CLIENT side of the TLS library every CL HTTP client (dexador, drakma, ...) reaches TLS through, over rontolisp:tls-upgrade — the real cl+ssl is a CFFI binding to OpenSSL and cannot load here. cl+ssl:make-ssl-client-stream upgrades an already-connected stream to TLS against its :hostname (which is required); make-context :verify-mode + with-global-context + ssl-check-verify-p carry the verify mode, so a client's "insecure" knob (e.g. dex:*not-verify-ssl*) reaches the primitive's :insecure; ensure-initialized is a no-op. What has no backing signals instead of being accepted and ignored: client certificates (:key/:certificate/:password, and use-certificate-chain-file) and a :verify-location CA path — point the javax.net.ssl.trustStore system properties at your trust store instead. Runs on the interpreter, the JVM and the WASM --component backend (tls-upgrade rides wasi:tls@0.3.0-draft there — add -S tls=y to the run flags; a non-nil insecure knob signals, since the draft exposes no verification opt-out); WASM Preview 1 keeps the compile error |
clack-handler-rontolisp | the Clack handler backend: package clack.handler.rontolisp exporting run/stop, bridging the Clack application protocol onto rontolisp's embedded HTTP server. You never load it by hand — (clack:clackup app :server :rontolisp) resolves it by name at run time (the system also answers to the dotted spelling clack.handler.rontolisp that clack derives from the package name). See the Clack guide |
clack-handler-reactor | the Clack handler backend for a host-driven reactor: a Cloudflare Worker, a browser page, a node or JVM embedding — any host that has already parsed the request and calls an exported function instead of handing the program a socket. Package clack.handler.reactor exports run/stop and, under them, handle (an application and a JSON request string in, a JSON response string out) and dispatch (the same over the application clackup stored). Resolved by (clack:clackup app :server :reactor), the dotted spelling included, exactly like the backend above. See the Clack guide |
The shims are deliberately thin: they satisfy what the loadable libraries actually call, not the full upstream APIs.
What can I actually load?
The real-world libraries below load unmodified today. The Backends column says
where each one is verified — "all four" means the interpreter, the JVM, WASM
Preview 1 and --component. Notes covers what is special about the load and
what does not work.
| Library | Backends | Notes |
|---|---|---|
| alexandria 1.0.1 | all four | The ecosystem's most-depended-on utility library, both packages (alexandria/alexandria-1 and alexandria-2), from its real sources. Every library below with dependencies pulls it in. Absent are the members standing on a primitive that is still missing: type= (subtypep's second value). format-symbol/ensure-symbol and ensure-function on a symbol work on the interpreter only (a compile-backend error, not a wrong answer). shuffle/random-elt/gaussian-random draw each backend's own entropy, so their output is not comparable across backends |
| split-sequence v2.0.1 | all four | The whole API on strings and lists, including the second return value (the resume index). Its CLOS-only extended-sequence.lisp is gated behind :if-feature (:or :sbcl :abcl) and drops out automatically |
| parse-number v1.8 | all four | The whole API over integers, ratios, floats, radix-prefixed literals (#xFF, #3r12) and exponent markers; the invalid-number condition signals with the intended diagnostics |
| cl-utilities v1.2.4 | all four | The whole public API — its own split-sequence, the extremum family, read-delimited, expt-mod, collecting/with-collectors, with-unique-names/with-gensyms/once-only (three-level nested backquote) usable from your own macros, rotate-byte, copy-array, compose |
| cl-who v1.1.5 | all four | (X)HTML generation macros — with-html-output(-to-string) with attributes, nested tags and the local str/esc/fmt/htm operators; :xml and :html5 both render correctly. :indent (pretty-printed output) is unsupported, and the output mode must be switched with (setf (html-mode) :html5): cl-who reads it at macro-expansion time, so a runtime let on *html-mode* is not observed |
| cl-mustache 0.12.3 | all four | Mustache template renderer, verbatim — render/render* over string AND file templates, compile-template (parse once, render many), define (bind a renderer to a name) and make-context with :data/:partials. A context is an alist, a hash table or a chain of them; sections, inverted sections, partials, dynamic partial names ({{>*name}}) and lambda sections all render. Its own copy of the 194-case mustache spec suite scores 158/194 on every backend — the identical set SBCL passes, so the 36 are upstream limits (null interpolation, dotted names pushing a context frame, and the whole 26-case inheritance module, which postdates the 1.1.2 spec cl-mustache targets), and t/test-api.lisp is 20/20. A missing partial signals partial-cant-be-found offering a use-value restart, so an artifact that handles it compiles in EH mode on both WASM backends |
| assoc-utils | all four | Alist utilities, whole API — aget (settable), the alist/plist/hash conversions, remove-from-alist/delete-from-alistf, with-keys, alist-get, alist=, alistp |
| cl-base64 v3.4 | all four | Base64 over strings, (unsigned-byte 8) arrays and integers, with :columns wrapping and the :uri alphabet; a bad input character signals bad-base64-character. That condition's :input/:position/:code slots are readable on the interpreter only — the compiled backends signal a plain condition, caught by the same handler-case |
| md5 v2.0.4 | all four | MD5 (RFC 1321) — md5sum-sequence/md5sum-string and the incremental API, matching the RFC test vectors identically on all four backends |
| chipz 0.8 | all four | Decompression — chipz:decompress for the gzip, zlib and deflate formats (to a fresh vector, into a supplied one, or incrementally through make-dstate), plus the CRC32/Adler-32 checksum entry points. The inflate state machine is a labels whose transitions store #'local-function in a struct slot, and it exits through catch/throw, so a compiled artifact is always in EH mode on both WASM backends. bzip2 loads with it — decompress's own typecase names bzip2-state — but is untested here. size-report/programs/zlib is built on it |
| cl-ppcre v2.1.2 | all four | Perl-compatible regular expressions from its real sources — scan, scan-to-strings, split, regex-replace(-all), all-matches, count-matches, the do-scans/do-matches macros, register-groups-bind, quote-meta-chars, parse-tree regexes and inline modifiers like (?i) |
| cl-unicode 0.1.6 (with cl-ppcre-unicode) | interpreter | The Unicode character database as a library — general-category, script, code-block, unicode-name, character-named, property-test, has-binary-property, the case and numeric mappings and the four normalization forms — plus the cl-ppcre-unicode resolver that makes \\p{L}, \\p{Nd} and the other property escapes work in a regex. Three of the eight components cl-unicode names do not exist in the release: real ASDF generates them by running a second system that parses the bundled UCD text and writes them next to the sources. rontolisp does that build step itself, deriving the same tables from the same files at load time, so the library needs nothing but (ql:quickload "cl-unicode"). Interpreter (and native binary) only. The tables are ~5 MB of data, which is more than one .class can name (the JVM backend stops at the constant-pool limit); WASM builds a 6.7 MB module but its equalp hash tables place keys case-sensitively, so cl-unicode's own property lookup misses at load |
| str 0.21 | interpreter | The string library — title-case/camel-case/snake-case/kebab-case and the rest of the case family (over cl-change-case), split/join/words/lines, trim/pad/shorten, starts-with-p/ends-with-p/containsp, replace-all/replace-using, remove-punctuation and the lettersp/alphanump/digitp predicates. The predicates are Unicode-aware because they go through cl-ppcre's \\p{L} escapes, which is why str pulls in cl-unicode and inherits its interpreter-only reach |
| com.inuoe.jzon v1.1.4 | all four | JSON parsing and stringification including the README walkthrough — hash-table / vector round-trips, :key-fn, the Gray-stream :stream writer, jzon:writer, CLOS-instance stringification; its dependencies resolve to the built-in shim systems above. Its three numeric leaf components (the eisel-lemire reader and Schubfach printer) are replaced by shims over rontolisp's own float arithmetic — float output is rontolisp's own shortest round-trip decimal, identical on every backend — and an extreme exponent can be a few ulps off when parsing (a decimal exponent of magnitude 22 or less rounds exactly). The usual WASM caveats apply: hash-table iteration order, non-ASCII \u escapes |
| ironclad v0.61 (the SHA-2 / HMAC / PBKDF2 / HKDF / SCRAM / RSA slice) | all four | From its real sources, reproducing the published FIPS 180-2, RFC 4231, RFC 5869 and RFC 7677 vectors — including SCRAM-SHA-256's client proof end to end, the sequence a PostgreSQL client authenticates with. The digests are SHA-224/256/384/512 and the MACs and KDFs over them (hmac, hmac-kdf, pbkdf2, pbkdf2-hash-password); the RSA stack is real too — generate-key-pair, sign-message/verify-signature and encrypt-message/decrypt-message, with and without PSS/OAEP. Only that slice loads (its own .asd is an executable program, so a bundled replacement declares the slice): the ciphers, the AEAD modes, the other public-key algorithms (DSA, ElGamal, the elliptic curves, ed25519) and the other digest families are absent, and requesting one signals at the call. prng.lisp is narrowed to the OS-entropy surface over rontolisp:random-bytes — nonces, RSA key generation and the PSS salt are cryptographically strong everywhere, but :fortuna and the seed-file operations are gone. PBKDF2 runs on a native kernel on the interpreter — the same bytes, roughly three orders of magnitude faster — so password hashing and SCRAM authentication are not interpreter-bound |
| uax-15 v0.1.3 | all four | Unicode normalization (UAX #15) in all four forms from its real sources; --system-path needs three directories (uax-15, split-sequence, cl-ppcre, joined with :). Upstream builds its tables by parsing 2.7 MB of bundled Unicode text at load time (minutes interpreted); rontolisp derives the same tables from the same files at compile/load time and builds each one on first read, leaving every normalization function verbatim — so loading is nearly free and a program that never normalizes pays nothing. One deliberate difference, and it is a fix: (unicode-letter-p #\A) answers T where the real load answers NIL (upstream's loop reads #+utf-32). get-mapping signals on every backend — it is broken upstream and nothing calls it |
| quri v0.7.0 | all four | URI library from its real sources via (ql:quickload "quri") — parsing into the scheme-specific structs, the accessors, render-uri, merge-uris, uri-query-params, percent-encoding, the public-suffix API and the address predicates. Its babel dependency resolves to the built-in UTF-8 shim, so a non-UTF-8 :encoding signals; the effective-TLD tables build on first read from the bundled 152 KB list, so (load-etld-data OTHER-FILE) reads that list rather than OTHER-FILE. :lenient percent-decoding skips a bad escape with a go out of a handler-bind handler, which the compile backends lower to a non-local exit, so it answers the same on all four. Needs alexandria, split-sequence, cl-utilities and idna on --system-path |
| local-time v1.0.6 | all four | Date/time library from its real sources via (ql:quickload "local-time") — encode-timestamp/decode-timestamp, now/today, the unix and universal-time conversions, parse-timestring, format-timestring over every bundled format (ISO 8601, RFC 3339, RFC 1123, asctime, ISO week date) and custom format lists, the comparison family, timestamp+/timestamp-/adjust-timestamp/timestamp-minimize-part, the julian-date pair and print-object. Its only dependency is the built-in uiop. Real TZif zone files load wherever the host has a filesystem — (local-time:define-timezone tokyo #p"/usr/share/zoneinfo/Asia/Tokyo" :load t) — and the load-time /etc/localtime read that seeds *default-timezone* works the same way, falling back to +utc-zone+ where the file cannot be read (which is what the WASM backends do without --dir). reread-timezone-repository walks the bundled zoneinfo/ tree on all four backends now that directory exists, so find-timezone-by-location-name resolves "Asia/Tokyo" and friends; on the compiled backends pass the repository explicitly ((local-time:reread-timezone-repository :timezone-repository "zoneinfo/")) because its default is computed at load time from asdf:component-pathname through a run-time eval, with *load-truename* as the fallback — neither of which the compiled backends can answer, so the default is nil there |
trivia (the trivia.trivial route) | all four | Optima-compatible pattern matching from its real sources via (ql:quickload "trivia") — match/match*/ematch (failure signals match-error), constant / variable / cons / list / list* / vector patterns, guard, or/and/not patterns, defpattern, struct patterns (keyword and conc-name shapes), class patterns (keyword slot and (class name (slot var)) shapes) and (type spec) patterns. System trivia is mapped to trivia.trivial — upstream's own base system for extensions — so clauses run under the :trivial optimizer: identical semantics, no balland2006 clause optimization (which would need iterate + type-i). Its dependencies (alexandria, lisp-namespace, the closer-mop / trivial-cltl2 shims) load with it. Note the interpreter re-expands macros per evaluation, so a hot match loop belongs on a compiled backend |
| sxql | all four | SQL generator from its verbatim sources via (ql:quickload "sxql") — sxql:yield returns the SQL string plus the bind-value list as multiple values, byte-identically on every backend (and identically to SBCL on the same sources): select with from/where (incl. :and/:or/:in/:like), order-by (:desc, nulls), limit/offset, left-join ... :on, insert-into with set=, update, delete-from, create-table with column options (the mito deftable shape), drop-table and alter-table. Its dependencies (trivia via the trivia.trivial route, alexandria, cl-package-locks — the last a no-op-shaped lock library) load with it. Like every macro-heavy library, hot query construction belongs on a compiled backend (the interpreter re-expands macros per evaluation). The O/R mapping guide walks through yield and the statement builders |
| esrap 0.19 | all four | Packrat / PEG parser from its verbatim sources via (ql:quickload "esrap") — esrap:parse over an inline expression or a named rule, defrule with :lambda / :destructure / :text transforms, add-rule / make-instance 'esrap:rule, case-insensitive (~ "lit") terminals, and / or / not / * / + / ? sequencing, semantic predicates ((oddp decimal)), :junk-allowed, and the accurate parse-error report (esrap:esrap-parse-error, whose text is byte-identical to SBCL's apart from SBCL's non-standard Unicode character NAMES). The parser is pure computation, so Preview 1 WASM is in — no sockets, no extra flags needed. Its dependencies (alexandria, trivial-with-current-source-form) load with it. esrap:trace-rule needs break, which does not exist here, and the swank indentation hook needs set; both are dead unless called |
| postmodern v1.33.12 (the MOP build) | interpreter, JVM, WASM component | PostgreSQL stack — s-sql included — from its verbatim upstream sources via (ql:quickload "postmodern"): with-connection/connect and the pool, query/execute over S-SQL forms or strings in every result style, doquery, prepared statements with the :reconnect/reset-prepared-statement restarts, transactions and savepoints, execute-file, deftable, and :postmodern-thread-safe ON so its locks really serialize. The DAO layer is in: the build takes :postmodern-use-mop ON, so table.lisp loads verbatim over the static metaobject subset — (defclass ... (:metaclass pomo:dao-class)) with :col-type/:keys/:table-name, dao-table-definition, deftable's !dao-def, insert-dao/get-dao/update-dao/upsert-dao/delete-dao/save-dao/select-dao/query-dao and make-dao. The metaclass protocol runs at DEFINITION time, so DAO classes must be top-level defclass forms with literal options (classes built from runtime data signal), and finalize-inheritance runs eagerly at class definition rather than at first use — definition errors surface earlier, results are unchanged. Connecting needs cl-postgres' socket layer, so Preview 1 WASM is out; a --component run additionally needs -S tcp=y -S inherit-network=y. The s-sql layer alone ((ql:quickload "s-sql")) opens no sockets and renders identical SQL on all four backends |
| clack v2.1.0 (with lack) | interpreter, JVM, WASM component | Web application environment from its verbatim upstream sources via (ql:quickload "clack"), served by the built-in clack-handler-rontolisp backend — see the Clack guide. The lack side loads too: lack:builder, lack-util's generate-random-id (over the ironclad slice) and the backtrace middleware, which clackup's default :use-default-middlewares t exercises end to end. clackup's default :use-thread t runs the acceptor on a real thread (rontolisp:make-thread) on the interpreter and the JVM; the WASM component serves under wasmtime serve instead (the host owns the socket). Preview 1 WASM has no incoming TCP by design, so clackup signals at call time there |
| tiny-routes v0.1.1 | all four | A routing layer for Clack applications, from its verbatim sources via (ql:quickload "tiny-routes") — the piece between clack:clackup and an application with routes. define-get/define-post/define-put/define-delete/define-any/define-route and define-routes, the :id-style path template (and a regex one with :regex t) over cl-ppcre, path-parameter, with-request/with-path-parameters, the pipe middleware combinator with wrap-request-body (the Clack :raw-body stream), wrap-query-parameters, wrap-request-predicate/-mapper, the response wrappers and the whole ok/created/not-found/… constructor set. Its companion system tiny-routes-middleware-cookie loads too (parse-cookie-header, write-set-cookie-header, wrap-request-cookies, wrap-response-cookies), pulling in cl-cookie, quri, local-time and proc-parse. Routing itself is pure computation, so all four backends are in; SERVING the routes needs clackup, which rules Preview 1 out — see the Clack guide. Its only dependency is cl-ppcre, so --system-path needs two directories when you load it from disk. The test system needs fiveam, which does not load. For a size-constrained compiled module there is a ppcre-free opt-in, tiny-routes/lite — the subsection right below |
| ningle v0.3.0 | all four | The "super micro framework" over Clack, from its verbatim sources via (ql:quickload "ningle") — the second routing layer here, and a genuinely different model from tiny-routes rather than another spelling. The application is a CLOS OBJECT ((make-instance 'ningle:app), a lack-component), every route is a setf ((setf (ningle:route app "/x") controller)), a controller receives the matched PARAMETERS rather than the environment, and a controller that is not a function is answered as the response body. Path templates with :name tokens and * splats, :regexp t routes, :method, :accept content negotiation and user-defined requirements ((setf (ningle:requirement app :key) fn)) — a route can therefore be selected by something that is not the path at all — plus the ningle:*request*/*response*/*session* specials, ningle:context and with-context-variables, ningle:next-route, and ningle:not-found, the overridable 404 method. Its router myway and myway's map-set load with it, as does the whole lack request chain it reads every request through (http-body, fast-http, smart-buffer, circular-streams, quri, yason, trivial-mimes) — which is why a compiled ningle module is an order of magnitude larger than the same routes through tiny-routes, and there is no size opt-in to offer: myway compiles every rule to a cl-ppcre scanner, so the regex engine is genuinely reachable. Routing itself is pure computation, so all four backends are in; SERVING the routes needs clackup, which rules Preview 1 out — see the Clack guide |
cl-dbi 0.11.1 (dbd-postgres only) | interpreter, JVM, WASM component | Database-independent interface from its verbatim sources via (ql:quickload "dbd-postgres"): dbi:connect (the driver resolves over the already-loaded system — a compiled program must contain the ql:quickload itself, since it cannot load a system at run time), dbi:do-sql, dbi:prepare/execute/fetch/fetch-all, dbi:with-transaction (commit and rollback), dbi:connect-cached and dbi:disconnect. The :mysql and :sqlite3 drivers need FFI and are absent. On the thread-capable backends the connection cache is per-thread (cache/thread.lisp over the bt2 shim's real locks and rontolisp:current-thread); the single-threaded WASM backends use upstream's own threadless cache. Its trivial-garbage dependency resolves to the no-op finalizer shim above, so call dbi:disconnect explicitly. Same socket constraints as postmodern: Preview 1 WASM is out, a component needs -S tcp=y -S inherit-network=y |
| mito 0.2.0 | interpreter, JVM, WASM component | O/R mapper from its verbatim sources via (ql:quickload "mito") — the full system (mito-core + mito-migration + lack-middleware-mito), covered by the O/R mapping guide. The DAO layer: connect-toplevel/disconnect-toplevel, deftable (the dao-table-class metaclass over the static metaobject protocol — auto-pk :serial and :uuid, record-timestamps-mixin's created-at/updated-at), table-definition, ensure-table-exists, create-dao/insert-dao/save-dao/delete-dao, find-dao, select-dao with sxql clauses, object-id, retrieve-by-sql and execute-sql. The migration layer: migration-expressions and migrate-table diff a class against the live schema on all three backends, while generate-migrations / migrate over migration FILES are interpreter + JVM (the WASM backends import no directory-creation or file-removal call, so they signal at the call); migrate re-reads the generated .sql with esrap, and the advisory lock rides a CRC32-only slice of chipz. PostgreSQL only (dbd-postgres, which must be quickloaded explicitly); like every metaclass consumer, deftable forms must be top level with literal options. Known gaps, all in the guide: the :conc-name accessors are not generated (slot-value works), and sxql's SQL FUNCTION operators — (:count ...) and therefore mito:count-dao — are interpreter-only. Two shapes fail identically on SBCL and are upstream defects, not gaps: a bare :references without a :col-type, and adding a NOT NULL column with an :initform. The uuid dependency loads (its v1/v4 generation draws the backend's own entropy) and dissect's stack introspection is the no-op interface. Same socket constraints as cl-dbi: Preview 1 WASM is out, a component needs -S tcp=y -S inherit-network=y |
| rove v0.10.0 | all four | Testing framework from its verbatim sources via (ql:quickload "rove"), covered by the testing guide — deftest/testing/ok/ng/signals/outputs/expands/pass/fail/skip/failing/setup/teardown/defhook/diag with the :spec (default) and :dot reporters, and every entry point: rove:run over a :package-inferred-system or a plain defsystem test system, run-test/run-tests, and run-suite at the end of a test file. A test body that signals becomes a recorded failure instead of ending the run (on the WASM backends a raw trap — (car 1), (/ 1 0) — still ends it), and rontolisp test TARGET (or (uiop:quit (if (rove:run ...) 0 1)) inside your own runner) turns the result into a CI exit code on every backend. Its dissect dependency loads from its real sources with the stack introspection empty, so failure reports carry no backtraces, and assertion descriptions print symbols package-qualified where SBCL prints them bare — details and the run commands are in the guide |
| jose | all four | JSON Object Signing and Encryption (JWS/JWT) from its verbatim sources via (ql:quickload "jose") — encode/decode/inspect-token over HS256/384/512, RS256/384/512, PS256/384/512 and the unsecured none, with the registered claim checks (iat/nbf/exp/jti, plus the :issuer/:audience/:subject keywords). The tokens are byte-identical to SBCL's on the same sources and to Python's hmac/hashlib — the HS256 one is the token jose's own README publishes. RSA keys are ironclad objects, so ironclad:generate-key-pair :rsa (or any parser that produces one) supplies them; PS512 needs a modulus of at least 1040 bits, which is ironclad's own assertion, not a limit here. An expired exp, an nbf in the future and a failed signature all signal through cerror, so a handler-bind that continues decodes anyway — and any program handling them compiles in EH mode on both WASM backends. Upstream's own jose/tests/jwt suite runs green under rontolisp test on all four backends; its jose/tests/jws sibling runs nowhere, on any implementation — it (:use #:pem), and pem is not in the Quicklisp distribution. Loading from vendored sources instead of the Quicklisp cache needs eight directories in --system-path: jose, cl-json, ironclad, cl-base64, split-sequence, assoc-utils, alexandria and trivial-utf-8 |
| cffi | interpreter, JVM | The Common Foreign Function Interface — the library every C binding in the ecosystem is written against — from its verbatim sources via (ql:quickload "cffi"), covered by the C libraries guide. defcfun, foreign-funcall (variadic included), define-foreign-library/use-foreign-library, with-foreign-object + mem-ref/mem-aref, defctype, defcstruct + foreign-slot-value, defcallback, defcvar and the whole foreign-string family all work; structures by value work with no cffi-libffi, because the foreign function API lays them out itself. What rontolisp supplies is the cffi-sys backend, a replacement cffi.asd and a substitute for src/strings.lisp; the other eleven files are upstream's, byte for byte. cffi-grovel and cffi-libffi are refused with the reason rather than half-loaded. Runs under java -jar, in the native binary (against a registered call-shape grid; an outlying shape signals the one metadata entry that would register it) and in a compiled .class/.jar, which embed the binding; neither WASM backend has a foreign function API at all, so those refuse to compile it, permanently |
The size opt-in: tiny-routes/lite
(ql:quickload "tiny-routes/lite") loads the same tiny-routes tree with one
component substituted — path-template.lisp, whose matcher upstream is a
cl-ppcre scanner — and the :cl-ppcre dependency dropped with it. It exists
because routing keeps the regex engine live: a route template compiles to
a scanner when the route is built, so in a compiled module no amount of
tree-shaking can remove cl-ppcre, and on a size-limited target that is most of
the module — the
routed Worker example
measures 974,530 B raw with the full system and 408,448 B with the lite
one, same routes, same answers, request for request.
The substitution never changes what a template matches — it matches identically to the full system, or it refuses loudly when the route is built:
- Accepted: templates made of literal characters and
:nametokens — a token is:followed by a letter or_, continuing over letters, digits,_and-, anywhere in the template:/users/:id,/files/v:version,/pair/:a/:b. Within this subset the lite matcher reproduces the full system's semantics exactly, greedy backtracking and upstream's greedy token-name scan included (in/a/:x-:ythe first token is namedx-); the two engines are pinned template-for-template by the test suite. - Refused at route-build time, with an error naming the escape: a
template containing any regex metacharacter —
.\[](){}|^$*+?— and every:regex ttemplate. (A template with no:nametoken is never a regex upstream either — it is compared withstring=— so metacharacters there are fine on both systems.)
Plain (ql:quickload "tiny-routes") is untouched — the verbatim library,
cl-ppcre included — and the two systems refuse to load into one program,
in either order (whichever loaded last would silently redefine the matcher).
tiny-routes/lite is not in the Quicklisp index; the name downloads the
tiny-routes release and resolves against its .asd.
cl-ppcre's load drove the widest feature batch so far — local
(declare (special ...)), CLOS slot accessors as generics,
initialize-instance :after, &environment + get-setf-expansion, psetf,
(setf (subseq ...)), subst/search/copy-tree and the
descending/case-insensitive character comparisons.
uax-15's load drove the second widest: compile-time folding of the ASDF/UIOP
pathname primitives, inlining a bundled data file read with with-open-file
into the artifact, a per-clause rewrite of the LOOP macro, and the UTF-8 byte
model behind WASM GC strings.
alexandria's is the batch every other library inherits, because everything above
that has dependencies depends on it: &whole in defmacro/destructuring-bind,
a destructuring pattern after &rest/&body (if-let), lambda-list-keywords,
do-external-symbols, intern with a package designator, the hash-table
introspection readers (hash-table-test/-size/-rehash-size/-rehash-threshold),
mismatch, arrayp, with-open-stream and #'open as a first-class value —
plus, for mappend, #'mapcar as a value over more than one list.
Runnable demos for sixteen of them — with the per-backend commands and
expected output — live in
examples/asdf/.
A library qualifies today roughly when it stays inside: plain
defun/defmacro/defpackage code, loop, multiple-value-bind over
values-tailed functions, check-type/etypecase with the supported type
specifiers, declarations (parsed no-ops, deftype included), the CLOS static
subset (defclass/defgeneric/defmethod/make-instance/slot-value with
single dispatch, plus (defun (setf name) ...) setf functions), the condition and
restart system (define-condition/handler-case/handler-bind/restart-case/
invoke-restart), return-from, and dynamic (special) variable binding (let/let* over a defvar
special). Libraries built on the full metaobject protocol or the interactive
debugger (break, *debugger-hook*) do not load yet (see
Unsupported CL Features). For anything else, the
practical use is structuring your own multi-file rontolisp projects —
with .asd files that real ASDF can read too.