diff --git a/lib/kernel/eval.sx b/lib/kernel/eval.sx index 6b2dcde9..3fa57c60 100644 --- a/lib/kernel/eval.sx +++ b/lib/kernel/eval.sx @@ -172,9 +172,19 @@ (when (not (= eparam :knl-ignore)) (kernel-env-bind! local eparam dyn-env))) - (kernel-eval (get op :body) local))) + ;; :body is a list of forms — evaluate in sequence, return last. + (knl-eval-body (get op :body) local))) (:else (error "kernel-call-operative: malformed operative"))))) +(define knl-eval-body + (fn (forms env) + (cond + ((= (length forms) 1) (kernel-eval (first forms) env)) + (:else + (begin + (kernel-eval (first forms) env) + (knl-eval-body (rest forms) env)))))) + ;; Phase 3 supports a flat parameter list only — destructuring later. (define kernel-bind-params! diff --git a/lib/kernel/runtime.sx b/lib/kernel/runtime.sx index 82625eef..0ed7521c 100644 --- a/lib/kernel/runtime.sx +++ b/lib/kernel/runtime.sx @@ -40,13 +40,13 @@ (fn (args dyn-env) (cond - ((not (= (length args) 3)) - (error "$vau: expects (formals env-param body)")) + ((< (length args) 3) + (error "$vau: expects (formals env-param body...)")) (:else (let ((formals (first args)) (eparam-raw (nth args 1)) - (body (nth args 2))) + (body-forms (rest (rest args)))) (cond ((not (knl-formals-ok? formals)) (error "$vau: formals must be a list of symbols")) @@ -56,7 +56,7 @@ (kernel-make-user-operative formals (knl-eparam-sentinel eparam-raw) - body + body-forms dyn-env)))))))) (define @@ -70,17 +70,21 @@ (fn (args dyn-env) (cond - ((not (= (length args) 2)) - (error "$lambda: expects (formals body)")) + ((< (length args) 2) + (error "$lambda: expects (formals body...)")) (:else (let - ((formals (first args)) (body (nth args 1))) + ((formals (first args)) (body-forms (rest args))) (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))))))))) + (kernel-make-user-operative + formals + :knl-ignore + body-forms + dyn-env))))))))) (define kernel-lambda-operative diff --git a/lib/kernel/tests/vau.sx b/lib/kernel/tests/vau.sx index a1ac8b6d..b64e7690 100644 --- a/lib/kernel/tests/vau.sx +++ b/lib/kernel/tests/vau.sx @@ -286,4 +286,24 @@ (guard (e (true :raised)) (kv-eval-src "(unwrap 42)" (kv-make-env))) :raised) +;; ── Multi-expression body (implicit $sequence) ────────────────── + +(kv-test "lambda: two body forms — value of last" + (kv-eval-src "(($lambda (n) (+ n 1) (+ n 10)) 5)" (kv-make-env)) 15) + +(kv-test "lambda: three body forms" + (kv-eval-src "(($lambda (n) n (+ n 1) (+ n 2)) 10)" (kv-make-env)) 12) + +(kv-test "vau: two body forms" + (kv-eval-src "(($vau (a b) _ a (list a b)) 7 8)" (kv-make-env)) + (list 7 8)) + +(kv-test "lambda: $define! in early body visible in later body" + (kv-eval-src + "(($lambda (n) ($define! double (+ n n)) double) 6)" + (kv-make-env)) 12) + +(kv-test "lambda: zero-arg multi-body" + (kv-eval-src "(($lambda () 1 2 3))" (kv-make-env)) 3) + (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 6d36c337..7de414f2 100644 --- a/plans/kernel-on-sx.md +++ b/plans/kernel-on-sx.md @@ -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 — 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. - 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.