From 7e57e0b215e122a8df3e4d2c0fd0e253f8be2c0f Mon Sep 17 00:00:00 2001 From: giles Date: Sun, 10 May 2026 20:50:42 +0000 Subject: [PATCH] =?UTF-8?q?kernel:=20Phase=202=20evaluator=20=E2=80=94=20l?= =?UTF-8?q?ookup-and-combine=20+=2036=20tests=20[shapes-reflective]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kernel-eval/kernel-combine dispatch on tagged values: operatives see un-evaluated args + dynamic env; applicatives evaluate args then recurse. No hardcoded special forms — $if/$quote tested as ordinary operatives built on the fly. Pure-SX env representation {:knl-tag :env :bindings DICT :parent P}, surfaced as a candidate lib/guest/reflective/env.sx API since SX make-env is HTTP-mode only. --- lib/kernel/eval.sx | 169 ++++++++++++++++++++++++ lib/kernel/tests/eval.sx | 270 +++++++++++++++++++++++++++++++++++++++ plans/kernel-on-sx.md | 21 ++- 3 files changed, 455 insertions(+), 5 deletions(-) create mode 100644 lib/kernel/eval.sx create mode 100644 lib/kernel/tests/eval.sx diff --git a/lib/kernel/eval.sx b/lib/kernel/eval.sx new file mode 100644 index 00000000..b694b5ae --- /dev/null +++ b/lib/kernel/eval.sx @@ -0,0 +1,169 @@ +;; lib/kernel/eval.sx — Kernel evaluator (Phase 2 skeleton). +;; +;; 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 +;; machinery and the operative/applicative tagged-value protocol. +;; +;; Tagged values +;; ------------- +;; {:knl-tag :env :bindings DICT :parent PARENT-OR-NIL} +;; A first-class Kernel environment. Bindings is a mutable SX dict +;; 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 +;; 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. +;; +;; User-defined ($vau) operatives are added in Phase 3 — same tag, with +;; extra fields :params :env-param :body :static-env. +;; +;; Public API +;; (kernel-eval EXPR ENV) — primary entry +;; (kernel-combine COMBINER ARGS DYN-ENV) — apply a combiner +;; (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-wrap OP) / (kernel-unwrap APP) +;; (kernel-operative? V) / (kernel-applicative? V) / (kernel-combiner? V) +;; +;; Consumes: lib/kernel/parser.sx (kernel-string?, kernel-string-value) + +;; ── Environments — first-class, pure-SX (binding dict + parent) ── + +(define kernel-env? (fn (v) (and (dict? v) (= (get v :knl-tag) :env)))) + +(define kernel-make-env (fn () {:parent nil :knl-tag :env :bindings {}})) + +(define kernel-extend-env (fn (parent) {:parent parent :knl-tag :env :bindings {}})) + +(define + kernel-env-bind! + (fn (env name val) (dict-set! (get env :bindings) name val) val)) + +(define + kernel-env-has? + (fn + (env name) + (cond + ((nil? env) false) + ((not (kernel-env? env)) false) + ((dict-has? (get env :bindings) name) true) + (:else (kernel-env-has? (get env :parent) name))))) + +(define + kernel-env-lookup + (fn + (env name) + (cond + ((nil? env) (error (str "kernel-eval: unbound symbol: " name))) + ((not (kernel-env? env)) + (error (str "kernel-eval: corrupt env: " env))) + ((dict-has? (get env :bindings) name) (get (get env :bindings) name)) + (:else (kernel-env-lookup (get env :parent) name))))) + +;; ── Tagged-value constructors and predicates ───────────────────── + +(define kernel-make-primitive-operative (fn (impl) {:impl impl :knl-tag :operative})) + +(define + kernel-operative? + (fn (v) (and (dict? v) (= (get v :knl-tag) :operative)))) + +(define + kernel-applicative? + (fn (v) (and (dict? v) (= (get v :knl-tag) :applicative)))) + +(define + kernel-combiner? + (fn (v) (or (kernel-operative? v) (kernel-applicative? v)))) + +(define + kernel-wrap + (fn + (op) + (cond + ((kernel-operative? op) {:knl-tag :applicative :underlying op}) + (:else (error "kernel-wrap: argument must be an operative"))))) + +(define + kernel-unwrap + (fn + (app) + (cond + ((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 + (impl) + (kernel-wrap + (kernel-make-primitive-operative (fn (args dyn-env) (impl args)))))) + +;; ── The evaluator ──────────────────────────────────────────────── + +(define + kernel-eval + (fn + (expr env) + (cond + ((number? expr) expr) + ((boolean? expr) expr) + ((nil? expr) expr) + ((kernel-string? expr) (kernel-string-value expr)) + ((string? expr) (kernel-env-lookup env expr)) + ((list? expr) + (cond + ((= (length expr) 0) expr) + (:else + (let + ((combiner (kernel-eval (first expr) env)) + (args (rest expr))) + (kernel-combine combiner args env))))) + (:else (error (str "kernel-eval: unknown form: " expr)))))) + +(define + kernel-combine + (fn + (combiner args dyn-env) + (cond + ((kernel-operative? combiner) ((get combiner :impl) args dyn-env)) + ((kernel-applicative? combiner) + (kernel-combine + (get combiner :underlying) + (kernel-eval-args args dyn-env) + dyn-env)) + (:else (error (str "kernel-eval: not a combiner: " combiner)))))) + +(define + kernel-eval-args + (fn + (args env) + (cond + ((or (nil? args) (= (length args) 0)) (list)) + (:else + (cons + (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 + (forms env) + (cond + ((or (nil? forms) (= (length forms) 0)) nil) + ((= (length forms) 1) (kernel-eval (first forms) env)) + (:else + (begin + (kernel-eval (first forms) env) + (kernel-eval-program (rest forms) env)))))) diff --git a/lib/kernel/tests/eval.sx b/lib/kernel/tests/eval.sx new file mode 100644 index 00000000..7e2f3ada --- /dev/null +++ b/lib/kernel/tests/eval.sx @@ -0,0 +1,270 @@ +;; lib/kernel/tests/eval.sx — exercises lib/kernel/eval.sx. +;; +;; Phase 2 covers literal evaluation, symbol lookup, and combiner +;; dispatch (operative vs applicative). Standard-environment operatives +;; ($if, $define!, $lambda, …) arrive in Phase 4, so tests build a +;; minimal env on the fly and verify the dispatch contract directly. + +(define ke-test-pass 0) +(define ke-test-fail 0) +(define ke-test-fails (list)) + +(define + ke-test + (fn + (name actual expected) + (if + (= actual expected) + (set! ke-test-pass (+ ke-test-pass 1)) + (begin + (set! ke-test-fail (+ ke-test-fail 1)) + (append! ke-test-fails {:name name :actual actual :expected expected}))))) + +;; ── helpers ────────────────────────────────────────────────────── + +(define ke-eval-src (fn (src env) (kernel-eval (kernel-parse src) env))) + +(define + ke-make-test-env + (fn + () + (let + ((env (kernel-make-env))) + (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 + "$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))) + +;; ── literal evaluation ─────────────────────────────────────────── +(ke-test "lit: number" (ke-eval-src "42" (kernel-make-env)) 42) +(ke-test "lit: zero" (ke-eval-src "0" (kernel-make-env)) 0) +(ke-test "lit: float" (ke-eval-src "3.14" (kernel-make-env)) 3.14) +(ke-test "lit: true" (ke-eval-src "#t" (kernel-make-env)) true) +(ke-test "lit: false" (ke-eval-src "#f" (kernel-make-env)) false) +(ke-test "lit: string" (ke-eval-src "\"hello\"" (kernel-make-env)) "hello") +(ke-test "lit: empty list" (ke-eval-src "()" (kernel-make-env)) (list)) + +;; ── symbol lookup ──────────────────────────────────────────────── +(ke-test + "sym: bound to number" + (let + ((env (kernel-make-env))) + (kernel-env-bind! env "x" 100) + (ke-eval-src "x" env)) + 100) + +(ke-test + "sym: bound to string" + (let + ((env (kernel-make-env))) + (kernel-env-bind! env "name" "kernel") + (ke-eval-src "name" env)) + "kernel") + +(ke-test + "sym: parent-chain lookup" + (let + ((p (kernel-make-env))) + (kernel-env-bind! p "outer" 1) + (let + ((c (kernel-extend-env p))) + (kernel-env-bind! c "inner" 2) + (+ (ke-eval-src "outer" c) (ke-eval-src "inner" c)))) + 3) + +(ke-test + "sym: child shadows parent" + (let + ((p (kernel-make-env))) + (kernel-env-bind! p "x" 1) + (let + ((c (kernel-extend-env p))) + (kernel-env-bind! c "x" 2) + (ke-eval-src "x" c))) + 2) + +(ke-test + "env-has?: present" + (let + ((env (kernel-make-env))) + (kernel-env-bind! env "x" 1) + (kernel-env-has? env "x")) + true) + +(ke-test + "env-has?: missing" + (kernel-env-has? (kernel-make-env) "nope") + false) + +;; ── tagged-value predicates ───────────────────────────────────── +(ke-test + "tag: operative?" + (kernel-operative? (kernel-make-primitive-operative (fn (a e) nil))) + true) + +(ke-test + "tag: applicative?" + (kernel-applicative? (kernel-make-primitive-applicative (fn (a) nil))) + true) + +(ke-test + "tag: combiner? operative" + (kernel-combiner? (kernel-make-primitive-operative (fn (a e) nil))) + true) + +(ke-test + "tag: combiner? applicative" + (kernel-combiner? (kernel-make-primitive-applicative (fn (a) nil))) + true) + +(ke-test "tag: combiner? number" (kernel-combiner? 42) false) + +(ke-test "tag: number is not operative" (kernel-operative? 42) false) + +;; ── wrap / unwrap ──────────────────────────────────────────────── +(ke-test + "wrap+unwrap roundtrip" + (let + ((op (kernel-make-primitive-operative (fn (a e) :sentinel)))) + (= (kernel-unwrap (kernel-wrap op)) op)) + true) + +(ke-test + "wrap produces applicative" + (kernel-applicative? + (kernel-wrap (kernel-make-primitive-operative (fn (a e) nil)))) + true) + +(ke-test + "unwrap of primitive-applicative is operative" + (kernel-operative? + (kernel-unwrap (kernel-make-primitive-applicative (fn (a) nil)))) + true) + +;; ── combiner dispatch — applicatives evaluate their args ───────── +(ke-test + "applicative: simple call" + (ke-eval-src "(+ 2 3)" (ke-make-test-env)) + 5) + +(ke-test + "applicative: nested" + (ke-eval-src "(+ (+ 1 2) (+ 3 4))" (ke-make-test-env)) + 10) + +(ke-test + "applicative: receives evaluated args" + (let + ((env (ke-make-test-env))) + (kernel-env-bind! env "x" 10) + (kernel-env-bind! env "y" 20) + (ke-eval-src "(+ x y)" env)) + 30) + +(ke-test + "applicative: list builds an SX list of values" + (let + ((env (ke-make-test-env))) + (kernel-env-bind! env "a" 1) + (kernel-env-bind! env "b" 2) + (ke-eval-src "(list a b 99)" env)) + (list 1 2 99)) + +;; ── combiner dispatch — operatives DO NOT evaluate their args ─── +(ke-test + "operative: $quote returns symbol unevaluated" + (ke-eval-src "($quote foo)" (ke-make-test-env)) + "foo") + +(ke-test + "operative: $quote returns list unevaluated" + (ke-eval-src "($quote (+ 1 2))" (ke-make-test-env)) + (list "+" 1 2)) + +(ke-test + "operative: $if true branch" + (ke-eval-src "($if #t 1 2)" (ke-make-test-env)) + 1) + +(ke-test + "operative: $if false branch" + (ke-eval-src "($if #f 1 2)" (ke-make-test-env)) + 2) + +(ke-test + "operative: $if doesn't eval untaken branch" + (ke-eval-src "($if #t 99 unbound)" (ke-make-test-env)) + 99) + +(ke-test + "operative: $if takes dynamic env for branches" + (let + ((env (ke-make-test-env))) + (kernel-env-bind! env "x" 7) + (ke-eval-src "($if #t x 0)" env)) + 7) + +;; ── operative built ON-THE-FLY can inspect raw expressions ────── +(ke-test + "operative: sees raw symbol head" + (let + ((env (kernel-make-env))) + (kernel-env-bind! + env + "head" + (kernel-make-primitive-operative (fn (args dyn-env) (first args)))) + (ke-eval-src "(head (+ 1 2))" env)) + (list "+" 1 2)) + +(ke-test + "operative: sees dynamic env" + (let + ((env (kernel-make-env))) + (kernel-env-bind! env "x" 999) + (kernel-env-bind! + env + "$probe" + (kernel-make-primitive-operative + (fn (args dyn-env) (kernel-env-lookup dyn-env "x")))) + (ke-eval-src "($probe ignored)" env)) + 999) + +;; ── error cases ────────────────────────────────────────────────── +(ke-test + "error: unbound symbol" + (guard + (e (true :raised)) + (kernel-eval (kernel-parse "nope") (kernel-make-env))) + :raised) + +(ke-test + "error: combine non-combiner" + (guard + (e (true :raised)) + (let + ((env (kernel-make-env))) + (kernel-env-bind! env "x" 42) + (kernel-eval (kernel-parse "(x 1)") env))) + :raised) + +(define ke-tests-run! (fn () {:total (+ ke-test-pass ke-test-fail) :passed ke-test-pass :failed ke-test-fail :fails ke-test-fails})) diff --git a/plans/kernel-on-sx.md b/plans/kernel-on-sx.md index a9daf7a3..860415c0 100644 --- a/plans/kernel-on-sx.md +++ b/plans/kernel-on-sx.md @@ -61,11 +61,11 @@ The whole interesting thing: there are no special forms hardcoded in the evaluat - [x] 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`/`lambda` are env-bound. -- [ ] Tests in `lib/kernel/tests/eval.sx`. +- [x] `kernel-eval expr env` — primary entry, walks AST, threads env as a value. +- [x] Symbol lookup → environment value (using SX env-as-value primitives). +- [x] List → look up head, dispatch on tag (applicative vs operative). +- [x] No hardcoded special forms — even `if`/`define`/`lambda` are env-bound. +- [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}`. @@ -100,6 +100,16 @@ 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/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 @@ -109,6 +119,7 @@ The whole interesting thing: there are no special forms hardcoded in the evaluat ## Progress log +- 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). ## Blockers