User-defined operatives via $vau; applicatives via $lambda (wrap ∘ $vau). wrap/unwrap as Kernel-level applicatives. kernel-call-operative forks on :impl (primitive) vs :body (user) tag. kernel-base-env wires the four combiners + operative?/applicative? predicates. Env-param sentinel `_` / `#ignore` → :knl-ignore (skip dyn-env bind). Flat parameter list only; destructuring later. Headline test: custom applicative + custom operative composed from user code.
12 KiB
Kernel-on-SX: first-class everything
The natural successor to SX's recently-completed env-as-value work (sx-improvements Phase 4). Kernel — John Shutt's reformulation of Lisp from his 2010 PhD — pushes first-class all the way: environments, evaluators, special forms (operatives), lambda variants are all runtime values, manipulable by programs. SX already has env-as-value; Kernel is what env-as-value looks like all the way.
The chisel: reflection. Every language in the current set treats some part of itself as fixed and ineffable — Common Lisp's special forms, Erlang's process model, OCaml's modules. Kernel reifies more of itself than any other language does. Implementing it stresses the substrate's self-knowledge: which parts of evaluation does SX expose to user programs, and which stay opaque?
What this exposes about the substrate:
- Whether
eval-exprcan be called as a primitive on user-supplied environments without breaking invariants. - Whether CEK frames can be reified as values (they currently aren't).
- Whether special-form dispatch can be table-driven and user-extensible at runtime.
- Whether the macro hygiene story extends to Shutt's "hygienic operatives" (operatives that don't capture).
End-state goal: Kernel's R-1RK core — $vau/$lambda/wrap/unwrap, first-class environments, the applicative–operative distinction, the standard environment, encapsulations.
Ground rules
- Scope:
lib/kernel/**andplans/kernel-on-sx.mdonly. Substrate work belongs tosx-improvements.md— if a feature is missing, file it there, don't fix from this plan. - Consumes from
lib/guest/:core/lex.sx,core/pratt.sx(s-expression-shaped, minimal demand),core/ast.sx,core/match.sx. - May propose a new sub-layer
lib/guest/reflective/— environment reification helpers, applicative-vs-operative dispatch, evaluator continuation protocols. A second consumer would be needed; candidates are a hypothetical "MetaScheme" or a Common-Lisp port that exposes its evaluator. - Branch:
loops/kernel. Standard worktree pattern.
Architecture sketch
Kernel source text (S-expression syntax)
│
▼
lib/kernel/parser.sx — bog-standard s-expr reader
│
▼
lib/kernel/eval.sx — kernel-eval: walks the AST, threads first-class env
│ dispatches to operatives via env-bound bindings, not
│ a hardcoded switch
▼
lib/kernel/runtime.sx — applicative/operative tagged values, wrap/unwrap,
│ standard environment construction, encapsulations
▼
SX CEK evaluator
Semantic mappings
| Kernel construct | SX mapping |
|---|---|
($lambda (x) body) |
applicative: (make-applicative (fn (x) body)) — args evaluated |
($vau (x) e body) |
operative: (make-operative (fn (x e) body)) — args UN-evaluated, dynamic env passed as e |
(wrap op) |
applicative wrapping an operative: evaluate args, then call op |
(unwrap app) |
get the underlying operative of an applicative |
($define! x v) |
operative: bind x to v in dynamic env |
(eval expr env) |
call kernel-eval on expr in env — first-class |
(make-environment) |
fresh empty env |
(get-current-environment) |
reify the calling env (via SX env-as-value) |
($if c t e) |
operative: evaluate c, then t or e in dynamic env |
The whole interesting thing: there are no special forms hardcoded in the evaluator. $if, $define!, $lambda are all operatives bound in the standard environment. User code can rebind them. The evaluator is just lookup-and-call.
Roadmap
Phase 1 — Parser
- S-expression reader with the standard atoms (number, string, symbol, boolean, nil) and lists.
- Reader macros optional; defer to Phase 6.
- Tests in
lib/kernel/tests/parse.sx.
Phase 2 — Core evaluator with first-class environments
kernel-eval expr env— primary entry, walks AST, threads env as a value.- Symbol lookup → environment value (using SX env-as-value primitives).
- List → look up head, dispatch on tag (applicative vs operative).
- No hardcoded special forms — even
if/define/lambdaare env-bound. - Tests in
lib/kernel/tests/eval.sx.
Phase 3 — $vau / $lambda / wrap / unwrap
- Operative tagged value:
{:type :operative :params :env-param :body :static-env}. - Applicative tagged value wraps an operative + the "evaluate args first" contract.
$vaubuilds operatives;$lambdaiswrap∘$vau.wrap/unwrapround-trip cleanly.- Tests: define a custom operative, define a custom applicative on top of it.
Phase 4 — Standard environment
- Standard env construction: bind
$if,$define!,$lambda,$vau,wrap,unwrap,eval,make-environment,get-current-environment, plus arithmetic and list primitives. - Tests: classic Kernel programs (factorial, list operations, environment manipulation).
Phase 5 — Encapsulations
make-encapsulation-typereturns three operatives: encapsulator, predicate, decapsulator. Standard Kernel idiom for opaque types.- Tests: implement promises, streams, or simple modules via encapsulations.
Phase 6 — Hygienic operatives (Shutt's later work)
- Operatives that don't capture caller bindings — uses scope sets / frame stamps to track provenance.
- Bridge to SX's hygienic macro story; possibly extends
lib/guest/reflective/with hygiene primitives. - Tests: write an operative that introduces a binding and verify it doesn't shadow caller's same-named bindings.
Phase 7 — Propose lib/guest/reflective/
- Once Phase 3 lands and stabilises, identify which env-reification + dispatch primitives are reusable. Candidate API:
make-operative,make-applicative,with-current-env,eval-in-env. - Find a second consumer (Common-Lisp's macro-expansion evaluator? a metacircular Scheme variant? a future plan).
- Only extract once two consumers exist (per stratification rule).
lib/guest feedback loop
Consumes: core/lex, core/pratt, core/ast, core/match.
Stresses substrate: env-as-value (Phase 4 of sx-improvements) under heavy use; eval as a primitive on user environments; potentially CEK frame reification.
May propose: lib/guest/reflective/ sub-layer — environment manipulation, evaluator-as-value, applicative/operative dispatch protocols.
Proposed lib/guest/reflective/combiner.sx API (from Phase 3 chiselling — pending second consumer):
(refl-make-primitive-operative IMPL)— IMPL receives(args dyn-env), args unevaluated.(refl-make-user-operative PARAMS EPARAM BODY STATIC-ENV)— for $vau-like constructors. The EPARAM sentinel for "ignore dyn-env" is a fixed keyword (:refl-ignorein the proposal).(refl-wrap OP)/(refl-unwrap APP)— round-trip pair.(refl-operative? V)/(refl-applicative? V)/(refl-combiner? V).(refl-call-combiner COMBINER ARGS DYN-ENV)— the dispatch fork. Pairs withrefl-evalfrom the evaluator kit.- Representation:
{:refl-tag :operative :impl FN}or{:refl-tag :operative :params P :env-param EP :body B :static-env SE}; applicatives are{:refl-tag :applicative :underlying OP}. The dispatch decision lives in one fork: presence of:implis primitive, presence of:bodyis user-defined. - Driving insight: every reflective Lisp must distinguish "eval my args first" from "hand me the syntax". The tag protocol is identical across Kernel, CL fexprs, vau-style Schemes, possibly Forth's IMMEDIATE words.
Proposed lib/guest/reflective/env.sx API (from Phase 2 chiselling — pending second consumer per the two-consumer rule):
(refl-make-env)/(refl-extend-env PARENT)— fresh / chained envs, plain SX dicts so they're easy to introspect.(refl-env? V)— predicate.(refl-env-bind! ENV NAME VAL)— local bind; parent is untouched.(refl-env-has? ENV NAME)— recursive presence check.(refl-env-lookup ENV NAME)— recursive lookup, raises on miss.- Representation:
{:refl-tag :env :bindings DICT :parent ENV-OR-NIL}. Pure-SX dicts so any guest can serialize, diff, snapshot, or rewind environments without help from the host.
The motivation is that SX's host make-env family is registered only in HTTP/site-mode platform setup, so a guest that needs first-class envs in CLI / test contexts has to roll its own anyway. A shared kit means the next reflective consumer (CL macro evaluator? metacircular Scheme?) doesn't need to redo the work.
What it teaches: whether SX's recent env-as-value direction generalises to "evaluator-as-value." If Kernel implements cleanly in <2000 lines, env-as-value is real. If it requires substrate fixes at every turn, env-as-value was incomplete and the substrate is telling us what's missing.
References
- Shutt, "Fexprs as the basis of Lisp function application" (PhD thesis, 2010).
- Kernel Report (R-1RK): https://web.cs.wpi.edu/~jshutt/kernel.html
- Klisp implementation (Andres Navarro) — pragmatic reference.
Progress log
- 2026-05-11 — Phase 3 operatives landed.
lib/kernel/runtime.sxadds$vau(primitive operative that returns a user operative),$lambda(sugar forwrap ∘ $vau),wrapandunwrap(Kernel-level applicatives), plusoperative?andapplicative?predicates.kernel-base-envwires them all into a fresh env.kernel-eval.sxnow dispatches inkernel-call-operativebetween primitive ops (carry:impl) and user ops (carry:params :env-param :body :static-env). Parameter binding is a flat list — destructuring/&restdeferred. Env-param sentinel: spell_or#ignore→:knl-ignore, which skips the dyn-env bind. 34 tests intests/vau.sx, including the headline custom-operative + custom-applicative composition. chisel: shapes-reflective. Two further reflective-API candidates surfaced: (a) the operative/applicative tag protocol —make-primitive-operative,make-user-operative,wrap,unwrapare general for any Lisp-of-fexprs; (b) the call-dispatch fork (primitive vs user) is a single decision that every reflective evaluator hits. Both shape go into the proposedlib/guest/reflective/combiner.sxcandidate. - 2026-05-10 — Phase 2 evaluator landed.
lib/kernel/eval.sxislookup-and-combine: zero hardcoded special forms.kernel-eval EXPR ENVdispatches on shape — literals self-evaluate, Kernel strings unwrap, symbols lookup, lists evaluate head and combine.kernel-combinedistinguishes operatives (impl receives un-evaluated args + dynamic env) from applicatives (eval args, recurse into underlying op).kernel-wrap/kernel-unwrapround-trip cleanly. 36 tests verify literal evaluation, symbol lookup with parent-chain shadowing, tagged-value predicates, and the operative-vs-applicative contract (notably$ifonly evaluates the chosen branch,$quotereturns its arg unevaluated). chisel: shapes-reflective. Substrate gap surfaced: SX'smake-env/env-bind!family is only registered in HTTP/site mode (http_setup_platform_constructors), not in CLI epoch mode used for tests. So Kernel envs are modelled in pure SX as{:knl-tag :env :bindings DICT :parent P}— a binding-dict + parent-pointer + recursive lookup walk. This is exactly thelib/guest/reflective/env.sxcandidate API: any reflective language needs first-class env values that can be extended, queried, and walked. Recording the shape (constructor, extend, bind!, has?, lookup) here for the eventual Phase 7 extraction. - 2026-05-10 — Phase 1 parser landed.
lib/kernel/parser.sxreads R-1RK lexical syntax: numbers (int/float/exp), strings (with escapes), symbols (permissive — anything non-delimiting), booleans#t/#f, the empty list(), nested lists, and;line comments. Reader macros (',,@) deferred per plan. AST: numbers/booleans/lists pass through; strings are wrapped as{:knl-string …}to distinguish from symbols which are bare SX strings. 54 tests inlib/kernel/tests/parse.sxpass viasx_server.exeepoch protocol. chisel: consumes-lex (useslex-digit?andlex-whitespace?fromlib/guest/lex.sx— pratt deliberately not consumed because Kernel is plain s-expressions, no precedence climbing).
Blockers
(none yet — main risk is substrate gap discovery during Phase 2)