(rontolisp) docs

Unsupported Common Lisp Features

rontolisp is a deliberately small subset of Common Lisp that runs identically on three backends (interpreter, JVM, WASM). To keep the language compilable to plain bytecode without a runtime metaobject protocol, many features of full Common Lisp are intentionally left out.

This page lists only what is missing or partial. For what is available, see the Language Reference.

FeatureStatus
restartsavailable; no debugger integration (break, *debugger-hook*) and no condition-restart association
&environmentaccepted in a defmacro lambda list but always bound to nil (there is no macro-expansion environment object). &whole works, in defmacro and destructuring-bind alike
loop (extended)partial (see below)
CLOSpartial (static subset + a definition-time MOP subset)
defstruct :includesingle inheritance only; slot-overrides (:include parent (slot default) ...) work
declare / declaim / proclaim / thenever change a result; on WASM an array type declaration directs the element-accessor emission (smaller, faster modules), everywhere else parsed no-ops
typep / subtypep / coerce / concatenateliteral (quoted) type specifiers only; coerce targets 'list / 'vector / 'string (or a float type), concatenate builds those same three sequence families
make-package / rename-package / delete-package / unintern / shadow (runtime)not available; export / unexport / import / use-package ARE, as read/compile-time directives like in-package; defpackage :shadow / :shadowing-import-from are errors
eval-whentreated as progn (no phase distinction)
#:namereads as a plain symbol, without gensym-style freshness
*modules*not available (require/provide are)
complex numbersnot available
catch / throw / unwind-protect / conditions under --no-gccompile error (available on every other backend)

Multiple values

values and its consumers are available, including the values of user functions. The remaining deviations from Common Lisp:

  • a producer that calls values in a non-tail position and then returns normally may leave stale extra values behind, so keep values in result position (a consumer clears the values it received, so only a values call that nothing consumes can leave leftovers);
  • funcall #'values (the first-class value) yields the primary value only in compiled programs;
  • multiple-value-call with a built-in #'name keeps the wrapper's fixed arity — pass a user function or lambda for other argument counts;
  • other built-ins with secondary values in CL (read-from-string, subtypep, ...) remain single-value — find-symbol and intern do answer the accessibility status, and macroexpand-1 / macroexpand do answer expanded-p.

Non-local exit

catch / throw, block / return-from and tagbody / go are available, with two gaps on the compiled backends (the interpreter is unaffected):

  • a return-from that would cross an flet/labels local function is not yet supported (one crossing a lambda is, as a non-local exit);
  • go must target a tag of a tagbody that lexically encloses it; the interpreter additionally supports dynamic go across function-call boundaries, i.e. a tag established by the caller. A tag reached from inside a nested lambda -- the shape a handler-bind handler that resumes the protected loop with a go produces, and what quri's :lenient percent-decoding does -- is lowered like a cross-lambda return-from: a non-local exit that re-enters the tagbody at the tag and carries on.

A cross-lambda return-from or go, catch/throw, unwind-protect, and condition catching all compile in exception-handling mode; under --no-gc catch/throw, unwind-protect and the condition forms are a compile error.

Restarts

The condition system is complete through the restart layer: handler-bind handlers run at the signal point before unwinding, restart-case / restart-bind / with-simple-restart establish restarts, and find-restart / invoke-restart / compute-restarts / muffle-warning / abort / continue drive them; cerror is continuable. What is missing is the interactive debugger: break and *debugger-hook* do not exist, a restart's :report is stored but never rendered and its :interactive function never runs, and restarts are not associated with conditions (the optional condition argument of find-restart/compute-restarts is ignored). check-type / assert / ccase still signal without offering a store-value restart. Under --no-gc the restart forms degrade to the primary form (that backend has no condition objects at all); on the wasm-GC backends only signaled conditions are catchable — a runtime trap still aborts.

The loop macro

A bounded subset of the extended loop is available -- that page lists the supported clauses, which include destructuring patterns, parallel and, the anaphoric it, loop-finish and thereis/always/never. What is out of scope: named (and the return-from it would name); a destructuring pattern does not recognize lambda-list keywords (&optional and friends bind as ordinary variables instead of signalling); being drives hash tables, but its package form (being the external-symbols of ...) parses and iterates the EMPTY sequence, because there is no runtime intern table.

Structures and objects

