diff --git a/lib/kernel/eval.sx b/lib/kernel/eval.sx index b694b5ae..6b2dcde9 100644 --- a/lib/kernel/eval.sx +++ b/lib/kernel/eval.sx @@ -1,8 +1,8 @@ -;; lib/kernel/eval.sx — Kernel evaluator (Phase 2 skeleton). +;; lib/kernel/eval.sx — Kernel evaluator. ;; ;; The evaluator is `lookup-and-combine`: there are no hardcoded special -;; forms. Even $if / $define! / $lambda will be ordinary operatives bound -;; in the standard environment (Phase 4). This file builds the dispatch +;; forms. Even $if / $define! / $lambda are ordinary operatives bound in +;; the standard environment (Phase 4). This file builds the dispatch ;; machinery and the operative/applicative tagged-value protocol. ;; ;; Tagged values @@ -12,24 +12,31 @@ ;; keyed by symbol name; parent walks up the lookup chain. ;; ;; {:knl-tag :operative :impl FN} -;; A primitive operative. FN receives (args dyn-env) — args are the +;; Primitive operative. FN receives (args dyn-env) — args are the ;; UN-evaluated argument expressions, dyn-env is the calling env. ;; -;; {:knl-tag :applicative :underlying OP} -;; An applicative wraps an operative. Calls evaluate args first, then -;; forward to the underlying operative. +;; {:knl-tag :operative :params P :env-param EP :body B :static-env SE} +;; User-defined operative (built by $vau). Same tag; dispatch in +;; kernel-call-operative forks on which keys are present. ;; -;; User-defined ($vau) operatives are added in Phase 3 — same tag, with -;; extra fields :params :env-param :body :static-env. +;; {:knl-tag :applicative :underlying OP} +;; An applicative wraps an operative. Calls evaluate args first, +;; then forward to the underlying operative. +;; +;; The env-param of a user operative may be the sentinel :knl-ignore, +;; in which case the dynamic env is not bound. ;; ;; Public API ;; (kernel-eval EXPR ENV) — primary entry -;; (kernel-combine COMBINER ARGS DYN-ENV) — apply a combiner +;; (kernel-combine COMBINER ARGS DYN-ENV) +;; (kernel-call-operative OP ARGS DYN-ENV) +;; (kernel-bind-params! ENV PARAMS ARGS) ;; (kernel-make-env) / (kernel-extend-env P) ;; (kernel-env-bind! E N V) / (kernel-env-lookup E N) ;; (kernel-env-has? E N) / (kernel-env? V) ;; (kernel-make-primitive-operative IMPL) -;; (kernel-make-primitive-applicative IMPL) — IMPL receives evaled args +;; (kernel-make-primitive-applicative IMPL) +;; (kernel-make-user-operative PARAMS EPARAM BODY STATIC-ENV) ;; (kernel-wrap OP) / (kernel-unwrap APP) ;; (kernel-operative? V) / (kernel-applicative? V) / (kernel-combiner? V) ;; @@ -72,6 +79,10 @@ (define kernel-make-primitive-operative (fn (impl) {:impl impl :knl-tag :operative})) +(define + kernel-make-user-operative + (fn (params eparam body static-env) {:knl-tag :operative :static-env static-env :params params :body body :env-param eparam})) + (define kernel-operative? (fn (v) (and (dict? v) (= (get v :knl-tag) :operative)))) @@ -100,8 +111,6 @@ ((kernel-applicative? app) (get app :underlying)) (:else (error "kernel-unwrap: argument must be an applicative"))))) -;; A primitive applicative: sugar for (wrap (primitive-operative …)) where -;; the impl receives already-evaluated args. (define kernel-make-primitive-applicative (fn @@ -136,7 +145,8 @@ (fn (combiner args dyn-env) (cond - ((kernel-operative? combiner) ((get combiner :impl) args dyn-env)) + ((kernel-operative? combiner) + (kernel-call-operative combiner args dyn-env)) ((kernel-applicative? combiner) (kernel-combine (get combiner :underlying) @@ -144,6 +154,44 @@ dyn-env)) (:else (error (str "kernel-eval: not a combiner: " combiner)))))) +;; Operatives may be primitive (:impl is a host fn) or user-defined +;; (carry :params / :env-param / :body / :static-env). The dispatch +;; fork is here so kernel-combine stays small. +(define + kernel-call-operative + (fn + (op args dyn-env) + (cond + ((dict-has? op :impl) ((get op :impl) args dyn-env)) + ((dict-has? op :body) + (let + ((local (kernel-extend-env (get op :static-env)))) + (kernel-bind-params! local (get op :params) args) + (let + ((eparam (get op :env-param))) + (when + (not (= eparam :knl-ignore)) + (kernel-env-bind! local eparam dyn-env))) + (kernel-eval (get op :body) local))) + (:else (error "kernel-call-operative: malformed operative"))))) + +;; Phase 3 supports a flat parameter list only — destructuring later. +(define + kernel-bind-params! + (fn + (env params args) + (cond + ((or (nil? params) (= (length params) 0)) + (cond + ((or (nil? args) (= (length args) 0)) nil) + (:else (error "kernel-call: too many arguments")))) + ((or (nil? args) (= (length args) 0)) + (error "kernel-call: too few arguments")) + (:else + (begin + (kernel-env-bind! env (first params) (first args)) + (kernel-bind-params! env (rest params) (rest args))))))) + (define kernel-eval-args (fn @@ -155,7 +203,6 @@ (kernel-eval (first args) env) (kernel-eval-args (rest args) env)))))) -;; Evaluate a sequence of forms in env, returning the value of the last. (define kernel-eval-program (fn diff --git a/lib/kernel/runtime.sx b/lib/kernel/runtime.sx new file mode 100644 index 00000000..61d91440 --- /dev/null +++ b/lib/kernel/runtime.sx @@ -0,0 +1,167 @@ +;; lib/kernel/runtime.sx — the operative–applicative substrate. +;; +;; Builds the first user-visible operatives so Kernel programs can +;; construct their own combiners: +;; +;; $vau — primitive operative that returns a user operative +;; $lambda — primitive operative; sugar for (wrap ($vau …)) +;; wrap — primitive applicative; wraps an operative +;; unwrap — primitive applicative; extracts the underlying op +;; +;; In Kernel, $lambda is *defined* in terms of $vau and wrap: +;; ($define! $lambda +;; ($vau (formals . body) #ignore +;; (wrap (eval (list $vau formals #ignore (cons $sequence body)) env)))) +;; Phase 3 supplies it natively (single-expression body) so tests can +;; build applicatives without a working $define!/$sequence yet. The +;; native-then-portable migration is a Phase 4 concern. +;; +;; The env-param sentinel +;; ---------------------- +;; A user operative records an `:env-param` slot. If the source said +;; `#ignore`, the slot holds the keyword :knl-ignore and kernel-call- +;; operative skips binding the dynamic env. The parser doesn't recognise +;; `#ignore` yet (Phase 1 covered #t/#f only); guests must spell it +;; `_` for now — the spelling-to-sentinel conversion lives here in +;; knl-eparam-sentinel. +;; +;; Public API +;; (kernel-base-env) — fresh env with $vau, $lambda, wrap, unwrap +;; +;; Consumes: lib/kernel/eval.sx (everything tagged kernel-*). + +(define + knl-eparam-sentinel + (fn + (sym) + (cond + ((= sym "_") :knl-ignore) + ((= sym "#ignore") :knl-ignore) + (:else sym)))) + +;; Validate that a formals list is a plain list of symbol names. +(define + knl-formals-ok? + (fn + (formals) + (cond + ((not (list? formals)) false) + ((= (length formals) 0) true) + ((string? (first formals)) (knl-formals-ok? (rest formals))) + (:else false)))) + +;; ── $vau ───────────────────────────────────────────────────────── +;; ($vau FORMALS ENV-PARAM BODY) → user operative. +;; +;; FORMALS — unevaluated list of parameter symbols. +;; ENV-PARAM — symbol (or `_` / `#ignore`). +;; BODY — single expression (Phase 3 limitation; $sequence later). +;; +;; The returned operative closes over the env where $vau was invoked. + +(define + kernel-vau-impl + (fn + (args dyn-env) + (cond + ((not (= (length args) 3)) + (error "$vau: expects (formals env-param body)")) + (:else + (let + ((formals (first args)) + (eparam-raw (nth args 1)) + (body (nth args 2))) + (cond + ((not (knl-formals-ok? formals)) + (error "$vau: formals must be a list of symbols")) + ((not (string? eparam-raw)) + (error "$vau: env-param must be a symbol")) + (:else + (kernel-make-user-operative + formals + (knl-eparam-sentinel eparam-raw) + body + dyn-env)))))))) + +(define + kernel-vau-operative + (kernel-make-primitive-operative kernel-vau-impl)) + +;; ── $lambda ────────────────────────────────────────────────────── +;; ($lambda FORMALS BODY) → user applicative. +;; +;; Equivalent to (wrap ($vau FORMALS #ignore BODY)) — args are evaluated +;; before the operative body runs, and the operative ignores the dynamic +;; environment. + +(define + kernel-lambda-impl + (fn + (args dyn-env) + (cond + ((not (= (length args) 2)) + (error "$lambda: expects (formals body)")) + (:else + (let + ((formals (first args)) (body (nth args 1))) + (cond + ((not (knl-formals-ok? formals)) + (error "$lambda: formals must be a list of symbols")) + (:else + (kernel-wrap + (kernel-make-user-operative formals :knl-ignore body dyn-env))))))))) + +(define + kernel-lambda-operative + (kernel-make-primitive-operative kernel-lambda-impl)) + +;; ── wrap / unwrap as Kernel applicatives ───────────────────────── + +(define + kernel-wrap-applicative + (kernel-make-primitive-applicative + (fn + (args) + (cond + ((not (= (length args) 1)) + (error "wrap: expects exactly 1 argument")) + (:else (kernel-wrap (first args))))))) + +(define + kernel-unwrap-applicative + (kernel-make-primitive-applicative + (fn + (args) + (cond + ((not (= (length args) 1)) + (error "unwrap: expects exactly 1 argument")) + (:else (kernel-unwrap (first args))))))) + +;; Convenience predicates as applicatives too — tests want them. +(define + kernel-operative?-applicative + (kernel-make-primitive-applicative + (fn (args) (kernel-operative? (first args))))) + +(define + kernel-applicative?-applicative + (kernel-make-primitive-applicative + (fn (args) (kernel-applicative? (first args))))) + +;; ── Base environment ───────────────────────────────────────────── +;; A fresh env with the Phase 3 combiners bound. Standard env (Phase 4) +;; will extend this with $if, $define!, arithmetic, list ops, etc. + +(define + kernel-base-env + (fn + () + (let + ((env (kernel-make-env))) + (kernel-env-bind! env "$vau" kernel-vau-operative) + (kernel-env-bind! env "$lambda" kernel-lambda-operative) + (kernel-env-bind! env "wrap" kernel-wrap-applicative) + (kernel-env-bind! env "unwrap" kernel-unwrap-applicative) + (kernel-env-bind! env "operative?" kernel-operative?-applicative) + (kernel-env-bind! env "applicative?" kernel-applicative?-applicative) + env))) diff --git a/lib/kernel/tests/vau.sx b/lib/kernel/tests/vau.sx new file mode 100644 index 00000000..a1ac8b6d --- /dev/null +++ b/lib/kernel/tests/vau.sx @@ -0,0 +1,289 @@ +;; lib/kernel/tests/vau.sx — exercises lib/kernel/runtime.sx. +;; +;; Verifies the Phase 3 promise: user-defined operatives and applicatives +;; constructible from inside the language. Tests build a Kernel +;; base-env, bind a few helper applicatives (+, *, list, =, $if), and +;; run programs that construct and use custom combiners. + +(define kv-test-pass 0) +(define kv-test-fail 0) +(define kv-test-fails (list)) + +(define + kv-test + (fn + (name actual expected) + (if + (= actual expected) + (set! kv-test-pass (+ kv-test-pass 1)) + (begin + (set! kv-test-fail (+ kv-test-fail 1)) + (append! kv-test-fails {:name name :actual actual :expected expected}))))) + +(define kv-eval-src (fn (src env) (kernel-eval (kernel-parse src) env))) + +(define + kv-make-env + (fn + () + (let + ((env (kernel-base-env))) + (kernel-env-bind! + env + "+" + (kernel-make-primitive-applicative + (fn (args) (+ (first args) (nth args 1))))) + (kernel-env-bind! + env + "*" + (kernel-make-primitive-applicative + (fn (args) (* (first args) (nth args 1))))) + (kernel-env-bind! + env + "-" + (kernel-make-primitive-applicative + (fn (args) (- (first args) (nth args 1))))) + (kernel-env-bind! + env + "=" + (kernel-make-primitive-applicative + (fn (args) (= (first args) (nth args 1))))) + (kernel-env-bind! + env + "list" + (kernel-make-primitive-applicative (fn (args) args))) + (kernel-env-bind! + env + "cons" + (kernel-make-primitive-applicative + (fn (args) (cons (first args) (nth args 1))))) + (kernel-env-bind! + env + "$quote" + (kernel-make-primitive-operative (fn (args dyn-env) (first args)))) + (kernel-env-bind! + env + "$if" + (kernel-make-primitive-operative + (fn + (args dyn-env) + (if + (kernel-eval (first args) dyn-env) + (kernel-eval (nth args 1) dyn-env) + (kernel-eval (nth args 2) dyn-env))))) + env))) + +;; ── $vau: builds an operative ─────────────────────────────────── +(kv-test + "vau: identity returns first arg unevaluated" + (kv-eval-src "(($vau (a) _ a) hello)" (kv-make-env)) + "hello") + +(kv-test + "vau: returns args as raw expressions" + (kv-eval-src "(($vau (a b) _ (list a b)) (+ 1 2) (+ 3 4))" (kv-make-env)) + (list (list "+" 1 2) (list "+" 3 4))) + +(kv-test + "vau: env-param is a kernel env" + (kernel-env? (kv-eval-src "(($vau () e e))" (kv-make-env))) + true) + +(kv-test + "vau: returns operative" + (kernel-operative? (kv-eval-src "($vau (x) _ x)" (kv-make-env))) + true) + +(kv-test + "vau: returns operative not applicative" + (kernel-applicative? (kv-eval-src "($vau (x) _ x)" (kv-make-env))) + false) + +(kv-test + "vau: zero-arg body" + (kv-eval-src "(($vau () _ 42))" (kv-make-env)) + 42) + +(kv-test + "vau: static-env closure captured" + (let + ((outer (kv-make-env))) + (kernel-env-bind! outer "captured" 17) + (let + ((op (kv-eval-src "($vau () _ captured)" outer)) + (caller (kv-make-env))) + (kernel-env-bind! caller "captured" 99) + (kernel-combine op (list) caller))) + 17) + +(kv-test + "vau: env-param exposes caller's dynamic env" + (let + ((outer (kv-make-env))) + (kernel-env-bind! outer "x" 1) + (let + ((op (kv-eval-src "($vau () e e)" outer)) (caller (kv-make-env))) + (kernel-env-bind! caller "x" 2) + (let + ((e-val (kernel-combine op (list) caller))) + (kernel-env-lookup e-val "x")))) + 2) + +;; ── $lambda: applicatives evaluate their args ─────────────────── +(kv-test + "lambda: identity" + (kv-eval-src "(($lambda (x) x) 42)" (kv-make-env)) + 42) + +(kv-test + "lambda: addition" + (kv-eval-src "(($lambda (x y) (+ x y)) 3 4)" (kv-make-env)) + 7) + +(kv-test + "lambda: args are evaluated before bind" + (kv-eval-src "(($lambda (x) x) (+ 2 3))" (kv-make-env)) + 5) + +(kv-test + "lambda: zero args" + (kv-eval-src "(($lambda () 99))" (kv-make-env)) + 99) + +(kv-test + "lambda: returns applicative" + (kernel-applicative? (kv-eval-src "($lambda (x) x)" (kv-make-env))) + true) + +(kv-test + "lambda: returns applicative not operative" + (kernel-operative? (kv-eval-src "($lambda (x) x)" (kv-make-env))) + false) + +(kv-test + "lambda: higher-order" + (kv-eval-src "(($lambda (f) (f 10)) ($lambda (x) (+ x 1)))" (kv-make-env)) + 11) + +;; ── wrap / unwrap as user-callable applicatives ───────────────── + +(kv-test + "wrap: makes applicative from operative" + (kernel-applicative? (kv-eval-src "(wrap ($vau (x) _ x))" (kv-make-env))) + true) + +(kv-test + "wrap: result evaluates its arg" + (kv-eval-src "((wrap ($vau (x) _ x)) (+ 1 2))" (kv-make-env)) + 3) + +(kv-test + "unwrap: extracts operative from applicative" + (kernel-operative? (kv-eval-src "(unwrap ($lambda (x) x))" (kv-make-env))) + true) + +(kv-test + "wrap/unwrap roundtrip preserves identity" + (kv-eval-src + "(($lambda (op) (= op (unwrap (wrap op)))) ($vau (x) _ x))" + (kv-make-env)) + true) + +;; ── operative? / applicative? as user-visible predicates ──────── + +(kv-test + "operative? on vau result" + (kv-eval-src "(operative? ($vau (x) _ x))" (kv-make-env)) + true) + +(kv-test + "operative? on lambda result" + (kv-eval-src "(operative? ($lambda (x) x))" (kv-make-env)) + false) + +(kv-test + "applicative? on lambda result" + (kv-eval-src "(applicative? ($lambda (x) x))" (kv-make-env)) + true) + +(kv-test + "applicative? on vau result" + (kv-eval-src "(applicative? ($vau (x) _ x))" (kv-make-env)) + false) + +(kv-test + "operative? on number" + (kv-eval-src "(operative? 42)" (kv-make-env)) + false) + +;; ── Build BOTH layers from user code ──────────────────────────── +;; The headline Phase 3 test: defining an operative on top of an +;; applicative defined on top of a vau. + +(kv-test + "custom: applicative + operative compose" + (let + ((env (kv-make-env))) + (kernel-env-bind! env "square" (kv-eval-src "($lambda (x) (* x x))" env)) + (kv-eval-src "(square 4)" env)) + 16) + +(kv-test "custom: operative captures argument syntax" + ;; ($capture x) returns the raw expression `x`, regardless of value. + (let ((env (kv-make-env))) + (kernel-env-bind! env "$capture" + (kv-eval-src "($vau (form) _ form)" env)) + (kv-eval-src "($capture (+ 1 2))" env)) + (list "+" 1 2)) + +(kv-test "custom: applicative re-wraps an operative" + ;; Build a captured operative, then wrap it into an applicative that + ;; evaluates args before re-entry. This exercises wrap+$vau composed. + (let ((env (kv-make-env))) + (kernel-env-bind! env "id-app" + (kv-eval-src "(wrap ($vau (x) _ x))" env)) + (kv-eval-src "(id-app (+ 10 20))" env)) + 30) + +;; ── Error cases ────────────────────────────────────────────────── + +(kv-test + "vau: rejects non-list formals" + (guard (e (true :raised)) (kv-eval-src "($vau x _ x)" (kv-make-env))) + :raised) + +(kv-test + "vau: rejects non-symbol formal" + (guard (e (true :raised)) (kv-eval-src "($vau (1) _ x)" (kv-make-env))) + :raised) + +(kv-test + "vau: rejects non-symbol env-param" + (guard (e (true :raised)) (kv-eval-src "($vau (x) 7 x)" (kv-make-env))) + :raised) + +(kv-test + "vau: too few args at call site" + (guard + (e (true :raised)) + (kv-eval-src "(($vau (x y) _ x) 1)" (kv-make-env))) + :raised) + +(kv-test + "vau: too many args at call site" + (guard + (e (true :raised)) + (kv-eval-src "(($vau (x) _ x) 1 2)" (kv-make-env))) + :raised) + +(kv-test + "wrap: rejects non-operative" + (guard (e (true :raised)) (kv-eval-src "(wrap 42)" (kv-make-env))) + :raised) + +(kv-test + "unwrap: rejects non-applicative" + (guard (e (true :raised)) (kv-eval-src "(unwrap 42)" (kv-make-env))) + :raised) + +(define kv-tests-run! (fn () {:total (+ kv-test-pass kv-test-fail) :passed kv-test-pass :failed kv-test-fail :fails kv-test-fails})) diff --git a/plans/kernel-on-sx.md b/plans/kernel-on-sx.md index 860415c0..adeae4ca 100644 --- a/plans/kernel-on-sx.md +++ b/plans/kernel-on-sx.md @@ -68,11 +68,11 @@ The whole interesting thing: there are no special forms hardcoded in the evaluat - [x] 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. -- [ ] `$vau` builds operatives; `$lambda` is `wrap` ∘ `$vau`. -- [ ] `wrap` / `unwrap` round-trip cleanly. -- [ ] Tests: define a custom operative, define a custom applicative on top of it. +- [x] Operative tagged value: `{:type :operative :params :env-param :body :static-env}`. +- [x] Applicative tagged value wraps an operative + the "evaluate args first" contract. +- [x] `$vau` builds operatives; `$lambda` is `wrap` ∘ `$vau`. +- [x] `wrap` / `unwrap` round-trip cleanly. +- [x] 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. @@ -100,6 +100,15 @@ The whole interesting thing: there are no special forms hardcoded in the evaluat **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-ignore` in 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 with `refl-eval` from 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 `:impl` is primitive, presence of `:body` is 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. @@ -119,6 +128,7 @@ The motivation is that SX's host `make-env` family is registered only in HTTP/si ## Progress log +- 2026-05-11 — Phase 3 operatives landed. `lib/kernel/runtime.sx` adds `$vau` (primitive operative that returns a user operative), `$lambda` (sugar for `wrap ∘ $vau`), `wrap` and `unwrap` (Kernel-level applicatives), plus `operative?` and `applicative?` predicates. `kernel-base-env` wires them all into a fresh env. `kernel-eval.sx` now dispatches in `kernel-call-operative` between primitive ops (carry `:impl`) and user ops (carry `:params :env-param :body :static-env`). Parameter binding is a flat list — destructuring/`&rest` deferred. Env-param sentinel: spell `_` or `#ignore` → `:knl-ignore`, which skips the dyn-env bind. 34 tests in `tests/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`, `unwrap` are 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 proposed `lib/guest/reflective/combiner.sx` candidate. - 2026-05-10 — Phase 2 evaluator landed. `lib/kernel/eval.sx` is `lookup-and-combine`: zero hardcoded special forms. `kernel-eval EXPR ENV` dispatches on shape — literals self-evaluate, Kernel strings unwrap, symbols lookup, lists evaluate head and combine. `kernel-combine` distinguishes operatives (impl receives un-evaluated args + dynamic env) from applicatives (eval args, recurse into underlying op). `kernel-wrap`/`kernel-unwrap` round-trip cleanly. 36 tests verify literal evaluation, symbol lookup with parent-chain shadowing, tagged-value predicates, and the operative-vs-applicative contract (notably `$if` only evaluates the chosen branch, `$quote` returns its arg unevaluated). chisel: shapes-reflective. Substrate gap surfaced: SX's `make-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 the `lib/guest/reflective/env.sx` candidate 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.sx` reads 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 in `lib/kernel/tests/parse.sx` pass via `sx_server.exe` epoch protocol. chisel: consumes-lex (uses `lex-digit?` and `lex-whitespace?` from `lib/guest/lex.sx` — pratt deliberately not consumed because Kernel is plain s-expressions, no precedence climbing).