Fe Lisp — Language Reference
The Fe VM is the cartridge authoring substrate for KN-86 (ADR-0001, ADR-0004). This page is the implementer-facing reference for syntax, types, special forms, and evaluation semantics. Cartridge authors and runtime engineers read this to know exactly what the Fe reader and evaluator accept.
../../../adr/ADR-0001-embedded-lisp-scripting-layer.md— substrate decision (Fe is the sole cartridge authoring language).../../../adr/ADR-0004-vm-selection.md— Fe selection rationale, memory and latency budgets.../../../adr/ADR-0012-lisp-slot-table-widening.md— handler slot table that Lisp lambdas register into.builtins.md— Fe-resident builtins.memory-model.md— arena rules.README.md— Fe-Lisp overview.
Origin and stability
Section titled “Origin and stability”Fe is rxi/fe (~800 LOC C, MIT). The vendored copy lives at kn86-emulator/vendor/fe/{fe.h, fe.c}. KN-86 takes Fe largely as-is: the language surface defined here is whatever Fe’s reader and evaluator implement, plus the runtime additions wired through nosh_lisp_bridge.c. There is no fork.
FE_VERSION is "1.0" (per fe.h). KN-86 pins to that release.
Syntax
Section titled “Syntax”S-expressions
Section titled “S-expressions”Programs are s-expressions. Atoms or parenthesized lists. Lists are heterogeneous; trailing dot syntax for improper pairs is supported by the reader ((a . b) constructs the cons cell a . b).
(fn (x) (* x x))(let result (cons 1 2))'(a b c)Comments
Section titled “Comments”Semicolon to end of line is the only comment form Fe recognizes:
; this is a comment(+ 1 2) ; trailing commentThere is no block-comment syntax.
Literals
Section titled “Literals”| Form | Reader produces | Notes |
|---|---|---|
123, -4.5, 3.14 | FE_TNUMBER | fe_Number is float (single-precision). Integers up to ~2^24 are exact. |
"text" | FE_TSTRING | Strings are immutable cons-chains of 7-byte buffers (STRBUFSIZE). |
foo, kebab-case-name, +, <= | FE_TSYMBOL | Symbols are interned in the per-context symlist. |
nil | FE_TNIL (the singleton &nil) | nil is not a symbol — it is the nil sentinel. See “Reader subtleties” below. |
'expr | (quote expr) | Reader sugar; identical to writing (quote expr). |
Booleans are not a separate type. nil is false; everything else (including the special truthy singleton ctx->t) is true. Predicates return ctx->t for true and nil for false; fe_bool materializes either.
Reader subtleties
Section titled “Reader subtleties”nilvs'nil. The reader resolves the literalnil(and'nil) to the nil sentinel object, not to a symbol named"nil". The runtime’s symbol-name extractor (fe_tostring) renders nil as the string"nil", so the bridge’s name-keyed dispatchers (e.g.lisp_event_name_to_slot, see ADR-0012) can still match. Cartridge authors must understand the type difference when debugging unexpected pair shapes.- Numbers. Fe’s number type is
float. Treat integer arithmetic as exact only inside the[-2^24, 2^24]mantissa window; outside, expect floating-point rounding. - Symbols are case-sensitive.
Fooandfooare distinct.
The complete type tag set is enumerated in fe.h:
| Tag | Lisp surface | Description |
|---|---|---|
FE_TPAIR | cons cell | Construct via (cons a b) or list literals. The fundamental aggregate. |
FE_TFREE | (internal) | Free-list slot in the arena; never user-visible. |
FE_TNIL | nil | The empty list / false. Singleton. |
FE_TNUMBER | 123, -4.5 | float. |
FE_TSYMBOL | foo, + | Interned identifier. |
FE_TSTRING | "text" | Immutable byte sequence. |
FE_TFUNC | (fn ...) result | User function (lambda). |
FE_TMACRO | (mac ...) result | User macro. |
FE_TPRIM | if, let, etc. | Built-in special form (compiled-in primitive). |
FE_TCFUNC | NoshAPI bindings | C function wrapped via fe_cfunc and fe_set. The FFI surface lives here. |
FE_TPTR | opaque handles | Boxed C pointer (e.g. cell handles passed from runtime). Marked with cartridge-supplied mark / gc callbacks. |
There are no separate boolean, integer, character, vector, hash-table, or record types. Everything aggregates through pairs and strings. KN-86 record-style structures (deck state, cells) are passed as FE_TPTR opaque handles; field access is via NoshAPI accessor primitives, not Fe-native struct ops.
Special forms (primitives)
Section titled “Special forms (primitives)”The following are recognized by the evaluator as special forms (P_* enum in fe.c). These are not functions — they don’t evaluate their arguments by the normal rules. Cartridge authors should think of these as the language; everything else (including builtins like car, cdr, cons, arithmetic) is also implemented as a primitive but follows normal evaluation.
| Form | Shape | Semantics |
|---|---|---|
(let sym value) | binding | Bind sym to evaluated value in the current scope. Returns value. |
(= sym value) | assignment | Same shape as let but used for re-assignment to an existing binding. |
(if cond then else?) | conditional | Evaluate cond. Truthy → evaluate then. Falsy → evaluate else (or return nil). Multiple then/else pairs supported in cond-style chaining. |
(fn (params...) body...) | lambda | Construct a closure. Captures lexical environment. Body is a do-style sequence. |
(mac (params...) body...) | macro | Construct a macro. Argument forms are passed unevaluated; the macro expansion is then evaluated. |
(while cond body...) | loop | Iterate while cond evaluates truthy. Returns nil. |
(quote x) | suppression | Return x unevaluated. Reader sugar: 'x. |
(and a b ...) | short-circuit | Evaluate left-to-right; return first falsy value, or last value if all truthy. |
(or a b ...) | short-circuit | Evaluate left-to-right; return first truthy value, or nil. |
(do expr...) | sequence | Evaluate forms in order; return last value. Implicit in function bodies. |
(cons a b) | construct | Build a pair. (Implemented as primitive for speed; semantically a function.) |
(car p) / (cdr p) | accessors | First / rest of a pair. (Primitives.) |
(setcar p v) / (setcdr p v) | mutation | In-place pair mutation. Use with care under arena discipline (see memory-model.md). |
(list a b ...) | construct | Build a list of evaluated values. |
(not x) | predicate | True if x is nil. |
(is a b) | identity | Reference equality for atoms (numbers compare by value; strings compare structurally). |
(atom x) | predicate | True if x is not a pair. |
(print x ...) | I/O | Write to the configured fe_WriteFn. Used by the REPL; cartridges typically use NoshAPI text primitives instead. |
(< a b) / (<= a b) | comparison | Numeric. |
(+ a b ...) / (- a b ...) / (* a b ...) / (/ a b ...) | arithmetic | Variadic. Fold left over fe_Number (float). |
The complete list of primitive names in source order: let, =, if, fn, mac, while, quote, and, or, do, cons, car, cdr, setcar, setcdr, list, not, is, atom, print, <, <=, +, -, *, / (see primnames[] in fe.c).
Notes vs “standard” Lisp
Section titled “Notes vs “standard” Lisp”fninstead oflambda;macinstead ofdefmacro;=for assignment,letfor binding (Fe’slettakes one binding pair, not Scheme’s bindings-list-plus-body shape).- No
define. Top-level definitions are(= name value)or(let name value). - No
defun, nodefn. Define a function with(= my-fn (fn (x) ...)). Cartridge authoring in this repo uses macros likedefcellanddefmission— those are runtime-defined, not Fe primitives. Seebuiltins.mdand the cartridge authoring docs. - No
cond, nocase. Use nestediforand/orchains; runtime can shipcondas a macro. - No tail-call optimization is documented in Fe. Recursive handlers should bound their depth; the GC-stack depth is fixed at
GCSTACKSIZE = 256. Long iteration useswhilewithsetcar/setcdrmutation, not deep recursion.
Lexical scoping
Section titled “Lexical scoping”Fe uses standard lexical scoping. fn captures the binding environment at construction time; references to names inside the body look up to the enclosing scopes at call time.
(= make-counter (fn (start) (fn () (= start (+ start 1)) start)))
(= counter (make-counter 10))(counter) ; => 11(counter) ; => 12The captured environment is a chain of bindings rooted at the global symbol table; closures live in the arena alongside the rest of the cart’s allocations.
Evaluation model
Section titled “Evaluation model”Application
Section titled “Application”Function application: (f a b c) evaluates f, then a, b, c (left-to-right), then invokes f with the evaluated arguments. f may resolve to a FE_TFUNC (user lambda), FE_TCFUNC (FFI binding), FE_TPRIM (special form — these don’t evaluate args by the normal rules), or FE_TMACRO (expanded then re-evaluated).
Macros
Section titled “Macros”(mac (params...) body...) constructs a macro. When applied, arguments are bound to params unevaluated; the body runs and produces a new form; that form is then evaluated in the calling environment.
(= unless (mac (cond body) (list 'if cond nil body)))(unless (is x 0) (do-something))Errors
Section titled “Errors”fe_error(ctx, msg) aborts the current evaluation. The Fe handler chain (fe_handlers(ctx)->error) is invoked first; if it returns, Fe prints the message and a call-stack traceback to stderr and calls exit(EXIT_FAILURE). KN-86 installs a custom error handler that recovers into the REPL prompt rather than exiting.
Out-of-arena raises "out of memory" via the same path. See memory-model.md for arena-exhaustion handling.
GC stack
Section titled “GC stack”Fe maintains a small GC root stack (GCSTACKSIZE = 256). C callers wrap allocation-heavy sections with fe_savegc / fe_restoregc to avoid stack overflow. From within Fe, the GC stack is invisible — overflow shows up as the message "gc stack overflow".
Note: Fe upstream is GC-based, not arena-allocated. The KN-86 integration uses Fe’s mark-sweep over a fixed-size arena allocated at fe_open(ptr, size); per ADR-0004, the resulting envelope behaves arena-like at cart/mission boundaries because we reset the context (fe_close + fe_open) at those points rather than relying on incremental collection. See memory-model.md.
What Fe doesn’t have
Section titled “What Fe doesn’t have”For comparison against Common Lisp / Scheme:
- No vectors, hash tables, or records (use pairs and FFI handles).
- No keyword arguments (use plists by convention; the runtime spell-checks via
fe_tostring). - No
formator printf-style formatting (NoshAPI providestext-printf). - No
evalreflection from inside Fe (fe_evalis a C-only entry point; ADR-0007 forbids exposing it to scripted missions). - No standard library beyond what’s in
fe.c. Anything else is an FFI binding installed by the runtime (seebuiltins.md,../nosh-api/).