From a4a77533145ea2c09a5d71cdfe0f2ff8850ea27b Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 11 May 2026 21:06:35 +0000 Subject: [PATCH] kernel: $quasiquote runtime + reflective/quoting.sx sketch [shapes-reflective] MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit kernel-quasiquote-operative walks the template via mutually-recursive knl-quasi-walk ↔ knl-quasi-walk-list. $unquote forms eval in dyn-env; $unquote-splicing splices list-valued results. No depth tracking (nested quasiquotes flatten). 8 new tests, 230 total. Sketched the universal reflective quoting kit API for the eventual Phase 7 extraction. --- lib/kernel/runtime.sx | 52 ++++++++++++++++++++++++++++++++++++ lib/kernel/tests/standard.sx | 31 +++++++++++++++++++++ plans/kernel-on-sx.md | 7 +++++ 3 files changed, 90 insertions(+) diff --git a/lib/kernel/runtime.sx b/lib/kernel/runtime.sx index 0ed7521c..a7fde1eb 100644 --- a/lib/kernel/runtime.sx +++ b/lib/kernel/runtime.sx @@ -194,6 +194,57 @@ ((not (= (length args) 1)) (error "$quote: expects 1 argument")) (:else (first args)))))) +;; Quasiquote: walks the template, evaluating `$unquote` forms in the +;; dynamic env and splicing `$unquote-splicing` list results. +(define knl-quasi-walk + (fn (form dyn-env) + (cond + ((not (list? form)) form) + ((= (length form) 0) form) + ((and (string? (first form)) (= (first form) "$unquote")) + (cond + ((not (= (length form) 2)) + (error "$unquote: expects exactly 1 argument")) + (:else (kernel-eval (nth form 1) dyn-env)))) + (:else (knl-quasi-walk-list form dyn-env))))) + +(define knl-quasi-walk-list + (fn (forms dyn-env) + (cond + ((or (nil? forms) (= (length forms) 0)) (list)) + (:else + (let ((head (first forms))) + (cond + ((and (list? head) + (= (length head) 2) + (string? (first head)) + (= (first head) "$unquote-splicing")) + (let ((spliced (kernel-eval (nth head 1) dyn-env))) + (cond + ((not (list? spliced)) + (error "$unquote-splicing: value must be a list")) + (:else + (knl-list-concat + spliced + (knl-quasi-walk-list (rest forms) dyn-env)))))) + (:else + (cons (knl-quasi-walk head dyn-env) + (knl-quasi-walk-list (rest forms) dyn-env))))))))) + +(define knl-list-concat + (fn (xs ys) + (cond + ((or (nil? xs) (= (length xs) 0)) ys) + (:else (cons (first xs) (knl-list-concat (rest xs) ys)))))) + +(define kernel-quasiquote-operative + (kernel-make-primitive-operative + (fn (args dyn-env) + (cond + ((not (= (length args) 1)) + (error "$quasiquote: expects exactly 1 argument")) + (:else (knl-quasi-walk (first args) dyn-env)))))) + (define kernel-eval-applicative (kernel-make-primitive-applicative @@ -462,6 +513,7 @@ (kernel-env-bind! env "$define!" kernel-define!-operative) (kernel-env-bind! env "$sequence" kernel-sequence-operative) (kernel-env-bind! env "$quote" kernel-quote-operative) + (kernel-env-bind! env "$quasiquote" kernel-quasiquote-operative) (kernel-env-bind! env "eval" kernel-eval-applicative) (kernel-env-bind! env diff --git a/lib/kernel/tests/standard.sx b/lib/kernel/tests/standard.sx index 1a428bd0..32a87ca9 100644 --- a/lib/kernel/tests/standard.sx +++ b/lib/kernel/tests/standard.sx @@ -254,4 +254,35 @@ (ks-eval-in "z" env)) 77) +;; ── quasiquote ────────────────────────────────────────────────── +(ks-test "qq: plain atom" (ks-eval "`hello") "hello") +(ks-test "qq: plain list" (ks-eval "`(a b c)") (list "a" "b" "c")) +(ks-test "qq: unquote splices value" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! x 42)" env) + (ks-eval-in "`(a ,x b)" env)) (list "a" 42 "b")) +(ks-test "qq: unquote-splicing splices list" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! xs (list 1 2 3))" env) + (ks-eval-in "`(a ,@xs b)" env)) (list "a" 1 2 3 "b")) +(ks-test "qq: unquote-splicing at end" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! xs (list 9 8))" env) + (ks-eval-in "`(a b ,@xs)" env)) (list "a" "b" 9 8)) +(ks-test "qq: unquote-splicing at start" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! xs (list 1 2))" env) + (ks-eval-in "`(,@xs c)" env)) (list 1 2 "c")) +(ks-test "qq: nested list with unquote inside" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! x 5)" env) + (ks-eval-in "`(a (b ,x) c)" env)) + (list "a" (list "b" 5) "c")) +(ks-test "qq: error on bare unquote-splicing into non-list" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! x 42)" env) + (guard (e (true :raised)) + (ks-eval-in "`(a ,@x b)" env))) + :raised) + (define ks-tests-run! (fn () {:total (+ ks-test-pass ks-test-fail) :passed ks-test-pass :failed ks-test-fail :fails ks-test-fails})) diff --git a/plans/kernel-on-sx.md b/plans/kernel-on-sx.md index 7de414f2..d64f13ae 100644 --- a/plans/kernel-on-sx.md +++ b/plans/kernel-on-sx.md @@ -108,6 +108,12 @@ When the second consumer arrives, the extraction work is: rename `kernel-*` → **May propose:** `lib/guest/reflective/` sub-layer — environment manipulation, evaluator-as-value, applicative/operative dispatch protocols. +**Proposed `lib/guest/reflective/quoting.sx` API** (from quasiquote chiselling — pending second consumer): +- `(refl-quasi-walk FORM ENV)` — top-level entry. Recursively walks FORM; an `$unquote` sub-expression is evaluated in ENV and replaces itself in the result. +- `(refl-quasi-walk-list FORMS ENV)` — walks a list of forms, splicing `$unquote-splicing` results inline. +- `(refl-list-concat XS YS)` — pure-SX list concatenation (no host dependency on `append`). +- Driving insight: every reflective Lisp eventually adds quasiquote, and the recursion-with-splicing structure is identical across them. Nesting depth tracking (for `` ``e `` inside `` `e ``) is the only Kernel-specific complication; for the kit, a depth-tracking variant `refl-quasi-walk-depth FORM ENV DEPTH` would be the second-tier API. + **Proposed `lib/guest/reflective/hygiene.sx` API** (from Phase 6 chiselling — pending second consumer): - The substrate decision: a user-defined combiner's body runs in `(extend STATIC-ENV)`, NOT in the dyn-env. Any `$define!` inside the body binds in this fresh child, so callers' envs stay untouched. This is the cheap, lexical-scope hygiene story that R-1RK has had since the start. - `(refl-let BINDINGS BODY)` — bind names in a fresh child of dyn-env, evaluate body there. Values evaluated in OUTER env (parallel semantics). @@ -148,6 +154,7 @@ The motivation is that SX's host `make-env` family is registered only in HTTP/si ## Progress log +- 2026-05-11 — `$quasiquote` runtime. The parser's reader macros (Phase 1.5) produced unevaluated `$quasiquote`/`$unquote`/`$unquote-splicing` forms; the runtime side now interprets them. `kernel-quasiquote-operative` walks the template via mutual recursion `knl-quasi-walk` ↔ `knl-quasi-walk-list`: atoms and empty lists pass through; an `($unquote X)` head form returns `(kernel-eval X dyn-env)`; an `($unquote-splicing X)` *inside* a list evaluates X and splices its list result via `knl-list-concat`. Nesting depth (`` `\`...\` ``) is not tracked — for Phase-1.5 simplicity, nested quasiquotes flatten. 8 new tests in `tests/standard.sx`. chisel: shapes-reflective. The quoting walker shape is universal across reflective Lisps; sketched the `lib/guest/reflective/quoting.sx` candidate API (`refl-quasi-walk`, `refl-quasi-walk-list`, `refl-list-concat`). 230 tests total. - 2026-05-11 — Multi-expression body for `$vau`/`$lambda`. Both forms now accept `(formals env-param body1 body2 ...)` / `(formals body1 body2 ...)`. Implementation: `:body` slot now holds a LIST of forms (was a single expression); `kernel-call-operative` calls a new `knl-eval-body` that evaluates each in sequence, returning the last. No dependency on `$sequence` being in static-env — the iteration lives at the host level. 5 new tests in `tests/vau.sx` (multi-body lambda, multi-body vau, sequenced `$define!`, zero-arg multi-body). chisel: nothing (Kernel-internal improvement; doesn't change the reflective API surface). 223 tests total. - 2026-05-11 — Phase 1 reader macros landed (the deferred checkbox from Phase 1). Parser now recognises four shorthand forms: `'expr` → `($quote expr)`, `` `expr `` → `($quasiquote expr)`, `,expr` → `($unquote expr)`, `,@expr` → `($unquote-splicing expr)`. Delimiter set extended to include `'`, `` ` ``, `,` so they don't slip into adjacent atom tokens. The runtime already has `$quote`; `$quasiquote` / `$unquote` / `$unquote-splicing` are not bound yet (would need a recursive walker for quasi-quote expansion — left for whenever a consumer needs it). 8 new reader-macro tests in `tests/parse.sx` bring parse to 62, total to 218. chisel: consumes-lex (parser still leans on `lib/guest/lex.sx` whitespace + digit predicates only). - 2026-05-11 — Phase 7 proposal complete (partial extraction per two-consumer rule). Consolidated the four candidate reflective files into the plan's API surface section: `env.sx` (Phase 2), `combiner.sx` (Phase 3), `evaluator.sx` (Phase 4), `hygiene.sx` (Phase 6). Total proposed surface ~25 functions, all sketched with signatures and representation notes. Kernel alone is the first consumer; the *second* consumer must materialise before any actual extraction. Listed candidate second consumers in priority order: metacircular Scheme (highest fit — same scope semantics), CL macro evaluator (medium fit — would drive the deferred hygiene work), Maru/Schemely (eventual). Extraction is estimated at <500 lines moved when the time comes — clean separation of concerns across this loop's six prior commits means the rename-and-move work is mechanical, not a redesign. chisel: proposes-reflective-extraction (the candidate API surface is the entire artefact of this phase). 210 tests across six test files, zero regressions across the loop. The kernel-on-sx loop sustained one feature per commit for seven commits.