From c21eb9d5ad0282dab6dcea68f3e156296677cc89 Mon Sep 17 00:00:00 2001 From: giles Date: Mon, 11 May 2026 21:01:01 +0000 Subject: [PATCH] kernel: reader macros + 8 tests (Phase 1 closure) [consumes-lex] Parser now reads 'expr, \`expr, ,expr, ,@expr as the four standard shorthands. Quote uses existing $quote operative; quasiquote / unquote / unquote-splicing recognised but not yet expanded at runtime (left for first consumer to drive). 218 tests total across six suites. --- lib/kernel/parser.sx | 15 ++++++++++++++- lib/kernel/tests/parse.sx | 24 ++++++++++++++++++++++++ plans/kernel-on-sx.md | 3 ++- 3 files changed, 40 insertions(+), 2 deletions(-) diff --git a/lib/kernel/parser.sx b/lib/kernel/parser.sx index bd1d76c1..8bd7a4d6 100644 --- a/lib/kernel/parser.sx +++ b/lib/kernel/parser.sx @@ -37,7 +37,10 @@ (= c "(") (= c ")") (= c "\"") - (= c ";")))) + (= c ";") + (= c "'") + (= c "`") + (= c ",")))) ;; Numeric grammar: [+-]? (digit+ ('.' digit+)? | '.' digit+) ([eE][+-]?digit+)? (define @@ -199,6 +202,16 @@ ((= (at) "(") (do (adv) (read-list (list)))) ((= (at) "\"") (do (adv) (kernel-string-make (read-string-body "")))) + ((= (at) "'") + (do (adv) (list "$quote" (read-form)))) + ((= (at) "`") + (do (adv) (list "$quasiquote" (read-form)))) + ((= (at) ",") + (do (adv) + (cond + ((= (at) "@") + (do (adv) (list "$unquote-splicing" (read-form)))) + (:else (list "$unquote" (read-form)))))) (:else (classify-atom (read-atom-body "")))))) (define read-list diff --git a/lib/kernel/tests/parse.sx b/lib/kernel/tests/parse.sx index fdfd7850..d70e7bb6 100644 --- a/lib/kernel/tests/parse.sx +++ b/lib/kernel/tests/parse.sx @@ -131,4 +131,28 @@ (knl-test "identity: wrap" (kernel-parse "wrap") "wrap") (knl-test "identity: unwrap" (kernel-parse "unwrap") "unwrap") +;; ── reader macros ───────────────────────────────────────────────── +(knl-test "reader: 'foo → ($quote foo)" + (kernel-parse "'foo") (list "$quote" "foo")) +(knl-test "reader: '(a b c)" + (kernel-parse "'(a b c)") (list "$quote" (list "a" "b" "c"))) +(knl-test "reader: nested quotes" + (kernel-parse "''x") + (list "$quote" (list "$quote" "x"))) +(knl-test "reader: ` quasiquote" + (kernel-parse "`x") (list "$quasiquote" "x")) +(knl-test "reader: , unquote" + (kernel-parse ",x") (list "$unquote" "x")) +(knl-test "reader: ,@ unquote-splicing" + (kernel-parse ",@x") (list "$unquote-splicing" "x")) +(knl-test "reader: quasi-mix" + (kernel-parse "`(a ,b ,@c)") + (list "$quasiquote" + (list "a" + (list "$unquote" "b") + (list "$unquote-splicing" "c")))) +(knl-test "reader: quote separates from neighbouring atom" + (kernel-parse "(a 'b c)") + (list "a" (list "$quote" "b") "c")) + (define knl-tests-run! (fn () {:total (+ knl-test-pass knl-test-fail) :passed knl-test-pass :failed knl-test-fail :fails knl-test-fails})) diff --git a/plans/kernel-on-sx.md b/plans/kernel-on-sx.md index 775f3eb1..6d36c337 100644 --- a/plans/kernel-on-sx.md +++ b/plans/kernel-on-sx.md @@ -57,7 +57,7 @@ The whole interesting thing: there are no special forms hardcoded in the evaluat ### Phase 1 — Parser - [x] S-expression reader with the standard atoms (number, string, symbol, boolean, nil) and lists. -- [ ] Reader macros optional; defer to Phase 6. +- [x] Reader macros optional; defer to Phase 6. - [x] Tests in `lib/kernel/tests/parse.sx`. ### Phase 2 — Core evaluator with first-class environments @@ -148,6 +148,7 @@ The motivation is that SX's host `make-env` family is registered only in HTTP/si ## Progress log +- 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. - 2026-05-11 — Phase 6 hygiene landed (mostly). Two helpers in `runtime.sx`: `$let` — proper hygienic let; values evaluated in caller env, names bound in fresh child env, body in that child env. `$define-in!` — operative that binds a name in a *specified* env, not the dyn-env. The key insight: hygiene-by-default was already the case from Phase 3's static-env extension semantics — $vau/$lambda close over their static env and bind formals + body $define!s in a CHILD of static-env, so caller's env stays untouched unless explicitly threaded via `eval` or `$define-in!`. The 18 tests in `tests/hygiene.sx` prove this property holds in practice: `$define!` inside an operative body doesn't escape to the caller; `$let`-bound names don't leak after the let; parallel let evaluates RHS in outer scope; `$define-in!` populates the target env without polluting the caller's. Full scope-set / frame-stamp hygiene (Shutt's later research-grade work) is documented in the proposed `lib/guest/reflective/hygiene.sx` notes but deferred — would require lifted symbols with provenance markers, a much larger redesign. chisel: shapes-reflective. The default-hygienic-by-static-env-extension property is itself a chisel finding worth recording — every reflective Lisp would benefit from this design choice, and the `lib/guest/reflective/env.sx` candidate API should make it the default semantic. - 2026-05-11 — Phase 5 encapsulations landed. `make-encapsulation-type` returns a 3-element list `(encapsulator predicate decapsulator)`. Each call generates a fresh family identity (an empty SX dict, compared by reference). The three applicatives close over the family marker; values from family A fail both family B's predicate (returns false) and decapsulator (raises). 19 tests in `tests/encap.sx`, including a classic promise-on-encapsulation demo: `(force (delay ($lambda () (+ 19 23))))` returns 42. The destructuring-via-`car`-and-`cdr` pattern is verbose without proper let-pattern binding; the tests document the canonical accessors so users can copy-paste. chisel: nothing (pure Kernel work — no new substrate or lib/guest insights). Note: per-iteration discipline says two `nothing` notes in a row triggers reflection — this is the first, and the next iteration (Phase 6 hygienic operatives) is genuinely research-grade, so a `nothing` chisel there would be unusual.