diff --git a/lib/kernel/runtime.sx b/lib/kernel/runtime.sx index 59e36601..03480b32 100644 --- a/lib/kernel/runtime.sx +++ b/lib/kernel/runtime.sx @@ -572,14 +572,47 @@ (kernel-make-primitive-operative (fn (args dyn-env) (cond - ((not (= (length args) 2)) - (error "$let: expects (bindings body)")) + ((< (length args) 2) + (error "$let: expects (bindings body...)")) ((not (list? (first args))) (error "$let: bindings must be a list")) (:else (let ((local (kernel-extend-env dyn-env))) (knl-bind-let-vals! local (first args) dyn-env) - (kernel-eval (nth args 1) local))))))) + (knl-eval-body (rest args) local))))))) + +;; $let* — sequential let. Each binding sees prior names in scope. +;; Implemented by nesting envs one per binding; the body runs in the +;; innermost env, so later bindings shadow earlier ones if names repeat. +(define knl-let*-step + (fn (bindings env body-forms) + (cond + ((or (nil? bindings) (= (length bindings) 0)) + (knl-eval-body body-forms env)) + (:else + (let ((b (first bindings))) + (cond + ((not (and (list? b) (= (length b) 2))) + (error "$let*: each binding must be (name expr)")) + ((not (string? (first b))) + (error "$let*: binding name must be a symbol")) + (:else + (let ((child (kernel-extend-env env))) + (kernel-env-bind! child + (first b) + (kernel-eval (nth b 1) env)) + (knl-let*-step (rest bindings) child body-forms))))))))) + +(define kernel-let*-operative + (kernel-make-primitive-operative + (fn (args dyn-env) + (cond + ((< (length args) 2) + (error "$let*: expects (bindings body...)")) + ((not (list? (first args))) + (error "$let*: bindings must be a list")) + (:else + (knl-let*-step (first args) dyn-env (rest args))))))) (define kernel-define-in!-operative (kernel-make-primitive-operative @@ -646,5 +679,6 @@ (kernel-env-bind! env "make-encapsulation-type" kernel-make-encap-type-applicative) (kernel-env-bind! env "$let" kernel-let-operative) + (kernel-env-bind! env "$let*" kernel-let*-operative) (kernel-env-bind! env "$define-in!" kernel-define-in!-operative) env))) diff --git a/lib/kernel/tests/hygiene.sx b/lib/kernel/tests/hygiene.sx index 633fc6c5..1a6b6a31 100644 --- a/lib/kernel/tests/hygiene.sx +++ b/lib/kernel/tests/hygiene.sx @@ -191,4 +191,30 @@ (kh-eval-in "x" env)) 1) +;; ── $let* — sequential let ────────────────────────────────────── +(kh-test "let*: empty bindings" + (kh-eval-in "($let* () 42)" (kernel-standard-env)) 42) +(kh-test "let*: single binding" + (kh-eval-in "($let* ((x 5)) (+ x 1))" (kernel-standard-env)) 6) +(kh-test "let*: later sees earlier" + (kh-eval-in "($let* ((x 1) (y (+ x 1)) (z (+ y 1))) z)" + (kernel-standard-env)) 3) +(kh-test "let*: bindings don't leak after" + (let ((env (kernel-standard-env))) + (kh-eval-in "($define! x 1)" env) + (kh-eval-in "($let* ((x 99) (y (+ x 1))) y)" env) + (kh-eval-in "x" env)) 1) +(kh-test "let*: same-name later binding shadows earlier" + (kh-eval-in "($let* ((x 1) (x 2)) x)" (kernel-standard-env)) 2) +(kh-test "let*: multi-expression body" + (kh-eval-in "($let* ((x 5)) ($define! double (+ x x)) double)" + (kernel-standard-env)) 10) +(kh-test "let*: error on malformed binding" + (guard (e (true :raised)) + (kh-eval-in "($let* ((x)) x)" (kernel-standard-env))) + :raised) +(kh-test "let: multi-body" + (kh-eval-in "($let ((x 5)) ($define! tmp (+ x 1)) tmp)" + (kernel-standard-env)) 6) + (define kh-tests-run! (fn () {:total (+ kh-test-pass kh-test-fail) :passed kh-test-pass :failed kh-test-fail :fails kh-test-fails})) diff --git a/plans/kernel-on-sx.md b/plans/kernel-on-sx.md index f8d43307..cffb4885 100644 --- a/plans/kernel-on-sx.md +++ b/plans/kernel-on-sx.md @@ -160,6 +160,7 @@ The motivation is that SX's host `make-env` family is registered only in HTTP/si ## Progress log +- 2026-05-11 — `$let*` sequential let. Each binding evaluated in scope where earlier bindings are visible, so `($let* ((x 1) (y (+ x 1))) y)` returns 2. Implemented by nesting envs one per binding — `knl-let*-step` recursively builds the env chain. `$let` and `$let*` now both accept multi-expression bodies (`knl-eval-body` re-used). 8 new tests in `tests/hygiene.sx`. chisel: nothing (a standard derived form). 260 tests total. - 2026-05-11 — `$and?` / `$or?` short-circuit booleans. Operatives (not applicatives) so untaken arguments are NOT evaluated. Identity values: `$and?` empty = true, `$or?` empty = false. Returns the last evaluated value (Kernel convention — not coerced to bool). 10 new tests including the short-circuit verification (`($and? #f nope)` returns false without evaluating `nope`). chisel: shapes-reflective. Sketched `lib/guest/reflective/short-circuit.sx` API; the protocol is identical across reflective Lisps because short-circuit FORCES operative semantics — an applicative variant would defeat the purpose. 252 tests total. - 2026-05-11 — `$cond` / `$when` / `$unless`. Standard Kernel control flow added: `$cond` walks clauses in order, evaluates first truthy test, runs that clause's body in sequence; `else` is the catch-all symbol; empty cond and no-match cond return nil. `$when` and `$unless` are simple conditional execution. All three preserve hygiene (clauses not taken are NOT evaluated). 12 new tests in `tests/standard.sx`. chisel: nothing. 242 tests total. (Third `nothing` in a row but allowable here — these are textbook Kernel idioms with no novel reflective angle.) - 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.