(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)make-package, rename-package, delete-package, packagep, package-nicknames, find-all-symbols, do-all-symbols and apropos/apropos-list are available (two tiers below); shadow / shadowing-import / unintern change a package's member table, which on the compiled backends only the packages the program creates have
eval-whentreated as progn (no phase distinction)
#:namereads as a plain symbol, without gensym-style freshness
*modules*not available (require/provide are)
complex numbersavailable on every backend but --no-gc (Numeric tower)
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 (decode-universal-time, the truncate family's remainder on a ratio, ...) remain single-value — find-symbol and intern do answer the accessibility status, macroexpand-1 / macroexpand do answer expanded-p, subtypep does answer valid-p, and read-from-string does answer the stop index, and read-line does answer missing-newline-p.

Non-local exit

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

  • 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 return-from crossing a lambda or an flet/labels function behaves as in Common Lisp. 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 #., #n=/#n# and the one-argument read-from-string's #+/#- 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 initialize-instance / reinitialize-instance / shared-initialize are callable, and function values, with no user method — the system defaults fill the supplied initargs, as in CL, and a non-instance argument signals no-applicable-method. 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 read/compile-time directive supporting :use, :export, :nicknames, :import-from, :shadowing-import-from, :shadow and :intern (:documentation/:size are accepted and ignored); a defpackage that is not top-level registers its package when the form around it runs, on the interpreter only. use-package, unuse-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. Packages created at run time form the second tier: make-package creates an empty package (upcased name, :use entries that must already exist), rename-package renames one (replacing its nicknames), delete-package drops one, and the failures signal a catchable package-error carrying the offending designator for package-error-package. Read/compile-time packages (the built-ins, and the defpackage products of a compiled program) are immutable at run time -- renaming or deleting one signals -- because the compiled backends resolved against them; on the interpreter a defpackage product joins the runtime tier, so it may be renamed and deleted like any other. The queries cover both tiers: packagep, package-nicknames, find-all-symbols, do-all-symbols and apropos / apropos-list, with the compiled backends answering from a table baked in at compile time plus the packages the program itself creates. shadow, shadowing-import and unintern change a package's member table; on the compiled backends a read/compile-time package is frozen, so they answer t (unintern nil) and change nothing there. The queries are real: find-package, package-name, list-all-packages, package-use-list, package-used-by-list and package-shadowing-symbols, with the compiled backends answering from a table baked in at compile time plus the packages the program itself creates. 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*, progv and parameters named like a special is supported on every backend, and every exit restores the previous binding: normal return, an error caught by a handler outside the binding, catch/throw, return/return-from (including out of a lambda, flet or labels function), go, and a handler-bind handler that leaves by a go or return-from. The --no-gc backend rejects a top-level defvar.

Numeric tower

rontolisp supports integers (including arbitrary-precision bignums), ratios (1/3), double floats and complex numbers (#C(1 2), with rational or float parts). A negative square root answers the complex root:

The --no-gc backend has no complex representation: a complex literal or constructor is a compile error there.

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.