diff --git a/lib/scheme/eval.sx b/lib/scheme/eval.sx index f0df7b48..fdcf01b4 100644 --- a/lib/scheme/eval.sx +++ b/lib/scheme/eval.sx @@ -334,6 +334,76 @@ (scheme-define-op! "or" (fn (args env) (scm-or-step args env))) +;; ── quasiquote (R7RS backquote runtime) ───────────────────────── +;; Walks the template form. At depth 0 (the most common case): +;; (unquote X) → (scheme-eval X env) +;; (unquote-splicing X) → spliced into surrounding list +;; (quasiquote X) → bumps depth (nested) — kept literal in +;; simple R7RS; full depth tracking only +;; when nested quasiquotes appear in practice. +;; +;; Algorithm is identical to kernel's knl-quasi-walk; the shared +;; structure is the second-consumer candidate for +;; lib/guest/reflective/quoting.sx. + +(define scm-quasi-walk + (fn (form 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 (scheme-eval (nth form 1) env)))) + (:else (scm-quasi-walk-list form env))))) + +(define scm-quasi-walk-list + (fn (forms 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 (scheme-eval (nth head 1) env))) + (cond + ((not (list? spliced)) + (error "unquote-splicing: value must be a list")) + (:else + (scm-list-concat + spliced + (scm-quasi-walk-list (rest forms) env)))))) + (:else + (cons (scm-quasi-walk head env) + (scm-quasi-walk-list (rest forms) env))))))))) + +(define scm-list-concat + (fn (xs ys) + (cond + ((or (nil? xs) (= (length xs) 0)) ys) + (:else (cons (first xs) (scm-list-concat (rest xs) ys)))))) + +(scheme-define-op! "quasiquote" + (fn (args env) + (cond + ((not (= (length args) 1)) + (error "quasiquote: expects exactly 1 argument")) + (:else (scm-quasi-walk (first args) env))))) + +;; unquote / unquote-splicing at top level (outside quasiquote) +;; are errors per R7RS. We still bind them as ops so a more useful +;; message fires than "unbound symbol". +(scheme-define-op! "unquote" + (fn (args env) + (error "unquote: only valid inside quasiquote"))) +(scheme-define-op! "unquote-splicing" + (fn (args env) + (error "unquote-splicing: only valid inside quasiquote"))) + ;; ── syntax-rules / define-syntax (Phase 6a — no ellipsis yet) ──── ;; ;; Macros are tagged values: diff --git a/lib/scheme/tests/reflection.sx b/lib/scheme/tests/reflection.sx index 2cb3410c..1603557d 100644 --- a/lib/scheme/tests/reflection.sx +++ b/lib/scheme/tests/reflection.sx @@ -97,4 +97,34 @@ "(define sandbox (null-environment 7))\n (guard (e (else 'unbound))\n (eval '(+ 1 1) sandbox))") "unbound") +;; ── quasiquote / unquote / unquote-splicing ───────────────────── +(scm-ref-test "qq: plain atom" + (scm-ref "`hello") "hello") +(scm-ref-test "qq: plain list" + (scm-ref "`(a b c)") (list "a" "b" "c")) +(scm-ref-test "qq: unquote substitutes value" + (scm-ref-all "(define x 42) `(a ,x b)") + (list "a" 42 "b")) +(scm-ref-test "qq: unquote-splicing splices list" + (scm-ref-all "(define xs '(1 2 3)) `(a ,@xs b)") + (list "a" 1 2 3 "b")) +(scm-ref-test "qq: splice at start" + (scm-ref-all "(define xs '(1 2)) `(,@xs c)") + (list 1 2 "c")) +(scm-ref-test "qq: splice at end" + (scm-ref-all "(define xs '(9 8)) `(a b ,@xs)") + (list "a" "b" 9 8)) +(scm-ref-test "qq: nested list with unquote" + (scm-ref-all "(define x 5) `(a (b ,x) c)") + (list "a" (list "b" 5) "c")) +(scm-ref-test "qq: unquote evaluates expression" + (scm-ref "`(a ,(+ 1 2) b)") + (list "a" 3 "b")) +(scm-ref-test "qq: error on splicing non-list" + (scm-ref-all + "(define x 42) (guard (e (else 'raised)) `(a ,@x b))") + "raised") +(scm-ref-test "qq: bare unquote at top level errors" + (scm-ref "(guard (e (else 'raised)) (unquote 5))") "raised") + (define scm-ref-tests-run! (fn () {:total (+ scm-ref-pass scm-ref-fail) :passed scm-ref-pass :failed scm-ref-fail :fails scm-ref-fails}))