(rontolisp) docs

Compiled eval Limitations

eval works in all three backends. In the interpreter it is the full tree-walking evaluator. The WASM and JVM compilers each emit a small tree-walking interpreter into their output (_eval/_apply/_store plus the helpers _envLookup/_lookup) that runs the form at runtime, so no separate evaluator or parser is needed.

The compiled eval (WASM and JVM) implements a lexical environment plus a persistent global environment, and aims for parity with the interpreter: self-evaluating atoms, variable references, closures, the special forms and higher-order functions (let, lambda, cond, while, dotimes, setq, setf, push, pop, funcall, mapcar, mapc, reduce, nested eval, ...), and application of any function or interpreted closure all behave as in the interpreter. Rather than enumerate everything, the differences are listed below.

Compiled eval limitations

The compiled eval (WASM and JVM) differs from the interpreter only in these cases:

  • let binding lists must use the ((name value) ...) form (a bare (let (x) ...) is not supported).
  • Comparison operators are binary inside eval. Compiled top-level code supports variadic =, <, >, <=, >= and variadic min/max/gcd/lcm (desugared into nested binary operations at compile time), but that desugaring does not reach forms interpreted at runtime by eval, where these operators take two arguments and extra arguments are ignored (so (eval '(= 1 1 2)) evaluates (= 1 1) and returns true). + - * / list are fully variadic everywhere. User functions with more than 7 parameters return nil.
  • Edge cases that fail. A zero-argument (+)/(-)/(*)/(/) fails at runtime (a trap in WASM, an exception in JVM). Unary (- x) and (/ x) negate/invert like the interpreter.
  • A lambda built at runtime does not parse lambda-list keywords. Compiled defun/lambda forms support &optional/&rest/&key (desugared at compile time), and calling such a compiled variadic function from eval works. But a lambda that only exists inside an eval'd form, e.g. (eval '(funcall (lambda (&rest r) r) 1 2)), binds its parameters positionally — &rest is treated as an ordinary parameter name.
  • No big-integer promotion. Arithmetic inside the runtime eval interpreter uses fixed-width integers and wraps on overflow, even on the JVM where compiled code itself promotes to big integers.
  • An unbound variable evaluates to the symbol itself. The interpreter signals The variable x is unbound; the runtime eval has no error channel and returns the symbol instead. An undefined function in call position returns nil.
  • Top-level global variables are shared write-through from compiled code. A top-level setq/defvar/defparameter/defconstant mirrors its value into the runtime eval global environment, so an eval'd expression can read a global the compiled program defined (e.g. (setq add10 (make-adder 10)) then (eval '(funcall add10 100)) returns 110). The mirror is one-way: if eval later reassigns such a variable, compiled code keeps reading its own copy.
  • let*, do, do*, dolist, return, defvar, defparameter, defconstant, incf, decf, format, error, ecase, etypecase, ccase, concatenate, with-open-file and the file-stream functions (open, close, write-line) are not supported. These forms are expanded or handled at compile time only; the runtime eval interpreter does not recognize them. The sequence functions (length, reverse, member, member-if, find, find-if, position, count, assoc, assoc-if, getf, last, butlast, remove, remove-if, remove-if-not, remove-duplicates, delete, delete-if, delete-if-not, substitute, nsubstitute, nconc, copy-list, nreverse, make-list, union, intersection, set-difference, adjoin, identity, mapcan, sort, every, some) and princ-to-string/prin1-to-string work, since they resolve through the compiled function registry. The :test/:key keywords of the sequence and alist functions are a compile-time expansion, however: inside a runtime eval form they are silently ignored and the comparison stays eql (only the interpreter backend applies them inside eval).
  • defmacro and backquote are compile-time only. On the compilation path, user macros are fully expanded (and their definitions consumed) before the compilers run, and backquote templates are expanded by the reader; the runtime eval/read of a compiled program recognizes neither defmacro nor the backquote character. The same holds for macroexpand/macroexpand-1: a call with a literal quoted argument is folded to its expansion at compile time, and the runtime eval does not know these functions (gensym, by contrast, works — it has a first-class wrapper).
  • defstruct is compile-time only. A top-level defstruct is expanded into its generated functions before compilation, so calling a constructor/accessor/predicate from eval works, but an eval'd form can neither define a new structure nor use an accessor as a setf place.
  • The CLOS subset is compile-time only. Like defstruct, top-level defclass/defgeneric/defmethod forms are expanded before compilation — calling a generic function, a reader/accessor, or a constructor from eval works, but an eval'd form cannot define classes or methods, and make-instance/slot-value are not recognized inside eval (they resolve through the compile-time class registry).
  • The rontolisp package functions are not supported. rontolisp:version, rontolisp:list-functions, rontolisp:list-macros, rontolisp:list-special-forms, rontolisp:fetch, rontolisp:http-handler, rontolisp:await, rontolisp:then, rontolisp:promisep, rontolisp:json-parse and rontolisp:json-stringify are compiled directly (constants, inline calls or spliced-in library functions); the runtime eval/load does not recognize them.

These differences come from the design: the runtime eval resolves operators by name against a compile-time registry of the functions that were actually compiled into the output, and built-in functions are shared with the compiled code.