diff --git a/lib/scheme/eval.sx b/lib/scheme/eval.sx index 7c5c18f3..81d93d6c 100644 --- a/lib/scheme/eval.sx +++ b/lib/scheme/eval.sx @@ -198,6 +198,141 @@ :body body :env env})) +;; ── let / let* — bindings in a fresh child env ─────────────────── + +(define scm-bind-let-vals! + (fn (local bindings dyn-env) + (cond + ((or (nil? bindings) (= (length bindings) 0)) nil) + (: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 + (begin + (scheme-env-bind! local (first b) + (scheme-eval (nth b 1) dyn-env)) + (scm-bind-let-vals! local (rest bindings) dyn-env))))))))) + +(scheme-define-op! "let" + (fn (args env) + (cond + ((< (length args) 2) + (error "let: expects (bindings body...)")) + ((not (list? (first args))) + (error "let: bindings must be a list")) + (:else + (let ((local (scheme-extend-env env))) + (scm-bind-let-vals! local (first args) env) + (scheme-eval-body (rest args) local)))))) + +;; let* — sequential let; each binding sees earlier ones. +(define scm-let*-step + (fn (bindings env body) + (cond + ((or (nil? bindings) (= (length bindings) 0)) + (scheme-eval-body body env)) + (:else + (let ((b (first bindings))) + (cond + ((not (and (list? b) (= (length b) 2))) + (error "let*: each binding must be (name expr)")) + (:else + (let ((child (scheme-extend-env env))) + (scheme-env-bind! child (first b) + (scheme-eval (nth b 1) env)) + (scm-let*-step (rest bindings) child body))))))))) + +(scheme-define-op! "let*" + (fn (args env) + (cond + ((< (length args) 2) + (error "let*: expects (bindings body...)")) + ((not (list? (first args))) + (error "let*: bindings must be a list")) + (:else (scm-let*-step (first args) env (rest args)))))) + +;; ── cond / when / unless ───────────────────────────────────────── + +(define scm-cond-clauses + (fn (clauses env) + (cond + ((or (nil? clauses) (= (length clauses) 0)) nil) + (:else + (let ((clause (first clauses))) + (cond + ((not (list? clause)) + (error "cond: each clause must be a list")) + ((= (length clause) 0) + (error "cond: empty clause")) + ((and (string? (first clause)) (= (first clause) "else")) + (scheme-eval-body (rest clause) env)) + (:else + (let ((test-val (scheme-eval (first clause) env))) + (cond + ((not (= test-val false)) + (cond + ((= (length clause) 1) test-val) + (:else (scheme-eval-body (rest clause) env)))) + (:else (scm-cond-clauses (rest clauses) env))))))))))) + +(scheme-define-op! "cond" + (fn (args env) (scm-cond-clauses args env))) + +(scheme-define-op! "when" + (fn (args env) + (cond + ((< (length args) 1) (error "when: expects (when test body...)")) + (:else + (let ((v (scheme-eval (first args) env))) + (cond + ((= v false) nil) + (:else (scheme-eval-body (rest args) env)))))))) + +(scheme-define-op! "unless" + (fn (args env) + (cond + ((< (length args) 1) + (error "unless: expects (unless test body...)")) + (:else + (let ((v (scheme-eval (first args) env))) + (cond + ((= v false) (scheme-eval-body (rest args) env)) + (:else nil))))))) + +;; ── and / or — short-circuit boolean operators ────────────────── + +(define scm-and-step + (fn (args env) + (cond + ((or (nil? args) (= (length args) 0)) true) + ((= (length args) 1) (scheme-eval (first args) env)) + (:else + (let ((v (scheme-eval (first args) env))) + (cond + ((= v false) false) + (:else (scm-and-step (rest args) env)))))))) + +(scheme-define-op! "and" + (fn (args env) (scm-and-step args env))) + +(define scm-or-step + (fn (args env) + (cond + ((or (nil? args) (= (length args) 0)) false) + ((= (length args) 1) (scheme-eval (first args) env)) + (:else + (let ((v (scheme-eval (first args) env))) + (cond + ((not (= v false)) v) + (:else (scm-or-step (rest args) env)))))))) + +(scheme-define-op! "or" + (fn (args env) (scm-or-step args env))) + ;; ── eval-args helper ───────────────────────────────────────────── (define diff --git a/lib/scheme/tests/syntax.sx b/lib/scheme/tests/syntax.sx index 3c552bcb..2411e0de 100644 --- a/lib/scheme/tests/syntax.sx +++ b/lib/scheme/tests/syntax.sx @@ -234,4 +234,55 @@ (let ((env (scm-syn-env))) (scm-syn-eval-all "(define x 42) 'x" env)) "x") +;; ── let / let* ─────────────────────────────────────────────────── +(scm-syn-test "let: returns body" + (scm-syn-eval "(let ((x 5)) (+ x 1))" (scm-syn-env)) 6) +(scm-syn-test "let: multiple bindings" + (scm-syn-eval "(let ((x 3) (y 4)) (+ x y))" (scm-syn-env)) 7) +(scm-syn-test "let: parallel (RHS sees outer)" + (let ((env (scm-syn-env))) + (scm-syn-eval-all "(define x 1) (let ((x 10) (y x)) y)" env)) 1) +(scm-syn-test "let: bindings don't leak" + (let ((env (scm-syn-env))) + (scm-syn-eval-all "(define x 1) (let ((x 99)) x) x" env)) 1) +(scm-syn-test "let*: sequential" + (scm-syn-eval "(let* ((x 1) (y (+ x 1)) (z (+ y 1))) z)" + (scm-syn-env)) 3) +(scm-syn-test "let*: shadow earlier" + (scm-syn-eval "(let* ((x 1) (x 2)) x)" (scm-syn-env)) 2) + +;; ── cond / when / unless ───────────────────────────────────────── +(scm-syn-test "cond: first match" + (scm-syn-eval "(cond (#f 1) (#t 2) (#t 3))" (scm-syn-env)) 2) +(scm-syn-test "cond: else" + (scm-syn-eval "(cond (#f 1) (else 99))" (scm-syn-env)) 99) +(scm-syn-test "cond: untaken not evaluated" + (scm-syn-eval "(cond (#t 7) (nope ignored))" (scm-syn-env)) 7) +(scm-syn-test "cond: no match returns nil" + (scm-syn-eval "(cond (#f 1) (#f 2))" (scm-syn-env)) nil) +(scm-syn-test "cond: test-only clause" + (scm-syn-eval "(cond (42))" (scm-syn-env)) 42) +(scm-syn-test "when: true" + (scm-syn-eval "(when #t 1 2 3)" (scm-syn-env)) 3) +(scm-syn-test "when: false" + (scm-syn-eval "(when #f nope)" (scm-syn-env)) nil) +(scm-syn-test "unless: false" + (scm-syn-eval "(unless #f 42)" (scm-syn-env)) 42) +(scm-syn-test "unless: true" + (scm-syn-eval "(unless #t nope)" (scm-syn-env)) nil) + +;; ── and / or ───────────────────────────────────────────────────── +(scm-syn-test "and: empty" + (scm-syn-eval "(and)" (scm-syn-env)) true) +(scm-syn-test "and: all truthy returns last" + (scm-syn-eval "(and 1 2 3)" (scm-syn-env)) 3) +(scm-syn-test "and: short-circuit on #f" + (scm-syn-eval "(and 1 #f nope)" (scm-syn-env)) false) +(scm-syn-test "or: empty" + (scm-syn-eval "(or)" (scm-syn-env)) false) +(scm-syn-test "or: first truthy" + (scm-syn-eval "(or #f 42 nope)" (scm-syn-env)) 42) +(scm-syn-test "or: all #f" + (scm-syn-eval "(or #f #f #f)" (scm-syn-env)) false) + (define scm-syn-tests-run! (fn () {:total (+ scm-syn-pass scm-syn-fail) :passed scm-syn-pass :failed scm-syn-fail :fails scm-syn-fails}))