defstruct supports :include inheritance in its single-inheritance form only. Slot-overrides work: (:include parent (slot new-default) ...) re-defaults an inherited slot in the child's layout while it keeps its inherited index, so the parent's accessors still read it. An instance prints in the standard #S(...) syntax, and a #S(...) literal reads back into an instance -- in source and through the runtime read / read-from-string on every backend (a compiled program's reader has frontend parity; only #., #+/#- and #n=/#n# signal there). A structure that carries a (:print-object fn) / (:print-function fn) option prints through that function instead; both options are supported.

CLOS is a static subset (defclass, defgeneric / defmethod dispatching on the first argument, make-instance and slot-value with literal quoted names). A slot written with no :initform starts UNBOUND, as in CL: slot-boundp reports it, slot-makunbound restores it, and a read signals unbound-slot. change-class changes an instance's class in place (the target may be a runtime symbol or a class metaobject), and reinitialize-instance / shared-initialize are callable with no user method — the system defaults fill the supplied initargs, as in CL. A definition-time MOP subset is in: find-class and class-of answer real standard-class metaobjects, allocate-instance works, and a (:metaclass M) class option runs the class-definition protocol at definition time (see defclass) — this is what loads postmodern's DAO layer verbatim. Multiple inheritance works (class precedence list, slot merge across superclasses). Out of scope: runtime class construction (ensure-class from computed data, a non-top-level defclass, add-method, compute-applicable-methods, class redefinition, update-instance-for-different-class) — the class and method sets of a compiled program are fixed at compile time.

User-defined packages

defpackage is a literal, top-level, read/compile-time directive supporting :use, :export, :nicknames and :import-from (:documentation/:size are accepted and ignored). :shadow and :shadowing-import-from are errors (there is no symbol shadowing). use-package, export, unexport and import exist as the same kind of read/compile-time directive in-package is: a literal top-level call takes effect for the forms that follow it, on every backend, and a runtime-computed call works on the interpreter only. Creating or renaming a package at run time does not: make-package, rename-package and delete-package are not available. unintern (and the runtime shadow / shadowing-import) cannot exist here at all — a symbol IS its name, so there is no intern table to remove it from. The queries are real: find-package, package-name, list-all-packages, package-use-list, package-used-by-list and package-shadowing-symbols (always nil), with the compiled backends answering from a table baked in at compile time — so a package a compiled program creates later is invisible there. When several used packages export the same name, the first package in :use order wins instead of signaling a conflict.

Dynamic (special) variables

Dynamic binding through let/let* and progv is supported, with one limitation on the compiled backends (the interpreter is unaffected): while normal exit and a return/return-from that unwinds across a special let boundary both restore the binding, an error caught by a handler outside the let (a go across it, and on the WASM backends a return that also crosses an unwind-protect/handler-case) does not. progv restores on every exit an unwind-protect covers, including those cases.

Numeric tower

rontolisp supports integers (including arbitrary-precision bignums), ratios (1/3), and double floats, but not complex numbers. A negative square root yields a float NaN rather than a complex result:

CL-USER> (sqrt -1)
NaN      ; full Common Lisp would return #C(0.0 1.0)

Other omissions

  • lambda lists: an extended defmacro lambda list (&whole, &optional, &key, &aux, nested destructuring patterns) routes through destructuring-bind, which is deliberately lenient -- a missing argument is nil and a surplus one is ignored rather than signalling; and a function is limited to 10 physical parameters on the funcall/apply path.
  • user macros are unknown to the runtime eval of compiled programs, and a lambda built at runtime by that eval does not parse lambda-list keywords (see Compiled eval Limitations).
  • the PRETTY PRINTER produces the text a wide enough line holds, but never changes the LAYOUT: no rontolisp stream carries a column, so a logical block never wraps, every conditional line break (pprint-newline with :linear / :fill / :miser, the format directives ~_ / ~:_ / ~@_ / ~i) is a no-op, and *print-right-margin* / *print-miser-width* / *print-lines* are accepted and ignored. Only (pprint-newline :mandatory) and ~:@_ break a line. Every other *print-* variable exists and holds the value the printer really behaves as -- binding one to a non-default value is what has no effect, except for *print-escape* / *print-readably* / *print-pretty* and *print-case*, which are honored. *print-case* converts the case of the symbols the printer spells but leaves a symbol nested in a structure, a CLOS instance, a hash table or an array of rank other than one at its stored spelling (Reader Case). The ordinary printing operators do not consult *print-pprint-dispatch*: an entry fires where the program calls the entry function itself.
  • #. read-time eval is skipped with a warning inside .asd files.
  • built-in macro names (cond, case, when, setf, push, ...) cannot be redefined.

This list is not exhaustive; rontolisp implements a focused core rather than the full standard.