diff --git a/lib/kernel/eval.sx b/lib/kernel/eval.sx new file mode 100644 index 00000000..99a78a88 --- /dev/null +++ b/lib/kernel/eval.sx @@ -0,0 +1,234 @@ +;; lib/kernel/eval.sx — Kernel evaluator. +;; +;; The evaluator is `lookup-and-combine`: there are no hardcoded special +;; forms. Even $if / $define! / $lambda are ordinary operatives bound in +;; the standard environment (Phase 4). This file builds the dispatch +;; machinery and the operative/applicative tagged-value protocol. +;; +;; Tagged values +;; ------------- +;; {:knl-tag :env :bindings DICT :parent PARENT-OR-NIL} +;; A first-class Kernel environment. Bindings is a mutable SX dict +;; keyed by symbol name; parent walks up the lookup chain. +;; +;; {:knl-tag :operative :impl FN} +;; Primitive operative. FN receives (args dyn-env) — args are the +;; UN-evaluated argument expressions, dyn-env is the calling env. +;; +;; {:knl-tag :operative :params P :env-param EP :body B :static-env SE} +;; User-defined operative (built by $vau). Same tag; dispatch in +;; kernel-call-operative forks on which keys are present. +;; +;; {:knl-tag :applicative :underlying OP} +;; An applicative wraps an operative. Calls evaluate args first, +;; then forward to the underlying operative. +;; +;; The env-param of a user operative may be the sentinel :knl-ignore, +;; in which case the dynamic env is not bound. +;; +;; Public API +;; (kernel-eval EXPR ENV) — primary entry +;; (kernel-combine COMBINER ARGS DYN-ENV) +;; (kernel-call-operative OP ARGS DYN-ENV) +;; (kernel-bind-params! ENV PARAMS ARGS) +;; (kernel-make-env) / (kernel-extend-env P) +;; (kernel-env-bind! E N V) / (kernel-env-lookup E N) +;; (kernel-env-has? E N) / (kernel-env? V) +;; (kernel-make-primitive-operative IMPL) +;; (kernel-make-primitive-applicative IMPL) +;; (kernel-make-user-operative PARAMS EPARAM BODY STATIC-ENV) +;; (kernel-wrap OP) / (kernel-unwrap APP) +;; (kernel-operative? V) / (kernel-applicative? V) / (kernel-combiner? V) +;; +;; Consumes: lib/kernel/parser.sx (kernel-string?, kernel-string-value) + +;; ── Environments — first-class, pure-SX (binding dict + parent) ── + +(define kernel-env? (fn (v) (and (dict? v) (= (get v :knl-tag) :env)))) + +(define kernel-make-env (fn () {:parent nil :knl-tag :env :bindings {}})) + +(define kernel-extend-env (fn (parent) {:parent parent :knl-tag :env :bindings {}})) + +(define + kernel-env-bind! + (fn (env name val) (dict-set! (get env :bindings) name val) val)) + +(define + kernel-env-has? + (fn + (env name) + (cond + ((nil? env) false) + ((not (kernel-env? env)) false) + ((dict-has? (get env :bindings) name) true) + (:else (kernel-env-has? (get env :parent) name))))) + +(define + kernel-env-lookup + (fn + (env name) + (cond + ((nil? env) (error (str "kernel-eval: unbound symbol: " name))) + ((not (kernel-env? env)) + (error (str "kernel-eval: corrupt env: " env))) + ((dict-has? (get env :bindings) name) (get (get env :bindings) name)) + (:else (kernel-env-lookup (get env :parent) name))))) + +;; ── Tagged-value constructors and predicates ───────────────────── + +(define kernel-make-primitive-operative (fn (impl) {:impl impl :knl-tag :operative})) + +(define + kernel-make-user-operative + (fn (params eparam body static-env) {:knl-tag :operative :static-env static-env :params params :body body :env-param eparam})) + +(define + kernel-operative? + (fn (v) (and (dict? v) (= (get v :knl-tag) :operative)))) + +(define + kernel-applicative? + (fn (v) (and (dict? v) (= (get v :knl-tag) :applicative)))) + +(define + kernel-combiner? + (fn (v) (or (kernel-operative? v) (kernel-applicative? v)))) + +(define + kernel-wrap + (fn + (op) + (cond + ((kernel-operative? op) {:knl-tag :applicative :underlying op}) + (:else (error "kernel-wrap: argument must be an operative"))))) + +(define + kernel-unwrap + (fn + (app) + (cond + ((kernel-applicative? app) (get app :underlying)) + (:else (error "kernel-unwrap: argument must be an applicative"))))) + +(define + kernel-make-primitive-applicative + (fn + (impl) + (kernel-wrap + (kernel-make-primitive-operative (fn (args dyn-env) (impl args)))))) + +;; As above, but IMPL receives (args dyn-env). Used by combinators that +;; re-enter the evaluator (map, filter, reduce, apply, eval, ...). +(define kernel-make-primitive-applicative-with-env + (fn (impl) + (kernel-wrap + (kernel-make-primitive-operative + (fn (args dyn-env) (impl args dyn-env)))))) + +;; ── The evaluator ──────────────────────────────────────────────── + +(define + kernel-eval + (fn + (expr env) + (cond + ((number? expr) expr) + ((boolean? expr) expr) + ((nil? expr) expr) + ((kernel-string? expr) (kernel-string-value expr)) + ((string? expr) (kernel-env-lookup env expr)) + ((list? expr) + (cond + ((= (length expr) 0) expr) + (:else + (let + ((combiner (kernel-eval (first expr) env)) + (args (rest expr))) + (kernel-combine combiner args env))))) + (:else (error (str "kernel-eval: unknown form: " expr)))))) + +(define + kernel-combine + (fn + (combiner args dyn-env) + (cond + ((kernel-operative? combiner) + (kernel-call-operative combiner args dyn-env)) + ((kernel-applicative? combiner) + (kernel-combine + (get combiner :underlying) + (kernel-eval-args args dyn-env) + dyn-env)) + (:else (error (str "kernel-eval: not a combiner: " combiner)))))) + +;; Operatives may be primitive (:impl is a host fn) or user-defined +;; (carry :params / :env-param / :body / :static-env). The dispatch +;; fork is here so kernel-combine stays small. +(define + kernel-call-operative + (fn + (op args dyn-env) + (cond + ((dict-has? op :impl) ((get op :impl) args dyn-env)) + ((dict-has? op :body) + (let + ((local (kernel-extend-env (get op :static-env)))) + (kernel-bind-params! local (get op :params) args) + (let + ((eparam (get op :env-param))) + (when + (not (= eparam :knl-ignore)) + (kernel-env-bind! local eparam dyn-env))) + ;; :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! + (fn + (env params args) + (cond + ((or (nil? params) (= (length params) 0)) + (cond + ((or (nil? args) (= (length args) 0)) nil) + (:else (error "kernel-call: too many arguments")))) + ((or (nil? args) (= (length args) 0)) + (error "kernel-call: too few arguments")) + (:else + (begin + (kernel-env-bind! env (first params) (first args)) + (kernel-bind-params! env (rest params) (rest args))))))) + +(define + kernel-eval-args + (fn + (args env) + (cond + ((or (nil? args) (= (length args) 0)) (list)) + (:else + (cons + (kernel-eval (first args) env) + (kernel-eval-args (rest args) env)))))) + +(define + kernel-eval-program + (fn + (forms env) + (cond + ((or (nil? forms) (= (length forms) 0)) nil) + ((= (length forms) 1) (kernel-eval (first forms) env)) + (:else + (begin + (kernel-eval (first forms) env) + (kernel-eval-program (rest forms) env)))))) diff --git a/lib/kernel/parser.sx b/lib/kernel/parser.sx new file mode 100644 index 00000000..8bd7a4d6 --- /dev/null +++ b/lib/kernel/parser.sx @@ -0,0 +1,253 @@ +;; lib/kernel/parser.sx — Kernel s-expression reader. +;; +;; Reads R-1RK lexical syntax: numbers, strings, symbols, booleans (#t/#f), +;; the empty list (), nested lists, and ; line comments. Reader macros +;; (' ` , ,@) deferred to Phase 6 per the plan. +;; +;; Public AST shape: +;; number → SX number +;; #t / #f → SX true / false +;; () → SX empty list (Kernel's nil — the empty list) +;; "..." → {:knl-string "..."} wrapped to distinguish from symbols +;; foo → "foo" bare SX string is a Kernel symbol +;; (a b c) → SX list of forms +;; +;; Public API: +;; (kernel-parse SRC) — first form; errors on extra trailing input +;; (kernel-parse-all SRC) — all top-level forms, as SX list +;; (kernel-string? V) — recognise wrapped string literal +;; (kernel-string-value V) — extract the underlying string +;; +;; Consumes: lib/guest/lex.sx (lex-digit?, lex-whitespace?) + +(define kernel-string-make (fn (s) {:knl-string s})) +(define + kernel-string? + (fn (v) (and (dict? v) (string? (get v :knl-string))))) +(define kernel-string-value (fn (v) (get v :knl-string))) + +;; Atom delimiters: characters that end a symbol or numeric token. +(define + knl-delim? + (fn + (c) + (or + (nil? c) + (lex-whitespace? c) + (= c "(") + (= c ")") + (= c "\"") + (= c ";") + (= c "'") + (= c "`") + (= c ",")))) + +;; Numeric grammar: [+-]? (digit+ ('.' digit+)? | '.' digit+) ([eE][+-]?digit+)? +(define + knl-numeric? + (fn + (s) + (let + ((n (string-length s))) + (cond + ((= n 0) false) + (:else + (let + ((c0 (substring s 0 1))) + (let + ((start (if (or (= c0 "+") (= c0 "-")) 1 0))) + (knl-num-body? s start n)))))))) + +(define + knl-num-body? + (fn + (s start n) + (cond + ((>= start n) false) + ((= (substring s start (+ start 1)) ".") + (knl-num-need-digits? s (+ start 1) n false)) + ((lex-digit? (substring s start (+ start 1))) + (knl-num-int-tail? s (+ start 1) n)) + (:else false)))) + +(define + knl-num-int-tail? + (fn + (s i n) + (cond + ((>= i n) true) + ((lex-digit? (substring s i (+ i 1))) + (knl-num-int-tail? s (+ i 1) n)) + ((= (substring s i (+ i 1)) ".") + (knl-num-need-digits? s (+ i 1) n true)) + ((or (= (substring s i (+ i 1)) "e") (= (substring s i (+ i 1)) "E")) + (knl-num-exp-sign? s (+ i 1) n)) + (:else false)))) + +(define + knl-num-need-digits? + (fn + (s i n had-int) + (cond + ((>= i n) had-int) + ((lex-digit? (substring s i (+ i 1))) + (knl-num-frac-tail? s (+ i 1) n)) + (:else false)))) + +(define + knl-num-frac-tail? + (fn + (s i n) + (cond + ((>= i n) true) + ((lex-digit? (substring s i (+ i 1))) + (knl-num-frac-tail? s (+ i 1) n)) + ((or (= (substring s i (+ i 1)) "e") (= (substring s i (+ i 1)) "E")) + (knl-num-exp-sign? s (+ i 1) n)) + (:else false)))) + +(define + knl-num-exp-sign? + (fn + (s i n) + (cond + ((>= i n) false) + ((or (= (substring s i (+ i 1)) "+") (= (substring s i (+ i 1)) "-")) + (knl-num-exp-digits? s (+ i 1) n false)) + (:else (knl-num-exp-digits? s i n false))))) + +(define + knl-num-exp-digits? + (fn + (s i n had) + (cond + ((>= i n) had) + ((lex-digit? (substring s i (+ i 1))) + (knl-num-exp-digits? s (+ i 1) n true)) + (:else false)))) + +;; Reader: a closure over (src, pos). Exposes :read-form and :read-all. +(define + knl-make-reader + (fn + (src) + (let + ((pos 0) (n (string-length src))) + (define + at + (fn () (if (< pos n) (substring src pos (+ pos 1)) nil))) + (define adv (fn () (set! pos (+ pos 1)))) + (define + skip-line + (fn () (when (and (at) (not (= (at) "\n"))) (adv) (skip-line)))) + (define + skip-ws + (fn + () + (cond + ((nil? (at)) nil) + ((lex-whitespace? (at)) (do (adv) (skip-ws))) + ((= (at) ";") (do (adv) (skip-line) (skip-ws))) + (:else nil)))) + (define + read-string-body + (fn + (acc) + (cond + ((nil? (at)) (error "kernel-parse: unterminated string")) + ((= (at) "\"") (do (adv) acc)) + ((= (at) "\\") + (do + (adv) + (let + ((c (at))) + (when (nil? c) (error "kernel-parse: trailing backslash")) + (adv) + (read-string-body + (str + acc + (cond + ((= c "n") "\n") + ((= c "t") "\t") + ((= c "r") "\r") + ((= c "\"") "\"") + ((= c "\\") "\\") + (:else c))))))) + (:else + (let ((c (at))) (adv) (read-string-body (str acc c))))))) + (define + read-atom-body + (fn + (acc) + (cond + ((knl-delim? (at)) acc) + (:else (let ((c (at))) (adv) (read-atom-body (str acc c))))))) + (define + classify-atom + (fn + (s) + (cond + ((= s "#t") true) + ((= s "#f") false) + ((knl-numeric? s) (string->number s)) + (:else s)))) + (define + read-form + (fn + () + (skip-ws) + (cond + ((nil? (at)) :knl-eof) + ((= (at) ")") (error "kernel-parse: unexpected ')'")) + ((= (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 + (fn + (acc) + (skip-ws) + (cond + ((nil? (at)) (error "kernel-parse: unterminated list")) + ((= (at) ")") (do (adv) acc)) + (:else (read-list (append acc (list (read-form)))))))) + (define + read-all + (fn + (acc) + (skip-ws) + (if (nil? (at)) acc (read-all (append acc (list (read-form))))))) + {:read-form read-form :read-all read-all}))) + +(define + kernel-parse-all + (fn (src) ((get (knl-make-reader src) :read-all) (list)))) + +(define + kernel-parse + (fn + (src) + (let + ((r (knl-make-reader src))) + (let + ((form ((get r :read-form)))) + (cond + ((= form :knl-eof) (error "kernel-parse: empty input")) + (:else + (let + ((next ((get r :read-form)))) + (if + (= next :knl-eof) + form + (error "kernel-parse: trailing input after first form"))))))))) diff --git a/lib/kernel/runtime.sx b/lib/kernel/runtime.sx new file mode 100644 index 00000000..77bff089 --- /dev/null +++ b/lib/kernel/runtime.sx @@ -0,0 +1,911 @@ +;; lib/kernel/runtime.sx — the operative–applicative substrate and the +;; standard Kernel environment. +;; +;; Phase 3 supplied four user-visible combiners ($vau, $lambda, wrap, +;; unwrap). Phase 4 fills out the rest of the R-1RK core: $if, $define!, +;; $sequence, eval, make-environment, get-current-environment, plus +;; arithmetic, equality, list/pair, and boolean primitives — enough to +;; write factorial. +;; +;; The standard env is built by EXTENDING the base env, not replacing +;; it. So `kernel-standard-env` includes everything from `kernel-base-env`. +;; +;; Public API +;; (kernel-base-env) — Phase 3 combiners +;; (kernel-standard-env) — Phase 4 standard environment + +(define + knl-eparam-sentinel + (fn + (sym) + (cond + ((= sym "_") :knl-ignore) + ((= sym "#ignore") :knl-ignore) + (:else sym)))) + +(define + knl-formals-ok? + (fn + (formals) + (cond + ((not (list? formals)) false) + ((= (length formals) 0) true) + ((string? (first formals)) (knl-formals-ok? (rest formals))) + (:else false)))) + +;; ── $vau ───────────────────────────────────────────────────────── + +(define + kernel-vau-impl + (fn + (args dyn-env) + (cond + ((< (length args) 3) + (error "$vau: expects (formals env-param body...)")) + (:else + (let + ((formals (first args)) + (eparam-raw (nth args 1)) + (body-forms (rest (rest args)))) + (cond + ((not (knl-formals-ok? formals)) + (error "$vau: formals must be a list of symbols")) + ((not (string? eparam-raw)) + (error "$vau: env-param must be a symbol")) + (:else + (kernel-make-user-operative + formals + (knl-eparam-sentinel eparam-raw) + body-forms + dyn-env)))))))) + +(define + kernel-vau-operative + (kernel-make-primitive-operative kernel-vau-impl)) + +;; ── $lambda ────────────────────────────────────────────────────── + +(define + kernel-lambda-impl + (fn + (args dyn-env) + (cond + ((< (length args) 2) + (error "$lambda: expects (formals body...)")) + (:else + (let + ((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-forms + dyn-env))))))))) + +(define + kernel-lambda-operative + (kernel-make-primitive-operative kernel-lambda-impl)) + +;; ── wrap / unwrap / predicates ─────────────────────────────────── + +(define + kernel-wrap-applicative + (kernel-make-primitive-applicative + (fn + (args) + (cond + ((not (= (length args) 1)) + (error "wrap: expects exactly 1 argument")) + (:else (kernel-wrap (first args))))))) + +(define + kernel-unwrap-applicative + (kernel-make-primitive-applicative + (fn + (args) + (cond + ((not (= (length args) 1)) + (error "unwrap: expects exactly 1 argument")) + (:else (kernel-unwrap (first args))))))) + +(define + kernel-operative?-applicative + (kernel-make-primitive-applicative + (fn (args) (kernel-operative? (first args))))) + +(define + kernel-applicative?-applicative + (kernel-make-primitive-applicative + (fn (args) (kernel-applicative? (first args))))) + +(define + kernel-base-env + (fn + () + (let + ((env (kernel-make-env))) + (kernel-env-bind! env "$vau" kernel-vau-operative) + (kernel-env-bind! env "$lambda" kernel-lambda-operative) + (kernel-env-bind! env "wrap" kernel-wrap-applicative) + (kernel-env-bind! env "unwrap" kernel-unwrap-applicative) + (kernel-env-bind! env "operative?" kernel-operative?-applicative) + (kernel-env-bind! env "applicative?" kernel-applicative?-applicative) + env))) + +;; ── $if / $define! / $sequence ─────────────────────────────────── + +(define + kernel-if-operative + (kernel-make-primitive-operative + (fn + (args dyn-env) + (cond + ((not (= (length args) 3)) + (error "$if: expects (condition then-expr else-expr)")) + (:else + (let + ((c (kernel-eval (first args) dyn-env))) + (if + c + (kernel-eval (nth args 1) dyn-env) + (kernel-eval (nth args 2) dyn-env)))))))) + +(define + kernel-define!-operative + (kernel-make-primitive-operative + (fn + (args dyn-env) + (cond + ((not (= (length args) 2)) + (error "$define!: expects (name expr)")) + ((not (string? (first args))) + (error "$define!: name must be a symbol")) + (:else + (let + ((v (kernel-eval (nth args 1) dyn-env))) + (kernel-env-bind! dyn-env (first args) v) + v)))))) + +(define + kernel-sequence-operative + (kernel-make-primitive-operative + (fn + (args dyn-env) + (cond + ((or (nil? args) (= (length args) 0)) nil) + ((= (length args) 1) (kernel-eval (first args) dyn-env)) + (:else + (begin + (kernel-eval (first args) dyn-env) + ((get kernel-sequence-operative :impl) (rest args) dyn-env))))))) + +;; ── eval / make-environment / get-current-environment ─────────── + +(define + kernel-quote-operative + (kernel-make-primitive-operative + (fn + (args dyn-env) + (cond + ((not (= (length args) 1)) (error "$quote: expects 1 argument")) + (:else (first args)))))) + +;; Quasiquote: walks the template, evaluating `$unquote` forms in the +;; dynamic env and splicing `$unquote-splicing` list results. +(define knl-quasi-walk + (fn (form dyn-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 (kernel-eval (nth form 1) dyn-env)))) + (:else (knl-quasi-walk-list form dyn-env))))) + +(define knl-quasi-walk-list + (fn (forms dyn-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 (kernel-eval (nth head 1) dyn-env))) + (cond + ((not (list? spliced)) + (error "$unquote-splicing: value must be a list")) + (:else + (knl-list-concat + spliced + (knl-quasi-walk-list (rest forms) dyn-env)))))) + (:else + (cons (knl-quasi-walk head dyn-env) + (knl-quasi-walk-list (rest forms) dyn-env))))))))) + +(define knl-list-concat + (fn (xs ys) + (cond + ((or (nil? xs) (= (length xs) 0)) ys) + (:else (cons (first xs) (knl-list-concat (rest xs) ys)))))) + +;; $cond — multi-clause branch. +;; ($cond (TEST1 EXPR1 ...) (TEST2 EXPR2 ...) ...) +;; Evaluates each TEST in order; first truthy one runs its EXPRs (in +;; sequence) and returns the last; if no TEST is truthy, returns nil. +;; A clause with TEST = `else` always matches (sugar for $if's default). +(define knl-cond-impl + (fn (clauses dyn-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")) + (knl-cond-eval-body (rest clause) dyn-env)) + (:else + (let ((test-val (kernel-eval (first clause) dyn-env))) + (cond + (test-val (knl-cond-eval-body (rest clause) dyn-env)) + (:else (knl-cond-impl (rest clauses) dyn-env))))))))))) + +(define knl-cond-eval-body + (fn (body dyn-env) + (cond + ((or (nil? body) (= (length body) 0)) nil) + ((= (length body) 1) (kernel-eval (first body) dyn-env)) + (:else + (begin + (kernel-eval (first body) dyn-env) + (knl-cond-eval-body (rest body) dyn-env)))))) + +(define kernel-cond-operative + (kernel-make-primitive-operative + (fn (args dyn-env) (knl-cond-impl args dyn-env)))) + +;; $when COND BODY... — evaluate body iff COND is truthy; else nil. +(define kernel-when-operative + (kernel-make-primitive-operative + (fn (args dyn-env) + (cond + ((< (length args) 1) + (error "$when: expects (cond body...)")) + (:else + (let ((c (kernel-eval (first args) dyn-env))) + (cond + (c (knl-cond-eval-body (rest args) dyn-env)) + (:else nil)))))))) + +;; $and? — short-circuit AND. Operative (not applicative) so untaken +;; clauses are NOT evaluated. Empty $and? returns true (the identity). +(define knl-and?-impl + (fn (args dyn-env) + (cond + ((or (nil? args) (= (length args) 0)) true) + ((= (length args) 1) (kernel-eval (first args) dyn-env)) + (:else + (let ((v (kernel-eval (first args) dyn-env))) + (cond + (v (knl-and?-impl (rest args) dyn-env)) + (:else v))))))) + +(define kernel-and?-operative + (kernel-make-primitive-operative knl-and?-impl)) + +;; $or? — short-circuit OR. Operative; untaken clauses NOT evaluated. +;; Empty $or? returns false (the identity). +(define knl-or?-impl + (fn (args dyn-env) + (cond + ((or (nil? args) (= (length args) 0)) false) + ((= (length args) 1) (kernel-eval (first args) dyn-env)) + (:else + (let ((v (kernel-eval (first args) dyn-env))) + (cond + (v v) + (:else (knl-or?-impl (rest args) dyn-env)))))))) + +(define kernel-or?-operative + (kernel-make-primitive-operative knl-or?-impl)) + +;; $unless COND BODY... — evaluate body iff COND is falsy; else nil. +(define kernel-unless-operative + (kernel-make-primitive-operative + (fn (args dyn-env) + (cond + ((< (length args) 1) + (error "$unless: expects (cond body...)")) + (:else + (let ((c (kernel-eval (first args) dyn-env))) + (cond + (c nil) + (:else (knl-cond-eval-body (rest args) dyn-env))))))))) + +(define kernel-quasiquote-operative + (kernel-make-primitive-operative + (fn (args dyn-env) + (cond + ((not (= (length args) 1)) + (error "$quasiquote: expects exactly 1 argument")) + (:else (knl-quasi-walk (first args) dyn-env)))))) + +(define + kernel-eval-applicative + (kernel-make-primitive-applicative + (fn + (args) + (cond + ((not (= (length args) 2)) + (error "eval: expects (expr env)")) + ((not (kernel-env? (nth args 1))) + (error "eval: second arg must be a kernel env")) + (:else (kernel-eval (first args) (nth args 1))))))) + +(define + kernel-make-environment-applicative + (kernel-make-primitive-applicative + (fn + (args) + (cond + ((= (length args) 0) (kernel-make-env)) + ((= (length args) 1) + (cond + ((not (kernel-env? (first args))) + (error "make-environment: parent must be a kernel env")) + (:else (kernel-extend-env (first args))))) + (:else (error "make-environment: 0 or 1 argument")))))) + +;; ── arithmetic and comparison (binary; trivial to extend later) ─ + +(define + kernel-get-current-env-operative + (kernel-make-primitive-operative + (fn + (args dyn-env) + (cond + ((not (= (length args) 0)) + (error "get-current-environment: expects 0 arguments")) + (:else dyn-env))))) + +(define + knl-bin-app + (fn + (name f) + (kernel-make-primitive-applicative + (fn + (args) + (cond + ((not (= (length args) 2)) + (error (str name ": expects 2 arguments"))) + (:else (f (first args) (nth args 1)))))))) + +;; Variadic left-fold helper. ZERO-RES is the identity (`(+)` → 0); +;; ONE-FN handles single-arg case (`(- x)` negates; `(+ x)` returns x). +(define knl-fold-step + (fn (f acc rest-args) + (cond + ((or (nil? rest-args) (= (length rest-args) 0)) acc) + (:else + (knl-fold-step f (f acc (first rest-args)) (rest rest-args)))))) + +(define knl-fold-app + (fn (name f zero-res one-fn) + (kernel-make-primitive-applicative + (fn (args) + (cond + ((= (length args) 0) zero-res) + ((= (length args) 1) (one-fn (first args))) + (:else (knl-fold-step f (first args) (rest args)))))))) + +;; Variadic n-ary chained comparison: `(< 1 2 3)` ≡ `(< 1 2)` AND `(< 2 3)`. +(define knl-chain-step + (fn (cmp prev rest-args) + (cond + ((or (nil? rest-args) (= (length rest-args) 0)) true) + (:else + (let ((next (first rest-args))) + (cond + ((cmp prev next) + (knl-chain-step cmp next (rest rest-args))) + (:else false))))))) + +(define knl-chain-cmp + (fn (name cmp) + (kernel-make-primitive-applicative + (fn (args) + (cond + ((< (length args) 2) + (error (str name ": expects at least 2 arguments"))) + (:else (knl-chain-step cmp (first args) (rest args)))))))) + +;; ── list / pair primitives ────────────────────────────────────── + +(define + knl-unary-app + (fn + (name f) + (kernel-make-primitive-applicative + (fn + (args) + (cond + ((not (= (length args) 1)) + (error (str name ": expects 1 argument"))) + (:else (f (first args)))))))) + +(define kernel-cons-applicative (knl-bin-app "cons" (fn (a b) (cons a b)))) + +(define + kernel-car-applicative + (knl-unary-app + "car" + (fn + (xs) + (cond + ((or (nil? xs) (and (list? xs) (= (length xs) 0))) + (error "car: empty list")) + (:else (first xs)))))) + +(define + kernel-cdr-applicative + (knl-unary-app + "cdr" + (fn + (xs) + (cond + ((or (nil? xs) (and (list? xs) (= (length xs) 0))) + (error "cdr: empty list")) + (:else (rest xs)))))) + +(define + kernel-list-applicative + (kernel-make-primitive-applicative (fn (args) args))) + +(define + kernel-length-applicative + (knl-unary-app "length" (fn (xs) (length xs)))) + +(define + kernel-null?-applicative + (knl-unary-app + "null?" + (fn (v) (or (nil? v) (and (list? v) (= (length v) 0)))))) + +;; ── boolean / equality ────────────────────────────────────────── + +(define + kernel-pair?-applicative + (knl-unary-app + "pair?" + (fn (v) (and (list? v) (> (length v) 0))))) + +(define knl-append-step + (fn (xs ys) + (cond + ((or (nil? xs) (= (length xs) 0)) ys) + (:else (cons (first xs) (knl-append-step (rest xs) ys)))))) + +(define knl-all-lists? + (fn (xs) + (cond + ((or (nil? xs) (= (length xs) 0)) true) + ((list? (first xs)) (knl-all-lists? (rest xs))) + (:else false)))) + +(define knl-append-all + (fn (lists) + (cond + ((or (nil? lists) (= (length lists) 0)) (list)) + ((= (length lists) 1) (first lists)) + (:else + (knl-append-step (first lists) + (knl-append-all (rest lists))))))) + +(define kernel-append-applicative + (kernel-make-primitive-applicative + (fn (args) + (cond + ((knl-all-lists? args) (knl-append-all args)) + (:else (error "append: all arguments must be lists")))))) + +(define knl-reverse-step + (fn (xs acc) + (cond + ((or (nil? xs) (= (length xs) 0)) acc) + (:else (knl-reverse-step (rest xs) (cons (first xs) acc)))))) + +(define kernel-reverse-applicative + (knl-unary-app "reverse" + (fn (xs) + (cond + ((not (list? xs)) (error "reverse: argument must be a list")) + (:else (knl-reverse-step xs (list))))))) + +(define kernel-not-applicative (knl-unary-app "not" (fn (v) (not v)))) + +;; Type predicates (Kernel-visible). Note `string?` covers BOTH symbols +;; and string-literals in our representation (symbols are bare SX +;; strings); a `kernel-string?` applicative distinguishes the two if +;; needed. +(define kernel-number?-applicative + (knl-unary-app "number?" (fn (v) (number? v)))) +(define kernel-string?-applicative + (knl-unary-app "string?" (fn (v) (string? v)))) +(define kernel-list?-applicative + (knl-unary-app "list?" (fn (v) (list? v)))) +(define kernel-boolean?-applicative + (knl-unary-app "boolean?" (fn (v) (boolean? v)))) +(define kernel-symbol?-applicative + (knl-unary-app "symbol?" (fn (v) (string? v)))) + +(define kernel-eq?-applicative (knl-bin-app "eq?" (fn (a b) (= a b)))) + +;; ── the standard environment ──────────────────────────────────── + +(define + kernel-equal?-applicative + (knl-bin-app "equal?" (fn (a b) (= a b)))) + +;; ── List combinators: map / filter / reduce ───────────────────── +;; These re-enter the evaluator on each element, so they use the +;; with-env applicative constructor. + +;; When the combiner is an applicative, we MUST unwrap before calling +;; — otherwise kernel-combine will re-evaluate the already-evaluated +;; element values (and crash if an element is itself a list). +(define knl-apply-op + (fn (combiner) + (cond + ((kernel-applicative? combiner) (kernel-unwrap combiner)) + (:else combiner)))) + +(define knl-map-step + (fn (fn-val xs dyn-env) + (let ((op (knl-apply-op fn-val))) + (knl-map-walk op xs dyn-env)))) + +(define knl-map-walk + (fn (op xs dyn-env) + (cond + ((or (nil? xs) (= (length xs) 0)) (list)) + (:else + (cons (kernel-combine op (list (first xs)) dyn-env) + (knl-map-walk op (rest xs) dyn-env)))))) + +(define kernel-map-applicative + (kernel-make-primitive-applicative-with-env + (fn (args dyn-env) + (cond + ((not (= (length args) 2)) + (error "map: expects (fn list)")) + ((not (kernel-combiner? (first args))) + (error "map: first arg must be a combiner")) + ((not (list? (nth args 1))) + (error "map: second arg must be a list")) + (:else (knl-map-step (first args) (nth args 1) dyn-env)))))) + +(define knl-filter-step + (fn (pred xs dyn-env) + (knl-filter-walk (knl-apply-op pred) xs dyn-env))) + +(define knl-filter-walk + (fn (op xs dyn-env) + (cond + ((or (nil? xs) (= (length xs) 0)) (list)) + (:else + (let ((keep? (kernel-combine op (list (first xs)) dyn-env))) + (cond + (keep? + (cons (first xs) (knl-filter-walk op (rest xs) dyn-env))) + (:else (knl-filter-walk op (rest xs) dyn-env)))))))) + +(define kernel-filter-applicative + (kernel-make-primitive-applicative-with-env + (fn (args dyn-env) + (cond + ((not (= (length args) 2)) + (error "filter: expects (pred list)")) + ((not (kernel-combiner? (first args))) + (error "filter: first arg must be a combiner")) + ((not (list? (nth args 1))) + (error "filter: second arg must be a list")) + (:else (knl-filter-step (first args) (nth args 1) dyn-env)))))) + +(define knl-reduce-step + (fn (fn-val xs acc dyn-env) + (knl-reduce-walk (knl-apply-op fn-val) xs acc dyn-env))) + +(define knl-reduce-walk + (fn (op xs acc dyn-env) + (cond + ((or (nil? xs) (= (length xs) 0)) acc) + (:else + (knl-reduce-walk + op + (rest xs) + (kernel-combine op (list acc (first xs)) dyn-env) + dyn-env))))) + +;; (apply COMBINER ARGS-LIST) — call COMBINER with the elements of +;; ARGS-LIST as arguments. The Kernel canonical use: turn a constructed +;; list of values into a function call. We skip the applicative's +;; auto-eval step (via unwrap) because ARGS-LIST is already values, not +;; expressions; for a bare operative, we pass through directly. +(define kernel-apply-applicative + (kernel-make-primitive-applicative-with-env + (fn (args dyn-env) + (cond + ((not (= (length args) 2)) + (error "apply: expects (combiner args-list)")) + ((not (kernel-combiner? (first args))) + (error "apply: first arg must be a combiner")) + ((not (list? (nth args 1))) + (error "apply: second arg must be a list")) + (:else + (let ((op (cond + ((kernel-applicative? (first args)) + (kernel-unwrap (first args))) + (:else (first args))))) + (kernel-combine op (nth args 1) dyn-env))))))) + +(define kernel-reduce-applicative + (kernel-make-primitive-applicative-with-env + (fn (args dyn-env) + (cond + ((not (= (length args) 3)) + (error "reduce: expects (fn init list)")) + ((not (kernel-combiner? (first args))) + (error "reduce: first arg must be a combiner")) + ((not (list? (nth args 2))) + (error "reduce: third arg must be a list")) + (:else + (knl-reduce-step (first args) (nth args 2) + (nth args 1) dyn-env)))))) + +;; ── Encapsulations: Kernel's opaque-type idiom ────────────────── +;; +;; (make-encapsulation-type) → (encapsulator predicate decapsulator) +;; +;; Each call returns three applicatives over a fresh family identity. +;; - (encapsulator V) → an opaque wrapper around V. +;; - (predicate V) → true iff V was wrapped by THIS family. +;; - (decapsulator W) → the inner value; errors on wrong family. +;; +;; Family identity is a fresh empty dict; SX compares dicts by reference, +;; so two `(make-encapsulation-type)` calls return distinct families. +;; +;; Pattern usage (Phase 5 lacks destructuring, so accessors are explicit): +;; ($define! triple (make-encapsulation-type)) +;; ($define! wrap-promise (car triple)) +;; ($define! promise? (car (cdr triple))) +;; ($define! unwrap-promise (car (cdr (cdr triple)))) + +(define kernel-make-encap-type-impl + (fn (args) + (cond + ((not (= (length args) 0)) + (error "make-encapsulation-type: expects 0 arguments")) + (:else + (let ((family {})) + (let ((encap + (kernel-make-primitive-applicative + (fn (vargs) + (cond + ((not (= (length vargs) 1)) + (error "encapsulator: expects 1 argument")) + (:else + {:knl-tag :encap + :family family + :value (first vargs)}))))) + (pred + (kernel-make-primitive-applicative + (fn (vargs) + (cond + ((not (= (length vargs) 1)) + (error "predicate: expects 1 argument")) + (:else + (let ((v (first vargs))) + (and (dict? v) + (= (get v :knl-tag) :encap) + (= (get v :family) family)))))))) + (decap + (kernel-make-primitive-applicative + (fn (vargs) + (cond + ((not (= (length vargs) 1)) + (error "decapsulator: expects 1 argument")) + (:else + (let ((v (first vargs))) + (cond + ((not (and (dict? v) + (= (get v :knl-tag) :encap))) + (error "decapsulator: not an encapsulation")) + ((not (= (get v :family) family)) + (error "decapsulator: wrong family")) + (:else (get v :value)))))))))) + (list encap pred decap))))))) + +(define kernel-make-encap-type-applicative + (kernel-make-primitive-applicative kernel-make-encap-type-impl)) + +;; ── Hygiene: $let, $define-in!, make-environment ──────────────── +;; +;; Kernel-on-SX is hygienic *by default* because user-defined operatives +;; (Phase 3) bind their formals + any $define! in a CHILD env extending +;; the operative's static-env, never the dyn-env. The caller's env is +;; only mutated when code explicitly says so (e.g. `(eval expr env-arg)`). +;; +;; Phase 6 adds two helpers that make the property easy to lean on: +;; +;; ($let ((NAME EXPR) ...) BODY) +;; Evaluates each EXPR in the calling env, binds NAME in a fresh +;; child env, evaluates BODY in that child env. NAMES don't leak. +;; +;; ($define-in! ENV NAME EXPR) +;; Binds NAME=value-of-EXPR in the *specified* env, not the dyn-env. +;; Useful for operatives that need to mutate a sandbox env without +;; touching their caller's env. +;; +;; Shutt's full scope-set / frame-stamp hygiene (lifted symbols carrying +;; provenance markers so introduced bindings can shadow without +;; capturing) is research-grade and not implemented here. Notes for +;; `lib/guest/reflective/hygiene.sx` candidate API below the std env. + +(define knl-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 + (kernel-env-bind! local + (first b) + (kernel-eval (nth b 1) dyn-env)) + (knl-bind-let-vals! local (rest bindings) dyn-env))))))))) + +(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 + (let ((local (kernel-extend-env dyn-env))) + (knl-bind-let-vals! local (first args) dyn-env) + (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 + (fn (args dyn-env) + (cond + ((not (= (length args) 3)) + (error "$define-in!: expects (env-expr name expr)")) + ((not (string? (nth args 1))) + (error "$define-in!: name must be a symbol")) + (:else + (let ((target (kernel-eval (first args) dyn-env))) + (cond + ((not (kernel-env? target)) + (error "$define-in!: first arg must evaluate to an env")) + (:else + (let ((v (kernel-eval (nth args 2) dyn-env))) + (kernel-env-bind! target (nth args 1) v) + v))))))))) + +(define + kernel-standard-env + (fn + () + (let + ((env (kernel-base-env))) + (kernel-env-bind! env "$if" kernel-if-operative) + (kernel-env-bind! env "$define!" kernel-define!-operative) + (kernel-env-bind! env "$sequence" kernel-sequence-operative) + (kernel-env-bind! env "$quote" kernel-quote-operative) + (kernel-env-bind! env "$quasiquote" kernel-quasiquote-operative) + (kernel-env-bind! env "$cond" kernel-cond-operative) + (kernel-env-bind! env "$when" kernel-when-operative) + (kernel-env-bind! env "$unless" kernel-unless-operative) + (kernel-env-bind! env "$and?" kernel-and?-operative) + (kernel-env-bind! env "$or?" kernel-or?-operative) + (kernel-env-bind! env "eval" kernel-eval-applicative) + (kernel-env-bind! + env + "make-environment" + kernel-make-environment-applicative) + (kernel-env-bind! + env + "get-current-environment" + kernel-get-current-env-operative) + (kernel-env-bind! env "+" + (knl-fold-app "+" (fn (a b) (+ a b)) 0 (fn (x) x))) + (kernel-env-bind! env "-" + (knl-fold-app "-" (fn (a b) (- a b)) 0 (fn (x) (- 0 x)))) + (kernel-env-bind! env "*" + (knl-fold-app "*" (fn (a b) (* a b)) 1 (fn (x) x))) + (kernel-env-bind! env "/" + (knl-fold-app "/" (fn (a b) (/ a b)) 1 (fn (x) (/ 1 x)))) + (kernel-env-bind! env "<" (knl-chain-cmp "<" (fn (a b) (< a b)))) + (kernel-env-bind! env ">" (knl-chain-cmp ">" (fn (a b) (> a b)))) + (kernel-env-bind! env "<=?" (knl-chain-cmp "<=?" (fn (a b) (<= a b)))) + (kernel-env-bind! env ">=?" (knl-chain-cmp ">=?" (fn (a b) (>= a b)))) + (kernel-env-bind! env "=?" kernel-eq?-applicative) + (kernel-env-bind! env "equal?" kernel-equal?-applicative) + (kernel-env-bind! env "eq?" kernel-eq?-applicative) + (kernel-env-bind! env "cons" kernel-cons-applicative) + (kernel-env-bind! env "car" kernel-car-applicative) + (kernel-env-bind! env "cdr" kernel-cdr-applicative) + (kernel-env-bind! env "list" kernel-list-applicative) + (kernel-env-bind! env "length" kernel-length-applicative) + (kernel-env-bind! env "null?" kernel-null?-applicative) + (kernel-env-bind! env "pair?" kernel-pair?-applicative) + (kernel-env-bind! env "map" kernel-map-applicative) + (kernel-env-bind! env "filter" kernel-filter-applicative) + (kernel-env-bind! env "reduce" kernel-reduce-applicative) + (kernel-env-bind! env "apply" kernel-apply-applicative) + (kernel-env-bind! env "append" kernel-append-applicative) + (kernel-env-bind! env "reverse" kernel-reverse-applicative) + (kernel-env-bind! env "number?" kernel-number?-applicative) + (kernel-env-bind! env "string?" kernel-string?-applicative) + (kernel-env-bind! env "list?" kernel-list?-applicative) + (kernel-env-bind! env "boolean?" kernel-boolean?-applicative) + (kernel-env-bind! env "symbol?" kernel-symbol?-applicative) + (kernel-env-bind! env "not" kernel-not-applicative) + (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/encap.sx b/lib/kernel/tests/encap.sx new file mode 100644 index 00000000..7530df9f --- /dev/null +++ b/lib/kernel/tests/encap.sx @@ -0,0 +1,183 @@ +;; lib/kernel/tests/encap.sx — exercises make-encapsulation-type. +;; +;; The Phase 5 Kernel idiom: build opaque types whose constructor, +;; predicate, and accessor are all standard Kernel applicatives. The +;; identity is per-call, so two `(make-encapsulation-type)` calls +;; produce non-interchangeable families. + +(define ken-test-pass 0) +(define ken-test-fail 0) +(define ken-test-fails (list)) + +(define + ken-test + (fn + (name actual expected) + (if + (= actual expected) + (set! ken-test-pass (+ ken-test-pass 1)) + (begin + (set! ken-test-fail (+ ken-test-fail 1)) + (append! ken-test-fails {:name name :actual actual :expected expected}))))) + +(define ken-eval-in (fn (src env) (kernel-eval (kernel-parse src) env))) + +;; A helper that builds a standard env with `encap`/`pred?`/`decap` +;; bound from a single call to make-encapsulation-type. +(define + ken-make-encap-env + (fn + () + (let + ((env (kernel-standard-env))) + (ken-eval-in "($define! triple (make-encapsulation-type))" env) + (ken-eval-in "($define! encap (car triple))" env) + (ken-eval-in "($define! pred? (car (cdr triple)))" env) + (ken-eval-in "($define! decap (car (cdr (cdr triple))))" env) + env))) + +;; ── construction ──────────────────────────────────────────────── +(ken-test + "make: returns 3-element list" + (ken-eval-in "(length (make-encapsulation-type))" (kernel-standard-env)) + 3) + +(ken-test + "make: first is applicative" + (kernel-applicative? + (ken-eval-in "(car (make-encapsulation-type))" (kernel-standard-env))) + true) + +(ken-test + "make: second is applicative" + (kernel-applicative? + (ken-eval-in + "(car (cdr (make-encapsulation-type)))" + (kernel-standard-env))) + true) + +(ken-test + "make: third is applicative" + (kernel-applicative? + (ken-eval-in + "(car (cdr (cdr (make-encapsulation-type))))" + (kernel-standard-env))) + true) + +;; ── round-trip ────────────────────────────────────────────────── +(ken-test + "round-trip: number" + (ken-eval-in "(decap (encap 42))" (ken-make-encap-env)) + 42) + +(ken-test + "round-trip: string" + (ken-eval-in "(decap (encap ($quote hello)))" (ken-make-encap-env)) + "hello") + +(ken-test + "round-trip: list" + (ken-eval-in "(decap (encap (list 1 2 3)))" (ken-make-encap-env)) + (list 1 2 3)) + +;; ── predicate ─────────────────────────────────────────────────── +(ken-test + "pred?: wrapped value" + (ken-eval-in "(pred? (encap 1))" (ken-make-encap-env)) + true) + +(ken-test + "pred?: raw value" + (ken-eval-in "(pred? 1)" (ken-make-encap-env)) + false) + +(ken-test + "pred?: raw string" + (ken-eval-in "(pred? ($quote foo))" (ken-make-encap-env)) + false) + +(ken-test + "pred?: raw list" + (ken-eval-in "(pred? (list))" (ken-make-encap-env)) + false) + +;; ── opacity: different families are not interchangeable ───────── +(ken-test + "opacity: foreign value rejected by predicate" + (let + ((env (kernel-standard-env))) + (ken-eval-in "($define! tA (make-encapsulation-type))" env) + (ken-eval-in "($define! tB (make-encapsulation-type))" env) + (ken-eval-in "($define! encA (car tA))" env) + (ken-eval-in "($define! predB (car (cdr tB)))" env) + (ken-eval-in "(predB (encA 42))" env)) + false) + +(ken-test + "opacity: decap rejects foreign value" + (let + ((env (kernel-standard-env))) + (ken-eval-in "($define! tA (make-encapsulation-type))" env) + (ken-eval-in "($define! tB (make-encapsulation-type))" env) + (ken-eval-in "($define! encA (car tA))" env) + (ken-eval-in "($define! decapB (car (cdr (cdr tB))))" env) + (guard (e (true :raised)) (ken-eval-in "(decapB (encA 42))" env))) + :raised) + +(ken-test + "opacity: decap rejects raw value" + (guard + (e (true :raised)) + (ken-eval-in "(decap 42)" (ken-make-encap-env))) + :raised) + +;; ── promise: classic Kernel encapsulation use case ────────────── +;; A "promise" wraps a thunk to compute on demand and memoises the +;; first result. Built entirely with the standard encap idiom. +(ken-test + "promise: force returns thunk result" + (let + ((env (kernel-standard-env))) + (ken-eval-in + "($sequence\n ($define! ptriple (make-encapsulation-type))\n ($define! make-promise (car ptriple))\n ($define! promise? (car (cdr ptriple)))\n ($define! decode-promise (car (cdr (cdr ptriple))))\n ($define! force ($lambda (p) ((decode-promise p))))\n ($define! delay ($lambda (thunk) (make-promise thunk)))\n (force (delay ($lambda () (+ 19 23)))))" + env)) + 42) + +(ken-test + "promise: promise? recognises its own type" + (let + ((env (kernel-standard-env))) + (ken-eval-in + "($sequence\n ($define! ptriple (make-encapsulation-type))\n ($define! make-promise (car ptriple))\n ($define! promise? (car (cdr ptriple)))\n (promise? (make-promise ($lambda () 42))))" + env)) + true) + +(ken-test + "promise: promise? false on plain value" + (let + ((env (kernel-standard-env))) + (ken-eval-in + "($sequence\n ($define! ptriple (make-encapsulation-type))\n ($define! promise? (car (cdr ptriple)))\n (promise? 99))" + env)) + false) + +;; ── independent families don't leak ───────────────────────────── +(ken-test + "two families: distinct identity" + (let + ((env (kernel-standard-env))) + (ken-eval-in + "($sequence\n ($define! t1 (make-encapsulation-type))\n ($define! t2 (make-encapsulation-type))\n ($define! enc1 (car t1))\n ($define! pred2 (car (cdr t2)))\n (pred2 (enc1 ($quote stuff))))" + env)) + false) + +(ken-test + "same family: re-bound shares identity" + (let + ((env (kernel-standard-env))) + (ken-eval-in + "($sequence\n ($define! t (make-encapsulation-type))\n ($define! e (car t))\n ($define! p (car (cdr t)))\n ($define! d (car (cdr (cdr t))))\n (list (p (e 7)) (d (e 7))))" + env)) + (list true 7)) + +(define ken-tests-run! (fn () {:total (+ ken-test-pass ken-test-fail) :passed ken-test-pass :failed ken-test-fail :fails ken-test-fails})) diff --git a/lib/kernel/tests/eval.sx b/lib/kernel/tests/eval.sx new file mode 100644 index 00000000..7e2f3ada --- /dev/null +++ b/lib/kernel/tests/eval.sx @@ -0,0 +1,270 @@ +;; lib/kernel/tests/eval.sx — exercises lib/kernel/eval.sx. +;; +;; Phase 2 covers literal evaluation, symbol lookup, and combiner +;; dispatch (operative vs applicative). Standard-environment operatives +;; ($if, $define!, $lambda, …) arrive in Phase 4, so tests build a +;; minimal env on the fly and verify the dispatch contract directly. + +(define ke-test-pass 0) +(define ke-test-fail 0) +(define ke-test-fails (list)) + +(define + ke-test + (fn + (name actual expected) + (if + (= actual expected) + (set! ke-test-pass (+ ke-test-pass 1)) + (begin + (set! ke-test-fail (+ ke-test-fail 1)) + (append! ke-test-fails {:name name :actual actual :expected expected}))))) + +;; ── helpers ────────────────────────────────────────────────────── + +(define ke-eval-src (fn (src env) (kernel-eval (kernel-parse src) env))) + +(define + ke-make-test-env + (fn + () + (let + ((env (kernel-make-env))) + (kernel-env-bind! + env + "+" + (kernel-make-primitive-applicative + (fn (args) (+ (first args) (nth args 1))))) + (kernel-env-bind! + env + "list" + (kernel-make-primitive-applicative (fn (args) args))) + (kernel-env-bind! + env + "$quote" + (kernel-make-primitive-operative (fn (args dyn-env) (first args)))) + (kernel-env-bind! + env + "$if" + (kernel-make-primitive-operative + (fn + (args dyn-env) + (if + (kernel-eval (first args) dyn-env) + (kernel-eval (nth args 1) dyn-env) + (kernel-eval (nth args 2) dyn-env))))) + env))) + +;; ── literal evaluation ─────────────────────────────────────────── +(ke-test "lit: number" (ke-eval-src "42" (kernel-make-env)) 42) +(ke-test "lit: zero" (ke-eval-src "0" (kernel-make-env)) 0) +(ke-test "lit: float" (ke-eval-src "3.14" (kernel-make-env)) 3.14) +(ke-test "lit: true" (ke-eval-src "#t" (kernel-make-env)) true) +(ke-test "lit: false" (ke-eval-src "#f" (kernel-make-env)) false) +(ke-test "lit: string" (ke-eval-src "\"hello\"" (kernel-make-env)) "hello") +(ke-test "lit: empty list" (ke-eval-src "()" (kernel-make-env)) (list)) + +;; ── symbol lookup ──────────────────────────────────────────────── +(ke-test + "sym: bound to number" + (let + ((env (kernel-make-env))) + (kernel-env-bind! env "x" 100) + (ke-eval-src "x" env)) + 100) + +(ke-test + "sym: bound to string" + (let + ((env (kernel-make-env))) + (kernel-env-bind! env "name" "kernel") + (ke-eval-src "name" env)) + "kernel") + +(ke-test + "sym: parent-chain lookup" + (let + ((p (kernel-make-env))) + (kernel-env-bind! p "outer" 1) + (let + ((c (kernel-extend-env p))) + (kernel-env-bind! c "inner" 2) + (+ (ke-eval-src "outer" c) (ke-eval-src "inner" c)))) + 3) + +(ke-test + "sym: child shadows parent" + (let + ((p (kernel-make-env))) + (kernel-env-bind! p "x" 1) + (let + ((c (kernel-extend-env p))) + (kernel-env-bind! c "x" 2) + (ke-eval-src "x" c))) + 2) + +(ke-test + "env-has?: present" + (let + ((env (kernel-make-env))) + (kernel-env-bind! env "x" 1) + (kernel-env-has? env "x")) + true) + +(ke-test + "env-has?: missing" + (kernel-env-has? (kernel-make-env) "nope") + false) + +;; ── tagged-value predicates ───────────────────────────────────── +(ke-test + "tag: operative?" + (kernel-operative? (kernel-make-primitive-operative (fn (a e) nil))) + true) + +(ke-test + "tag: applicative?" + (kernel-applicative? (kernel-make-primitive-applicative (fn (a) nil))) + true) + +(ke-test + "tag: combiner? operative" + (kernel-combiner? (kernel-make-primitive-operative (fn (a e) nil))) + true) + +(ke-test + "tag: combiner? applicative" + (kernel-combiner? (kernel-make-primitive-applicative (fn (a) nil))) + true) + +(ke-test "tag: combiner? number" (kernel-combiner? 42) false) + +(ke-test "tag: number is not operative" (kernel-operative? 42) false) + +;; ── wrap / unwrap ──────────────────────────────────────────────── +(ke-test + "wrap+unwrap roundtrip" + (let + ((op (kernel-make-primitive-operative (fn (a e) :sentinel)))) + (= (kernel-unwrap (kernel-wrap op)) op)) + true) + +(ke-test + "wrap produces applicative" + (kernel-applicative? + (kernel-wrap (kernel-make-primitive-operative (fn (a e) nil)))) + true) + +(ke-test + "unwrap of primitive-applicative is operative" + (kernel-operative? + (kernel-unwrap (kernel-make-primitive-applicative (fn (a) nil)))) + true) + +;; ── combiner dispatch — applicatives evaluate their args ───────── +(ke-test + "applicative: simple call" + (ke-eval-src "(+ 2 3)" (ke-make-test-env)) + 5) + +(ke-test + "applicative: nested" + (ke-eval-src "(+ (+ 1 2) (+ 3 4))" (ke-make-test-env)) + 10) + +(ke-test + "applicative: receives evaluated args" + (let + ((env (ke-make-test-env))) + (kernel-env-bind! env "x" 10) + (kernel-env-bind! env "y" 20) + (ke-eval-src "(+ x y)" env)) + 30) + +(ke-test + "applicative: list builds an SX list of values" + (let + ((env (ke-make-test-env))) + (kernel-env-bind! env "a" 1) + (kernel-env-bind! env "b" 2) + (ke-eval-src "(list a b 99)" env)) + (list 1 2 99)) + +;; ── combiner dispatch — operatives DO NOT evaluate their args ─── +(ke-test + "operative: $quote returns symbol unevaluated" + (ke-eval-src "($quote foo)" (ke-make-test-env)) + "foo") + +(ke-test + "operative: $quote returns list unevaluated" + (ke-eval-src "($quote (+ 1 2))" (ke-make-test-env)) + (list "+" 1 2)) + +(ke-test + "operative: $if true branch" + (ke-eval-src "($if #t 1 2)" (ke-make-test-env)) + 1) + +(ke-test + "operative: $if false branch" + (ke-eval-src "($if #f 1 2)" (ke-make-test-env)) + 2) + +(ke-test + "operative: $if doesn't eval untaken branch" + (ke-eval-src "($if #t 99 unbound)" (ke-make-test-env)) + 99) + +(ke-test + "operative: $if takes dynamic env for branches" + (let + ((env (ke-make-test-env))) + (kernel-env-bind! env "x" 7) + (ke-eval-src "($if #t x 0)" env)) + 7) + +;; ── operative built ON-THE-FLY can inspect raw expressions ────── +(ke-test + "operative: sees raw symbol head" + (let + ((env (kernel-make-env))) + (kernel-env-bind! + env + "head" + (kernel-make-primitive-operative (fn (args dyn-env) (first args)))) + (ke-eval-src "(head (+ 1 2))" env)) + (list "+" 1 2)) + +(ke-test + "operative: sees dynamic env" + (let + ((env (kernel-make-env))) + (kernel-env-bind! env "x" 999) + (kernel-env-bind! + env + "$probe" + (kernel-make-primitive-operative + (fn (args dyn-env) (kernel-env-lookup dyn-env "x")))) + (ke-eval-src "($probe ignored)" env)) + 999) + +;; ── error cases ────────────────────────────────────────────────── +(ke-test + "error: unbound symbol" + (guard + (e (true :raised)) + (kernel-eval (kernel-parse "nope") (kernel-make-env))) + :raised) + +(ke-test + "error: combine non-combiner" + (guard + (e (true :raised)) + (let + ((env (kernel-make-env))) + (kernel-env-bind! env "x" 42) + (kernel-eval (kernel-parse "(x 1)") env))) + :raised) + +(define ke-tests-run! (fn () {:total (+ ke-test-pass ke-test-fail) :passed ke-test-pass :failed ke-test-fail :fails ke-test-fails})) diff --git a/lib/kernel/tests/hygiene.sx b/lib/kernel/tests/hygiene.sx new file mode 100644 index 00000000..1a6b6a31 --- /dev/null +++ b/lib/kernel/tests/hygiene.sx @@ -0,0 +1,220 @@ +;; lib/kernel/tests/hygiene.sx — exercises Phase 6 hygiene helpers. +;; +;; Kernel-on-SX is hygienic by default: $vau/$lambda close over their +;; static env, and bind their formals (plus any $define!s in the body) +;; in a CHILD env. The caller's env is only mutated when user code +;; explicitly threads the env-param through `eval` or `$define-in!`. +;; +;; These tests verify the property, plus the Phase 6 helpers ($let and +;; $define-in!). Shutt's full scope-set hygiene (lifted symbols with +;; provenance markers) is research-grade and is NOT implemented — see +;; the plan's reflective-API notes for the proposed approach. + +(define kh-test-pass 0) +(define kh-test-fail 0) +(define kh-test-fails (list)) + +(define + kh-test + (fn + (name actual expected) + (if + (= actual expected) + (set! kh-test-pass (+ kh-test-pass 1)) + (begin + (set! kh-test-fail (+ kh-test-fail 1)) + (append! kh-test-fails {:name name :actual actual :expected expected}))))) + +(define kh-eval-in (fn (src env) (kernel-eval (kernel-parse src) env))) + +;; ── Default hygiene: $define! inside operative body stays local ─ + +(kh-test + "hygiene: vau body $define! doesn't escape" + (let + ((env (kernel-standard-env))) + (kh-eval-in "($define! x 1)" env) + (kh-eval-in + "($define! my-op ($vau () _ ($sequence ($define! x 999) x)))" + env) + (kh-eval-in "(my-op)" env) + (kh-eval-in "x" env)) + 1) + +(kh-test + "hygiene: vau body $define! visible inside body" + (let + ((env (kernel-standard-env))) + (kh-eval-in "($define! x 1)" env) + (kh-eval-in + "($define! my-op ($vau () _ ($sequence ($define! x 999) x)))" + env) + (kh-eval-in "(my-op)" env)) + 999) + +(kh-test + "hygiene: lambda body $define! doesn't escape" + (let + ((env (kernel-standard-env))) + (kh-eval-in "($define! y 50)" env) + (kh-eval-in "($define! f ($lambda () ($sequence ($define! y 7) y)))" env) + (kh-eval-in "(f)" env) + (kh-eval-in "y" env)) + 50) + +(kh-test + "hygiene: caller's binding visible inside operative" + (let + ((env (kernel-standard-env))) + (kh-eval-in "($define! caller-x 88)" env) + (kh-eval-in "($define! my-op ($vau () _ caller-x))" env) + (kh-eval-in "(my-op)" env)) + 88) + +;; ── $let — proper hygienic scoping ────────────────────────────── + +(kh-test + "let: returns body value" + (kh-eval-in "($let ((x 5)) (+ x 1))" (kernel-standard-env)) + 6) + +(kh-test + "let: multiple bindings" + (kh-eval-in "($let ((x 3) (y 4)) (+ x y))" (kernel-standard-env)) + 7) + +(kh-test + "let: bindings shadow outer" + (let + ((env (kernel-standard-env))) + (kh-eval-in "($define! x 1)" env) + (kh-eval-in "($let ((x 99)) x)" env)) + 99) + +(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)) x)" env) + (kh-eval-in "x" env)) + 1) + +(kh-test + "let: parallel — RHS sees outer, not inner" + (let + ((env (kernel-standard-env))) + (kh-eval-in "($define! x 1)" env) + (kh-eval-in "($let ((x 10) (y x)) y)" env)) + 1) + +(kh-test + "let: nested" + (kh-eval-in "($let ((x 1)) ($let ((y 2)) (+ x y)))" (kernel-standard-env)) + 3) + +(kh-test + "let: error on malformed binding" + (guard + (e (true :raised)) + (kh-eval-in "($let ((x)) x)" (kernel-standard-env))) + :raised) + +(kh-test + "let: error on non-symbol name" + (guard + (e (true :raised)) + (kh-eval-in "($let ((1 2)) 1)" (kernel-standard-env))) + :raised) + +;; ── $define-in! — explicit env targeting ──────────────────────── + +(kh-test + "define-in!: binds in chosen env, not dyn-env" + (let + ((env (kernel-standard-env))) + (kh-eval-in "($define! sandbox (make-environment))" env) + (kh-eval-in "($define-in! sandbox z 77)" env) + (kernel-env-has? (kh-eval-in "sandbox" env) "z")) + true) + +(kh-test + "define-in!: doesn't pollute caller" + (let + ((env (kernel-standard-env))) + (kh-eval-in "($define! sandbox (make-environment))" env) + (kh-eval-in "($define-in! sandbox z 77)" env) + (kernel-env-has? env "z")) + false) + +(kh-test + "define-in!: error on non-env target" + (guard + (e (true :raised)) + (let + ((env (kernel-standard-env))) + (kh-eval-in "($define-in! 42 x 1)" env))) + :raised) + +;; ── Closure does NOT see post-definition caller binds ─────────── +;; The classic "lexical scope wins over dynamic" test. + +(kh-test + "lexical: closure sees its own static env" + (let + ((env (kernel-standard-env))) + (kh-eval-in "($define! x 1)" env) + (kh-eval-in "($define! get-x ($lambda () x))" env) + (kh-eval-in "($define! x 999)" env) + (kh-eval-in "(get-x)" env)) + 999) + +(kh-test + "lexical: $let-bound name invisible outside" + (guard + (e (true :raised)) + (let + ((env (kernel-standard-env))) + (kh-eval-in "($let ((private 42)) private)" env) + (kh-eval-in "private" env))) + :raised) + +;; ── Operative + $let: hygiene compose ─────────────────────────── + +(kh-test + "let-inside-vau: temp doesn't escape body" + (let + ((env (kernel-standard-env))) + (kh-eval-in "($define! x 1)" env) + (kh-eval-in "($define! op ($vau () _ ($let ((x 5)) x)))" env) + (kh-eval-in "(op)" env) + (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/lib/kernel/tests/metacircular.sx b/lib/kernel/tests/metacircular.sx new file mode 100644 index 00000000..8588b845 --- /dev/null +++ b/lib/kernel/tests/metacircular.sx @@ -0,0 +1,162 @@ +;; lib/kernel/tests/metacircular.sx — Kernel-in-Kernel demo. +;; +;; Demonstrates reflective completeness: a Kernel program implements +;; a recognisable subset of Kernel's own evaluation rules and produces +;; matching values for a battery of test programs. +;; +;; This is a SHALLOW metacircular: it dispatches on expression shape +;; itself (numbers, booleans, lists, symbols), recursively meta-evals +;; each argument of an applicative call, and delegates only to the +;; host evaluator for the leaf cases (operatives, symbol lookup). The +;; point is to show that env-as-value, first-class operatives, and +;; first-class evaluators all line up — enough so a Kernel program +;; can itself reason about Kernel programs. + +(define kmc-test-pass 0) +(define kmc-test-fail 0) +(define kmc-test-fails (list)) + +(define + kmc-test + (fn + (name actual expected) + (if + (= actual expected) + (set! kmc-test-pass (+ kmc-test-pass 1)) + (begin + (set! kmc-test-fail (+ kmc-test-fail 1)) + (append! kmc-test-fails {:name name :actual actual :expected expected}))))) + +;; Build a Kernel env with m-eval and m-apply defined. The two refer +;; to each other and to standard primitives, so we use the standard +;; env as the static-env for both. +(define + kmc-make-env + (fn + () + (let + ((env (kernel-standard-env))) + (kernel-eval + (kernel-parse + "($define! m-eval\n ($lambda (expr env)\n ($cond\n ((number? expr) expr)\n ((boolean? expr) expr)\n ((null? expr) expr)\n ((symbol? expr) (eval expr env))\n ((list? expr)\n ($let ((head-val (m-eval (car expr) env)))\n ($cond\n ((applicative? head-val)\n (apply head-val\n (map ($lambda (a) (m-eval a env)) (cdr expr))))\n (else (eval expr env)))))\n (else expr))))") + env) + env))) + +(define + kmc-eval + (fn + (src) + (let + ((env (kmc-make-env))) + (kernel-eval + (kernel-parse + (str "(m-eval (quote " src ") (get-current-environment))")) + env)))) + +;; ── literals self-evaluate via m-eval ────────────────────────── +(kmc-test + "m-eval: integer literal" + (kernel-eval + (kernel-parse "(m-eval 42 (get-current-environment))") + (kmc-make-env)) + 42) + +(kmc-test + "m-eval: boolean true" + (kernel-eval + (kernel-parse "(m-eval #t (get-current-environment))") + (kmc-make-env)) + true) + +(kmc-test + "m-eval: boolean false" + (kernel-eval + (kernel-parse "(m-eval #f (get-current-environment))") + (kmc-make-env)) + false) + +(kmc-test + "m-eval: empty list" + (kernel-eval + (kernel-parse "(m-eval () (get-current-environment))") + (kmc-make-env)) + (list)) + +;; ── symbol lookup goes through env ───────────────────────────── +(kmc-test + "m-eval: symbol lookup" + (let + ((env (kmc-make-env))) + (kernel-eval (kernel-parse "($define! shared-x 99)") env) + (kernel-eval + (kernel-parse "(m-eval ($quote shared-x) (get-current-environment))") + env)) + 99) + +;; ── applicative calls are dispatched by m-eval recursively ───── +(kmc-test + "m-eval: addition" + (kernel-eval + (kernel-parse "(m-eval ($quote (+ 1 2)) (get-current-environment))") + (kmc-make-env)) + 3) + +(kmc-test + "m-eval: nested arithmetic" + (kernel-eval + (kernel-parse + "(m-eval ($quote (+ (* 2 3) (- 10 4))) (get-current-environment))") + (kmc-make-env)) + 12) + +(kmc-test + "m-eval: variadic +" + (kernel-eval + (kernel-parse "(m-eval ($quote (+ 1 2 3 4 5)) (get-current-environment))") + (kmc-make-env)) + 15) + +(kmc-test + "m-eval: list construction" + (kernel-eval + (kernel-parse "(m-eval ($quote (list 1 2 3)) (get-current-environment))") + (kmc-make-env)) + (list 1 2 3)) + +(kmc-test "m-eval: cons reverse-style" + (kernel-eval + (kernel-parse "(m-eval ($quote (cons 0 (list 1 2))) (get-current-environment))") + (kmc-make-env)) (list 0 1 2)) + +(kmc-test "m-eval: nested apply" + (kernel-eval + (kernel-parse "(m-eval ($quote (apply + (list 10 20 30))) (get-current-environment))") + (kmc-make-env)) 60) + +;; ── operatives delegate to host eval (transparently for the caller) ─ +(kmc-test + "m-eval: $if true branch (via delegation)" + (kernel-eval + (kernel-parse "(m-eval ($quote ($if #t 1 2)) (get-current-environment))") + (kmc-make-env)) + 1) + +(kmc-test + "m-eval: $if false branch" + (kernel-eval + (kernel-parse "(m-eval ($quote ($if #f 1 2)) (get-current-environment))") + (kmc-make-env)) + 2) + +;; ── m-eval can call a user-defined lambda ────────────────────── +(kmc-test + "m-eval: user lambda call" + (let + ((env (kmc-make-env))) + (kernel-eval (kernel-parse "($define! sq ($lambda (x) (* x x)))") env) + (kernel-eval + (kernel-parse "(m-eval ($quote (sq 7)) (get-current-environment))") + env)) + 49) + +(define kmc-tests-run! (fn () {:total (+ kmc-test-pass kmc-test-fail) :passed kmc-test-pass :failed kmc-test-fail :fails kmc-test-fails})) diff --git a/lib/kernel/tests/parse.sx b/lib/kernel/tests/parse.sx new file mode 100644 index 00000000..d70e7bb6 --- /dev/null +++ b/lib/kernel/tests/parse.sx @@ -0,0 +1,158 @@ +;; lib/kernel/tests/parse.sx — exercises lib/kernel/parser.sx. + +(define knl-test-pass 0) +(define knl-test-fail 0) +(define knl-test-fails (list)) + +(define + knl-test + (fn + (name actual expected) + (if + (= actual expected) + (set! knl-test-pass (+ knl-test-pass 1)) + (begin + (set! knl-test-fail (+ knl-test-fail 1)) + (append! knl-test-fails {:name name :actual actual :expected expected}))))) + +;; ── atoms: numbers ──────────────────────────────────────────────── +(knl-test "num: integer" (kernel-parse "42") 42) +(knl-test "num: zero" (kernel-parse "0") 0) +(knl-test "num: negative integer" (kernel-parse "-7") -7) +(knl-test "num: positive sign" (kernel-parse "+5") 5) +(knl-test "num: float" (kernel-parse "3.14") 3.14) +(knl-test "num: negative float" (kernel-parse "-2.5") -2.5) +(knl-test "num: leading dot" (kernel-parse ".5") 0.5) +(knl-test "num: exponent" (kernel-parse "1e3") 1000) +(knl-test "num: exponent with sign" (kernel-parse "2.5e-1") 0.25) +(knl-test "num: capital E exponent" (kernel-parse "1E2") 100) + +;; ── atoms: booleans ─────────────────────────────────────────────── +(knl-test "bool: true" (kernel-parse "#t") true) +(knl-test "bool: false" (kernel-parse "#f") false) + +;; ── atoms: empty list (Kernel nil) ──────────────────────────────── +(knl-test "nil: ()" (kernel-parse "()") (list)) +(knl-test "nil: (= () (list))" (= (kernel-parse "()") (list)) true) + +;; ── atoms: symbols ──────────────────────────────────────────────── +(knl-test "sym: word" (kernel-parse "foo") "foo") +(knl-test "sym: hyphenated" (kernel-parse "foo-bar") "foo-bar") +(knl-test "sym: dollar-bang" (kernel-parse "$define!") "$define!") +(knl-test "sym: question" (kernel-parse "null?") "null?") +(knl-test "sym: lt-eq" (kernel-parse "<=") "<=") +(knl-test "sym: bare plus" (kernel-parse "+") "+") +(knl-test "sym: bare minus" (kernel-parse "-") "-") +(knl-test "sym: plus-letter" (kernel-parse "+a") "+a") +(knl-test "sym: arrow" (kernel-parse "->") "->") +(knl-test "sym: dot-prefixed" (kernel-parse ".foo") ".foo") + +;; ── atoms: strings ──────────────────────────────────────────────── +(knl-test "str: empty" (kernel-string-value (kernel-parse "\"\"")) "") +(knl-test + "str: hello" + (kernel-string-value (kernel-parse "\"hello\"")) + "hello") +(knl-test "str: predicate" (kernel-string? (kernel-parse "\"x\"")) true) +(knl-test "str: not symbol" (kernel-string? (kernel-parse "x")) false) +(knl-test + "str: escape newline" + (kernel-string-value (kernel-parse "\"a\\nb\"")) + "a\nb") +(knl-test + "str: escape tab" + (kernel-string-value (kernel-parse "\"a\\tb\"")) + "a\tb") +(knl-test + "str: escape quote" + (kernel-string-value (kernel-parse "\"a\\\"b\"")) + "a\"b") +(knl-test + "str: escape backslash" + (kernel-string-value (kernel-parse "\"a\\\\b\"")) + "a\\b") + +;; ── lists ───────────────────────────────────────────────────────── +(knl-test "list: flat" (kernel-parse "(a b c)") (list "a" "b" "c")) +(knl-test + "list: nested" + (kernel-parse "(a (b c) d)") + (list "a" (list "b" "c") "d")) +(knl-test + "list: deeply nested" + (kernel-parse "(((x)))") + (list (list (list "x")))) +(knl-test + "list: mixed atoms" + (kernel-parse "(1 #t foo)") + (list 1 true "foo")) +(knl-test + "list: empty inside" + (kernel-parse "(a () b)") + (list "a" (list) "b")) + +;; ── whitespace + comments ───────────────────────────────────────── +(knl-test "ws: leading" (kernel-parse " 42") 42) +(knl-test "ws: trailing" (kernel-parse "42 ") 42) +(knl-test "ws: tabs/newlines" (kernel-parse "\n\t 42 \n") 42) +(knl-test "comment: line" (kernel-parse "; nope\n42") 42) +(knl-test "comment: trailing" (kernel-parse "42 ; tail") 42) +(knl-test + "comment: inside list" + (kernel-parse "(a ; mid\n b)") + (list "a" "b")) + +;; ── parse-all ───────────────────────────────────────────────────── +(knl-test "all: empty input" (kernel-parse-all "") (list)) +(knl-test "all: only whitespace" (kernel-parse-all " ") (list)) +(knl-test "all: only comment" (kernel-parse-all "; nope") (list)) +(knl-test + "all: three forms" + (kernel-parse-all "1 2 3") + (list 1 2 3)) +(knl-test + "all: mixed" + (kernel-parse-all "($if #t 1 2) foo") + (list (list "$if" true 1 2) "foo")) + +;; ── classic Kernel programs (smoke) ─────────────────────────────── +(knl-test + "klisp: vau form" + (kernel-parse "($vau (x e) e (eval x e))") + (list "$vau" (list "x" "e") "e" (list "eval" "x" "e"))) +(knl-test + "klisp: define lambda" + (kernel-parse "($define! sq ($lambda (x) (* x x)))") + (list "$define!" "sq" (list "$lambda" (list "x") (list "*" "x" "x")))) + +;; ── round-trip identity for primitive symbols ───────────────────── +(knl-test "identity: $vau" (kernel-parse "$vau") "$vau") +(knl-test "identity: $lambda" (kernel-parse "$lambda") "$lambda") +(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/lib/kernel/tests/standard.sx b/lib/kernel/tests/standard.sx new file mode 100644 index 00000000..803dec0a --- /dev/null +++ b/lib/kernel/tests/standard.sx @@ -0,0 +1,445 @@ +;; lib/kernel/tests/standard.sx — exercises the Kernel standard env. +;; +;; Phase 4 tests verify that the standard env is rich enough to run +;; classic Kernel programs: factorial via recursion, list operations, +;; first-class environment manipulation. Each test starts from a fresh +;; standard env via `(kernel-standard-env)`. + +(define ks-test-pass 0) +(define ks-test-fail 0) +(define ks-test-fails (list)) + +(define + ks-test + (fn + (name actual expected) + (if + (= actual expected) + (set! ks-test-pass (+ ks-test-pass 1)) + (begin + (set! ks-test-fail (+ ks-test-fail 1)) + (append! ks-test-fails {:name name :actual actual :expected expected}))))) + +(define + ks-eval + (fn (src) (kernel-eval (kernel-parse src) (kernel-standard-env)))) + +(define ks-eval-in (fn (src env) (kernel-eval (kernel-parse src) env))) + +(define + ks-eval-all + (fn (src env) (kernel-eval-program (kernel-parse-all src) env))) + +;; ── $if ────────────────────────────────────────────────────────── +(ks-test "if: true branch" (ks-eval "($if #t 1 2)") 1) +(ks-test "if: false branch" (ks-eval "($if #f 1 2)") 2) +(ks-test "if: predicate" + (ks-eval "($if (<=? 1 2) ($quote yes) ($quote no))") "yes") +(ks-test + "if: untaken branch not evaluated" + (ks-eval "($if #t 42 nope)") + 42) + +;; ── $define! + arithmetic ─────────────────────────────────────── +(ks-test + "define!: returns value" + (let ((env (kernel-standard-env))) (ks-eval-in "($define! x 5)" env)) + 5) + +(ks-test + "define!: bound in env" + (let + ((env (kernel-standard-env))) + (ks-eval-in "($define! x 5)" env) + (ks-eval-in "x" env)) + 5) + +(ks-test "arith: +" (ks-eval "(+ 2 3)") 5) +(ks-test "arith: -" (ks-eval "(- 10 4)") 6) +(ks-test "arith: *" (ks-eval "(* 6 7)") 42) +(ks-test "arith: /" (ks-eval "(/ 20 5)") 4) +(ks-test "cmp: < true" (ks-eval "(< 1 2)") true) +(ks-test "cmp: < false" (ks-eval "(< 2 1)") false) +(ks-test "cmp: >=" (ks-eval "(>=? 2 2)") true) +(ks-test "cmp: <=" (ks-eval "(<=? 2 3)") true) +(ks-test "cmp: =" (ks-eval "(=? 7 7)") true) + +;; ── $sequence ──────────────────────────────────────────────────── +(ks-test "sequence: empty" (ks-eval "($sequence)") nil) +(ks-test "sequence: single" (ks-eval "($sequence 99)") 99) +(ks-test + "sequence: multi-effect" + (let + ((env (kernel-standard-env))) + (ks-eval-in "($sequence ($define! a 1) ($define! b 2) (+ a b))" env)) + 3) + +;; ── list primitives ────────────────────────────────────────────── +(ks-test + "list: builds" + (ks-eval "(list 1 2 3)") + (list 1 2 3)) +(ks-test "list: empty" (ks-eval "(list)") (list)) +(ks-test + "cons: prepend" + (ks-eval "(cons 0 (list 1 2 3))") + (list 0 1 2 3)) +(ks-test "car: head" (ks-eval "(car (list 10 20 30))") 10) +(ks-test + "cdr: tail" + (ks-eval "(cdr (list 10 20 30))") + (list 20 30)) +(ks-test "length: 3" (ks-eval "(length (list 1 2 3))") 3) +(ks-test "length: 0" (ks-eval "(length (list))") 0) +(ks-test "null?: empty" (ks-eval "(null? (list))") true) +(ks-test "null?: nonempty" (ks-eval "(null? (list 1))") false) +(ks-test "pair?: empty" (ks-eval "(pair? (list))") false) +(ks-test "pair?: nonempty" (ks-eval "(pair? (list 1))") true) + +;; ── $quote ─────────────────────────────────────────────────────── +(ks-test "quote: symbol" (ks-eval "($quote foo)") "foo") +(ks-test + "quote: list" + (ks-eval "($quote (+ 1 2))") + (list "+" 1 2)) + +;; ── boolean / not ──────────────────────────────────────────────── +(ks-test "not: true" (ks-eval "(not #t)") false) +(ks-test "not: false" (ks-eval "(not #f)") true) + +;; ── factorial ──────────────────────────────────────────────────── +(ks-test + "factorial: 5!" + (let + ((env (kernel-standard-env))) + (ks-eval-in + "($define! factorial ($lambda (n) ($if (<=? n 1) 1 (* n (factorial (- n 1))))))" + env) + (ks-eval-in "(factorial 5)" env)) + 120) + +(ks-test + "factorial: 0! = 1" + (let + ((env (kernel-standard-env))) + (ks-eval-in + "($define! factorial ($lambda (n) ($if (<=? n 1) 1 (* n (factorial (- n 1))))))" + env) + (ks-eval-in "(factorial 0)" env)) + 1) + +(ks-test + "factorial: 10!" + (let + ((env (kernel-standard-env))) + (ks-eval-in + "($define! factorial ($lambda (n) ($if (<=? n 1) 1 (* n (factorial (- n 1))))))" + env) + (ks-eval-in "(factorial 10)" env)) + 3628800) + +;; ── recursive list operations ──────────────────────────────────── +(ks-test + "sum: recursive over list" + (let + ((env (kernel-standard-env))) + (ks-eval-in + "($define! sum ($lambda (xs) ($if (null? xs) 0 (+ (car xs) (sum (cdr xs))))))" + env) + (ks-eval-in "(sum (list 1 2 3 4 5))" env)) + 15) + +(ks-test + "len: recursive count" + (let + ((env (kernel-standard-env))) + (ks-eval-in + "($define! mylen ($lambda (xs) ($if (null? xs) 0 (+ 1 (mylen (cdr xs))))))" + env) + (ks-eval-in "(mylen (list 1 2 3 4))" env)) + 4) + +(ks-test + "map-add1: build new list" + (let + ((env (kernel-standard-env))) + (ks-eval-in + "($define! add1-all ($lambda (xs) ($if (null? xs) (list) (cons (+ 1 (car xs)) (add1-all (cdr xs))))))" + env) + (ks-eval-in "(add1-all (list 10 20 30))" env)) + (list 11 21 31)) + +;; ── eval as a first-class applicative ──────────────────────────── +(ks-test + "eval: applies to constructed form" + (ks-eval "(eval (list ($quote +) 2 3) (get-current-environment))") + 5) + +(ks-test + "eval: with a fresh make-environment" + (guard + (e (true :raised)) + (ks-eval "(eval ($quote (+ 1 2)) (make-environment))")) + :raised) + +(ks-test + "eval: in extended env sees parent's bindings" + (let + ((env (kernel-standard-env))) + (ks-eval-in "($define! shared 7)" env) + (ks-eval-in + "(eval ($quote shared) (make-environment (get-current-environment)))" + env)) + 7) + +;; ── get-current-environment ────────────────────────────────────── +(ks-test + "get-current-environment: returns env" + (kernel-env? (ks-eval "(get-current-environment)")) + true) + +(ks-test + "get-current-environment: contains $if" + (let + ((env (ks-eval "(get-current-environment)"))) + (kernel-env-has? env "$if")) + true) + +(ks-test + "make-environment: empty" + (let ((env (ks-eval "(make-environment)"))) (kernel-env-has? env "$if")) + false) + +(ks-test + "make-environment: child sees parent" + (let + ((env (kernel-standard-env))) + (ks-eval-in "($define! marker 123)" env) + (let + ((child (ks-eval-in "(make-environment (get-current-environment))" env))) + (kernel-env-has? child "marker"))) + true) + +;; ── closures and lexical scope ─────────────────────────────────── +(ks-test + "closure: captures binding" + (let + ((env (kernel-standard-env))) + (ks-eval-in + "($define! make-adder ($lambda (n) ($lambda (x) (+ x n))))" + env) + (ks-eval-in "($define! add5 (make-adder 5))" env) + (ks-eval-in "(add5 10)" env)) + 15) + +(ks-test + "closure: nested lookups" + (let + ((env (kernel-standard-env))) + (ks-eval-in + "($define! curry-add ($lambda (a) ($lambda (b) ($lambda (c) (+ a (+ b c))))))" + env) + (ks-eval-in "(((curry-add 1) 2) 3)" env)) + 6) + +;; ── operative defined in standard env can reach $define! ───────── +(ks-test + "custom: define-via-vau" + (let + ((env (kernel-standard-env))) + (ks-eval-in + "($define! $let-it ($vau (name expr) e ($sequence ($define! tmp (eval expr e)) (eval (list ($quote $define!) name (list ($quote $quote) tmp)) e) tmp)))" + env) + (ks-eval-in "($let-it z 77)" env) + (ks-eval-in "z" env)) + 77) + +;; ── quasiquote ────────────────────────────────────────────────── +(ks-test "qq: plain atom" (ks-eval "`hello") "hello") +(ks-test "qq: plain list" (ks-eval "`(a b c)") (list "a" "b" "c")) +(ks-test "qq: unquote splices value" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! x 42)" env) + (ks-eval-in "`(a ,x b)" env)) (list "a" 42 "b")) +(ks-test "qq: unquote-splicing splices list" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! xs (list 1 2 3))" env) + (ks-eval-in "`(a ,@xs b)" env)) (list "a" 1 2 3 "b")) +(ks-test "qq: unquote-splicing at end" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! xs (list 9 8))" env) + (ks-eval-in "`(a b ,@xs)" env)) (list "a" "b" 9 8)) +(ks-test "qq: unquote-splicing at start" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! xs (list 1 2))" env) + (ks-eval-in "`(,@xs c)" env)) (list 1 2 "c")) +(ks-test "qq: nested list with unquote inside" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! x 5)" env) + (ks-eval-in "`(a (b ,x) c)" env)) + (list "a" (list "b" 5) "c")) +(ks-test "qq: error on bare unquote-splicing into non-list" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! x 42)" env) + (guard (e (true :raised)) + (ks-eval-in "`(a ,@x b)" env))) + :raised) + +;; ── $cond / $when / $unless ───────────────────────────────────── +(ks-test "cond: first match" + (ks-eval "($cond (#f 1) (#t 2) (#t 3))") 2) +(ks-test "cond: else fallback" + (ks-eval "($cond (#f 1) (else 99))") 99) +(ks-test "cond: no match returns nil" + (ks-eval "($cond (#f 1) (#f 2))") nil) +(ks-test "cond: empty clauses returns nil" + (ks-eval "($cond)") nil) +(ks-test "cond: multi-expr body" + (ks-eval "($cond (#t 1 2 3))") 3) +(ks-test "cond: doesn't evaluate untaken clauses" + ;; If the second clause's test were evaluated, the unbound `nope` would error. + (ks-eval "($cond (#t 7) (nope ignored))") 7) +(ks-test "cond: predicate evaluation" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! n 5)" env) + (ks-eval-in "($cond ((< n 0) ($quote negative)) ((= n 0) ($quote zero)) (else ($quote positive)))" env)) + "positive") + +(ks-test "when: true runs body" + (ks-eval "($when #t 1 2 3)") 3) +(ks-test "when: false returns nil" + (ks-eval "($when #f 1 2 3)") nil) +(ks-test "when: skips body when false" + (ks-eval "($when #f nope)") nil) + +(ks-test "unless: false runs body" + (ks-eval "($unless #f 99)") 99) +(ks-test "unless: true returns nil" + (ks-eval "($unless #t 99)") nil) +(ks-test "unless: skips body when true" + (ks-eval "($unless #t nope)") nil) + +;; ── $and? / $or? short-circuit ────────────────────────────────── +(ks-test "and: empty returns true" (ks-eval "($and?)") true) +(ks-test "and: single returns value" (ks-eval "($and? 42)") 42) +(ks-test "and: all true returns last" + (ks-eval "($and? 1 2 3)") 3) +(ks-test "and: first false short-circuits" + (ks-eval "($and? #f nope)") false) +(ks-test "and: false in middle short-circuits" + (ks-eval "($and? 1 #f nope)") false) +(ks-test "or: empty returns false" (ks-eval "($or?)") false) +(ks-test "or: single returns value" (ks-eval "($or? 42)") 42) +(ks-test "or: first truthy short-circuits" + (ks-eval "($or? 99 nope)") 99) +(ks-test "or: all false returns last" + (ks-eval "($or? #f #f #f)") false) +(ks-test "or: middle truthy" + (ks-eval "($or? #f 42 nope)") 42) + +;; ── variadic arithmetic ───────────────────────────────────────── +(ks-test "+: zero args = 0" (ks-eval "(+)") 0) +(ks-test "+: one arg = arg" (ks-eval "(+ 7)") 7) +(ks-test "+: two args" (ks-eval "(+ 3 4)") 7) +(ks-test "+: five args" (ks-eval "(+ 1 2 3 4 5)") 15) + +(ks-test "*: zero args = 1" (ks-eval "(*)") 1) +(ks-test "*: one arg" (ks-eval "(* 7)") 7) +(ks-test "*: four args" (ks-eval "(* 1 2 3 4)") 24) + +(ks-test "-: one arg negates" (ks-eval "(- 10)") -10) +(ks-test "-: two args" (ks-eval "(- 10 3)") 7) +(ks-test "-: four args fold" (ks-eval "(- 100 1 2 3)") 94) + +(ks-test "/: two args" (ks-eval "(/ 20 5)") 4) +(ks-test "/: three args fold" (ks-eval "(/ 100 2 5)") 10) + +;; ── variadic chained comparison ───────────────────────────────── +(ks-test "<: chained ascending" (ks-eval "(< 1 2 3 4 5)") true) +(ks-test "<: not strict" (ks-eval "(< 1 2 2 3)") false) +(ks-test "<: anti-monotonic" (ks-eval "(< 5 3)") false) +(ks-test ">: chained descending" (ks-eval "(> 5 4 3 2 1)") true) +(ks-test "<=? ascending equals" (ks-eval "(<=? 1 1 2 3 3)") true) +(ks-test "<=? violation" (ks-eval "(<=? 1 2 1)") false) +(ks-test ">=? descending equals" (ks-eval "(>=? 3 3 2 1)") true) + +;; ── list combinators ──────────────────────────────────────────── +(ks-test "map: square" + (ks-eval "(map ($lambda (x) (* x x)) (list 1 2 3 4))") + (list 1 4 9 16)) +(ks-test "map: empty list" + (ks-eval "(map ($lambda (x) x) (list))") (list)) +(ks-test "map: identity preserves" + (ks-eval "(map ($lambda (x) x) (list 1 2 3))") (list 1 2 3)) +(ks-test "map: with closure over outer" + (let ((env (kernel-standard-env))) + (ks-eval-in "($define! k 10)" env) + (ks-eval-in "(map ($lambda (x) (+ x k)) (list 1 2 3))" env)) + (list 11 12 13)) + +(ks-test "filter: positives" + (ks-eval "(filter ($lambda (x) (< 0 x)) (list -2 -1 0 1 2))") + (list 1 2)) +(ks-test "filter: empty result" + (ks-eval "(filter ($lambda (x) #f) (list 1 2 3))") (list)) +(ks-test "filter: all match" + (ks-eval "(filter ($lambda (x) #t) (list 1 2 3))") (list 1 2 3)) + +(ks-test "reduce: sum" + (ks-eval "(reduce ($lambda (a b) (+ a b)) 0 (list 1 2 3 4 5))") 15) +(ks-test "reduce: product" + (ks-eval "(reduce ($lambda (a b) (* a b)) 1 (list 1 2 3 4))") 24) +(ks-test "reduce: empty returns init" + (ks-eval "(reduce ($lambda (a b) (+ a b)) 42 (list))") 42) +(ks-test "reduce: build list" + (ks-eval "(reduce ($lambda (acc x) (cons x acc)) () (list 1 2 3))") + (list 3 2 1)) + +;; ── apply ──────────────────────────────────────────────────────── +(ks-test "apply: + over list" + (ks-eval "(apply + (list 1 2 3 4 5))") 15) +(ks-test "apply: lambda" + (ks-eval "(apply ($lambda (a b c) (* a (+ b c))) (list 2 3 4))") 14) +(ks-test "apply: list identity" + (ks-eval "(apply list (list 1 2 3))") (list 1 2 3)) +(ks-test "apply: empty args list" + (ks-eval "(apply + (list))") 0) +(ks-test "apply: single arg list" + (ks-eval "(apply ($lambda (x) (* x 10)) (list 7))") 70) +(ks-test "apply: built via map+apply" + ;; (apply + (map ($lambda (x) (* x x)) (list 1 2 3))) → 1+4+9 = 14 + (ks-eval + "(apply + (map ($lambda (x) (* x x)) (list 1 2 3)))") 14) +(ks-test "apply: error on non-list args" + (guard (e (true :raised)) + (ks-eval "(apply + 5)")) + :raised) + +;; ── append / reverse ──────────────────────────────────────────── +(ks-test "append: two lists" + (ks-eval "(append (list 1 2) (list 3 4))") (list 1 2 3 4)) +(ks-test "append: three lists" + (ks-eval "(append (list 1) (list 2) (list 3))") (list 1 2 3)) +(ks-test "append: empty list" + (ks-eval "(append)") (list)) +(ks-test "append: one list" + (ks-eval "(append (list 1 2 3))") (list 1 2 3)) +(ks-test "append: empty + nonempty" + (ks-eval "(append (list) (list 1 2))") (list 1 2)) +(ks-test "append: nonempty + empty" + (ks-eval "(append (list 1 2) (list))") (list 1 2)) +(ks-test "append: error on non-list" + (guard (e (true :raised)) + (ks-eval "(append (list 1) 5)")) + :raised) + +(ks-test "reverse: four elements" + (ks-eval "(reverse (list 1 2 3 4))") (list 4 3 2 1)) +(ks-test "reverse: empty" + (ks-eval "(reverse (list))") (list)) +(ks-test "reverse: single" + (ks-eval "(reverse (list 99))") (list 99)) +(ks-test "reverse: double reverse is identity" + (ks-eval "(reverse (reverse (list 1 2 3)))") (list 1 2 3)) + +(define ks-tests-run! (fn () {:total (+ ks-test-pass ks-test-fail) :passed ks-test-pass :failed ks-test-fail :fails ks-test-fails})) diff --git a/lib/kernel/tests/vau.sx b/lib/kernel/tests/vau.sx new file mode 100644 index 00000000..b64e7690 --- /dev/null +++ b/lib/kernel/tests/vau.sx @@ -0,0 +1,309 @@ +;; lib/kernel/tests/vau.sx — exercises lib/kernel/runtime.sx. +;; +;; Verifies the Phase 3 promise: user-defined operatives and applicatives +;; constructible from inside the language. Tests build a Kernel +;; base-env, bind a few helper applicatives (+, *, list, =, $if), and +;; run programs that construct and use custom combiners. + +(define kv-test-pass 0) +(define kv-test-fail 0) +(define kv-test-fails (list)) + +(define + kv-test + (fn + (name actual expected) + (if + (= actual expected) + (set! kv-test-pass (+ kv-test-pass 1)) + (begin + (set! kv-test-fail (+ kv-test-fail 1)) + (append! kv-test-fails {:name name :actual actual :expected expected}))))) + +(define kv-eval-src (fn (src env) (kernel-eval (kernel-parse src) env))) + +(define + kv-make-env + (fn + () + (let + ((env (kernel-base-env))) + (kernel-env-bind! + env + "+" + (kernel-make-primitive-applicative + (fn (args) (+ (first args) (nth args 1))))) + (kernel-env-bind! + env + "*" + (kernel-make-primitive-applicative + (fn (args) (* (first args) (nth args 1))))) + (kernel-env-bind! + env + "-" + (kernel-make-primitive-applicative + (fn (args) (- (first args) (nth args 1))))) + (kernel-env-bind! + env + "=" + (kernel-make-primitive-applicative + (fn (args) (= (first args) (nth args 1))))) + (kernel-env-bind! + env + "list" + (kernel-make-primitive-applicative (fn (args) args))) + (kernel-env-bind! + env + "cons" + (kernel-make-primitive-applicative + (fn (args) (cons (first args) (nth args 1))))) + (kernel-env-bind! + env + "$quote" + (kernel-make-primitive-operative (fn (args dyn-env) (first args)))) + (kernel-env-bind! + env + "$if" + (kernel-make-primitive-operative + (fn + (args dyn-env) + (if + (kernel-eval (first args) dyn-env) + (kernel-eval (nth args 1) dyn-env) + (kernel-eval (nth args 2) dyn-env))))) + env))) + +;; ── $vau: builds an operative ─────────────────────────────────── +(kv-test + "vau: identity returns first arg unevaluated" + (kv-eval-src "(($vau (a) _ a) hello)" (kv-make-env)) + "hello") + +(kv-test + "vau: returns args as raw expressions" + (kv-eval-src "(($vau (a b) _ (list a b)) (+ 1 2) (+ 3 4))" (kv-make-env)) + (list (list "+" 1 2) (list "+" 3 4))) + +(kv-test + "vau: env-param is a kernel env" + (kernel-env? (kv-eval-src "(($vau () e e))" (kv-make-env))) + true) + +(kv-test + "vau: returns operative" + (kernel-operative? (kv-eval-src "($vau (x) _ x)" (kv-make-env))) + true) + +(kv-test + "vau: returns operative not applicative" + (kernel-applicative? (kv-eval-src "($vau (x) _ x)" (kv-make-env))) + false) + +(kv-test + "vau: zero-arg body" + (kv-eval-src "(($vau () _ 42))" (kv-make-env)) + 42) + +(kv-test + "vau: static-env closure captured" + (let + ((outer (kv-make-env))) + (kernel-env-bind! outer "captured" 17) + (let + ((op (kv-eval-src "($vau () _ captured)" outer)) + (caller (kv-make-env))) + (kernel-env-bind! caller "captured" 99) + (kernel-combine op (list) caller))) + 17) + +(kv-test + "vau: env-param exposes caller's dynamic env" + (let + ((outer (kv-make-env))) + (kernel-env-bind! outer "x" 1) + (let + ((op (kv-eval-src "($vau () e e)" outer)) (caller (kv-make-env))) + (kernel-env-bind! caller "x" 2) + (let + ((e-val (kernel-combine op (list) caller))) + (kernel-env-lookup e-val "x")))) + 2) + +;; ── $lambda: applicatives evaluate their args ─────────────────── +(kv-test + "lambda: identity" + (kv-eval-src "(($lambda (x) x) 42)" (kv-make-env)) + 42) + +(kv-test + "lambda: addition" + (kv-eval-src "(($lambda (x y) (+ x y)) 3 4)" (kv-make-env)) + 7) + +(kv-test + "lambda: args are evaluated before bind" + (kv-eval-src "(($lambda (x) x) (+ 2 3))" (kv-make-env)) + 5) + +(kv-test + "lambda: zero args" + (kv-eval-src "(($lambda () 99))" (kv-make-env)) + 99) + +(kv-test + "lambda: returns applicative" + (kernel-applicative? (kv-eval-src "($lambda (x) x)" (kv-make-env))) + true) + +(kv-test + "lambda: returns applicative not operative" + (kernel-operative? (kv-eval-src "($lambda (x) x)" (kv-make-env))) + false) + +(kv-test + "lambda: higher-order" + (kv-eval-src "(($lambda (f) (f 10)) ($lambda (x) (+ x 1)))" (kv-make-env)) + 11) + +;; ── wrap / unwrap as user-callable applicatives ───────────────── + +(kv-test + "wrap: makes applicative from operative" + (kernel-applicative? (kv-eval-src "(wrap ($vau (x) _ x))" (kv-make-env))) + true) + +(kv-test + "wrap: result evaluates its arg" + (kv-eval-src "((wrap ($vau (x) _ x)) (+ 1 2))" (kv-make-env)) + 3) + +(kv-test + "unwrap: extracts operative from applicative" + (kernel-operative? (kv-eval-src "(unwrap ($lambda (x) x))" (kv-make-env))) + true) + +(kv-test + "wrap/unwrap roundtrip preserves identity" + (kv-eval-src + "(($lambda (op) (= op (unwrap (wrap op)))) ($vau (x) _ x))" + (kv-make-env)) + true) + +;; ── operative? / applicative? as user-visible predicates ──────── + +(kv-test + "operative? on vau result" + (kv-eval-src "(operative? ($vau (x) _ x))" (kv-make-env)) + true) + +(kv-test + "operative? on lambda result" + (kv-eval-src "(operative? ($lambda (x) x))" (kv-make-env)) + false) + +(kv-test + "applicative? on lambda result" + (kv-eval-src "(applicative? ($lambda (x) x))" (kv-make-env)) + true) + +(kv-test + "applicative? on vau result" + (kv-eval-src "(applicative? ($vau (x) _ x))" (kv-make-env)) + false) + +(kv-test + "operative? on number" + (kv-eval-src "(operative? 42)" (kv-make-env)) + false) + +;; ── Build BOTH layers from user code ──────────────────────────── +;; The headline Phase 3 test: defining an operative on top of an +;; applicative defined on top of a vau. + +(kv-test + "custom: applicative + operative compose" + (let + ((env (kv-make-env))) + (kernel-env-bind! env "square" (kv-eval-src "($lambda (x) (* x x))" env)) + (kv-eval-src "(square 4)" env)) + 16) + +(kv-test "custom: operative captures argument syntax" + ;; ($capture x) returns the raw expression `x`, regardless of value. + (let ((env (kv-make-env))) + (kernel-env-bind! env "$capture" + (kv-eval-src "($vau (form) _ form)" env)) + (kv-eval-src "($capture (+ 1 2))" env)) + (list "+" 1 2)) + +(kv-test "custom: applicative re-wraps an operative" + ;; Build a captured operative, then wrap it into an applicative that + ;; evaluates args before re-entry. This exercises wrap+$vau composed. + (let ((env (kv-make-env))) + (kernel-env-bind! env "id-app" + (kv-eval-src "(wrap ($vau (x) _ x))" env)) + (kv-eval-src "(id-app (+ 10 20))" env)) + 30) + +;; ── Error cases ────────────────────────────────────────────────── + +(kv-test + "vau: rejects non-list formals" + (guard (e (true :raised)) (kv-eval-src "($vau x _ x)" (kv-make-env))) + :raised) + +(kv-test + "vau: rejects non-symbol formal" + (guard (e (true :raised)) (kv-eval-src "($vau (1) _ x)" (kv-make-env))) + :raised) + +(kv-test + "vau: rejects non-symbol env-param" + (guard (e (true :raised)) (kv-eval-src "($vau (x) 7 x)" (kv-make-env))) + :raised) + +(kv-test + "vau: too few args at call site" + (guard + (e (true :raised)) + (kv-eval-src "(($vau (x y) _ x) 1)" (kv-make-env))) + :raised) + +(kv-test + "vau: too many args at call site" + (guard + (e (true :raised)) + (kv-eval-src "(($vau (x) _ x) 1 2)" (kv-make-env))) + :raised) + +(kv-test + "wrap: rejects non-operative" + (guard (e (true :raised)) (kv-eval-src "(wrap 42)" (kv-make-env))) + :raised) + +(kv-test + "unwrap: rejects non-applicative" + (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 bb9a2de1..9dddb988 100644 --- a/plans/kernel-on-sx.md +++ b/plans/kernel-on-sx.md @@ -56,41 +56,49 @@ The whole interesting thing: there are no special forms hardcoded in the evaluat ## Roadmap ### Phase 1 — Parser -- [ ] S-expression reader with the standard atoms (number, string, symbol, boolean, nil) and lists. -- [ ] Reader macros optional; defer to Phase 6. -- [ ] Tests in `lib/kernel/tests/parse.sx`. +- [x] S-expression reader with the standard atoms (number, string, symbol, boolean, nil) and lists. +- [x] Reader macros optional; defer to Phase 6. +- [x] Tests in `lib/kernel/tests/parse.sx`. ### Phase 2 — Core evaluator with first-class environments -- [ ] `kernel-eval expr env` — primary entry, walks AST, threads env as a value. -- [ ] Symbol lookup → environment value (using SX env-as-value primitives). -- [ ] List → look up head, dispatch on tag (applicative vs operative). -- [ ] No hardcoded special forms — even `if`/`define`/`lambda` are env-bound. -- [ ] Tests in `lib/kernel/tests/eval.sx`. +- [x] `kernel-eval expr env` — primary entry, walks AST, threads env as a value. +- [x] Symbol lookup → environment value (using SX env-as-value primitives). +- [x] List → look up head, dispatch on tag (applicative vs operative). +- [x] No hardcoded special forms — even `if`/`define`/`lambda` are env-bound. +- [x] Tests in `lib/kernel/tests/eval.sx`. ### Phase 3 — `$vau` / `$lambda` / `wrap` / `unwrap` -- [ ] Operative tagged value: `{:type :operative :params :env-param :body :static-env}`. -- [ ] Applicative tagged value wraps an operative + the "evaluate args first" contract. -- [ ] `$vau` builds operatives; `$lambda` is `wrap` ∘ `$vau`. -- [ ] `wrap` / `unwrap` round-trip cleanly. -- [ ] Tests: define a custom operative, define a custom applicative on top of it. +- [x] Operative tagged value: `{:type :operative :params :env-param :body :static-env}`. +- [x] Applicative tagged value wraps an operative + the "evaluate args first" contract. +- [x] `$vau` builds operatives; `$lambda` is `wrap` ∘ `$vau`. +- [x] `wrap` / `unwrap` round-trip cleanly. +- [x] Tests: define a custom operative, define a custom applicative on top of it. ### Phase 4 — Standard environment -- [ ] Standard env construction: bind `$if`, `$define!`, `$lambda`, `$vau`, `wrap`, `unwrap`, `eval`, `make-environment`, `get-current-environment`, plus arithmetic and list primitives. -- [ ] Tests: classic Kernel programs (factorial, list operations, environment manipulation). +- [x] Standard env construction: bind `$if`, `$define!`, `$lambda`, `$vau`, `wrap`, `unwrap`, `eval`, `make-environment`, `get-current-environment`, plus arithmetic and list primitives. +- [x] Tests: classic Kernel programs (factorial, list operations, environment manipulation). ### Phase 5 — Encapsulations -- [ ] `make-encapsulation-type` returns three operatives: encapsulator, predicate, decapsulator. Standard Kernel idiom for opaque types. -- [ ] Tests: implement promises, streams, or simple modules via encapsulations. +- [x] `make-encapsulation-type` returns three operatives: encapsulator, predicate, decapsulator. Standard Kernel idiom for opaque types. +- [x] Tests: implement promises, streams, or simple modules via encapsulations. ### Phase 6 — Hygienic operatives (Shutt's later work) -- [ ] Operatives that don't capture caller bindings — uses scope sets / frame stamps to track provenance. -- [ ] Bridge to SX's hygienic macro story; possibly extends `lib/guest/reflective/` with hygiene primitives. -- [ ] Tests: write an operative that introduces a binding and verify it doesn't shadow caller's same-named bindings. +- [x] Operatives that don't capture caller bindings — hygiene-by-default via static-env extension. Full scope-set / frame-stamp story is research-grade and documented but deferred. +- [x] Bridge to SX's hygienic macro story; extends proposed `lib/guest/reflective/` with `$let` and `$define-in!` hygiene primitives. +- [x] Tests: write an operative that introduces a binding and verify it doesn't shadow caller's same-named bindings. -### Phase 7 — Propose `lib/guest/reflective/` -- [ ] Once Phase 3 lands and stabilises, identify which env-reification + dispatch primitives are reusable. Candidate API: `make-operative`, `make-applicative`, `with-current-env`, `eval-in-env`. -- [ ] Find a second consumer (Common-Lisp's macro-expansion evaluator? a metacircular Scheme variant? a future plan). -- [ ] Only extract once two consumers exist (per stratification rule). +### Phase 7 — Propose `lib/guest/reflective/` *[partial — pending second consumer]* +- [x] Identified reusable env-reification + dispatch primitives across Phases 2–6. Consolidated API surface below as four candidate files: `env.sx`, `combiner.sx`, `evaluator.sx`, `hygiene.sx`. +- [ ] Find a second consumer (Common-Lisp's macro-expansion evaluator? a metacircular Scheme variant? a future plan). Until this lands, extraction is blocked by the two-consumer rule. +- [ ] Only extract once two consumers exist (per stratification rule). **Do not extract from this loop** — Kernel is one consumer; we need another before `lib/guest/reflective/` is real. + +**Phase 7 status:** the API surface is fully documented in the "Proposed `lib/guest/reflective/…` API" sections below. Candidate second consumers in priority order: + +1. **A metacircular Scheme** — Scheme can reuse `env.sx` directly (same scope semantics), borrow `evaluator.sx`'s eval/make-env/current-env triple, and pattern-match the `hygiene.sx` story (Scheme has identical lexical scope). Would NOT need `combiner.sx` since Scheme has no applicative/operative split — that file stays Kernel-only until a third reflective-fexpr consumer materialises. +2. **Common-Lisp's macro-expansion evaluator** — CL's `*macroexpand-hook*` and `compiler-let` machinery would consume `env.sx` (CL package envs map cleanly) and `evaluator.sx` (defmacro = an operative-like fexpr in expander phase). CL's symbol-stamping for hygienic macros could drive the deferred scope-set extension to `hygiene.sx`. +3. **A future Maru / Schemely port** — these languages have first-class fexprs and would use the whole kit verbatim. + +When the second consumer arrives, the extraction work is: rename `kernel-*` → `refl-*` in the relevant files, move into `lib/guest/reflective/`, update both consumers' references. Estimated <500 lines moved, since the bulk is already cleanly separated by responsibility in this loop's commits. ## lib/guest feedback loop @@ -100,15 +108,81 @@ The whole interesting thing: there are no special forms hardcoded in the evaluat **May propose:** `lib/guest/reflective/` sub-layer — environment manipulation, evaluator-as-value, applicative/operative dispatch protocols. +**Proposed `lib/guest/reflective/short-circuit.sx` API** (from $and?/$or? chiselling — pending second consumer): +- `(refl-short-and? ARGS DYN-ENV)` — recursive walker; evaluates each in DYN-ENV, returns first falsy value or last truthy. Identity is `true`. +- `(refl-short-or? ARGS DYN-ENV)` — symmetric; returns first truthy or last falsy. Identity is `false`. +- Both must be defined as operatives in any reflective Lisp because short-circuit semantics require staged evaluation — an applicative would force every argument before any decision could be made. +- Driving insight: short-circuit booleans are a forcing function for "operative semantics matter". Languages that lack first-class operatives have to special-case these as keywords; languages with operatives get them for free, in user code. + +**Proposed `lib/guest/reflective/quoting.sx` API** (from quasiquote chiselling — pending second consumer): +- `(refl-quasi-walk FORM ENV)` — top-level entry. Recursively walks FORM; an `$unquote` sub-expression is evaluated in ENV and replaces itself in the result. +- `(refl-quasi-walk-list FORMS ENV)` — walks a list of forms, splicing `$unquote-splicing` results inline. +- `(refl-list-concat XS YS)` — pure-SX list concatenation (no host dependency on `append`). +- Driving insight: every reflective Lisp eventually adds quasiquote, and the recursion-with-splicing structure is identical across them. Nesting depth tracking (for `` ``e `` inside `` `e ``) is the only Kernel-specific complication; for the kit, a depth-tracking variant `refl-quasi-walk-depth FORM ENV DEPTH` would be the second-tier API. + +**Proposed `lib/guest/reflective/hygiene.sx` API** (from Phase 6 chiselling — pending second consumer): +- The substrate decision: a user-defined combiner's body runs in `(extend STATIC-ENV)`, NOT in the dyn-env. Any `$define!` inside the body binds in this fresh child, so callers' envs stay untouched. This is the cheap, lexical-scope hygiene story that R-1RK has had since the start. +- `(refl-let BINDINGS BODY)` — bind names in a fresh child of dyn-env, evaluate body there. Values evaluated in OUTER env (parallel semantics). +- `(refl-define-in! ENV NAME EXPR)` — explicit-target bind. The operative that wants to mutate someone else's env says so explicitly. +- Full scope-set / frame-stamp hygiene (Shutt's later work, Racket-style) is research-grade and not implemented. The pieces would include: lifted symbols carrying a stamp set, `refl-introduce-symbol` to create a fresh-stamp name, `refl-symbol=?` that compares names *and* stamps. This belongs in a future Phase 7+ extraction once a second consumer wants it. + +**Proposed `lib/guest/reflective/evaluator.sx` API** (from Phase 4 chiselling — pending second consumer): +- `(refl-eval EXPR ENV)` — the primary entry. Used to be implicit; exposing it as a function lets guests call into their own evaluator. +- `(refl-make-environment [PARENT])` — fresh evaluation context, optionally a child of an existing one. +- `(refl-current-env-operative)` — a Kernel-shaped operative that returns the dyn-env when called. Other reflective languages will need the same mechanism (an operative-equivalent that exposes "the env at this point"). +- Driving insight: the eval/make-env/current-env triple IS the reflective evaluator interface. Every reflective Lisp eventually exposes these three. Even more so when you start needing macro-expansion-time vs run-time vs call-time envs (the Kernel hygienic operatives work in Phase 6 will reveal whether more `refl-env-at-foo-time` accessors should join the kit). + +**Proposed `lib/guest/reflective/combiner.sx` API** (from Phase 3 chiselling — pending second consumer): +- `(refl-make-primitive-operative IMPL)` — IMPL receives `(args dyn-env)`, args unevaluated. +- `(refl-make-user-operative PARAMS EPARAM BODY STATIC-ENV)` — for $vau-like constructors. The EPARAM sentinel for "ignore dyn-env" is a fixed keyword (`:refl-ignore` in the proposal). +- `(refl-make-primitive-applicative-with-env IMPL)` — like `refl-make-primitive-applicative` but IMPL receives `(args dyn-env)`. Used by combinators that re-enter the evaluator: `map`, `filter`, `reduce`, `apply`, `eval`, dynamic `call-with-current-environment`. Universal across reflective Lisps because such combinators MUST capture the caller's env to honor dynamic scoping. +- `(refl-apply-op COMBINER)` — if COMBINER is an applicative, returns its underlying operative; otherwise returns COMBINER unchanged. Critical helper for combinators that call user-supplied functions with already-evaluated values: passing values to an applicative would re-evaluate them (numbers/strings pass through, but lists get treated as calls). Every reflective Lisp has discovered this bug; the unwrap-then-combine pattern is the fix. Surfaced by the Kernel-on-SX metacircular demo when nested-list elements crashed map. +- `(refl-wrap OP)` / `(refl-unwrap APP)` — round-trip pair. +- `(refl-operative? V)` / `(refl-applicative? V)` / `(refl-combiner? V)`. +- `(refl-call-combiner COMBINER ARGS DYN-ENV)` — the dispatch fork. Pairs with `refl-eval` from the evaluator kit. +- Representation: `{:refl-tag :operative :impl FN}` or `{:refl-tag :operative :params P :env-param EP :body B :static-env SE}`; applicatives are `{:refl-tag :applicative :underlying OP}`. The dispatch decision lives in one fork: presence of `:impl` is primitive, presence of `:body` is user-defined. +- Driving insight: every reflective Lisp must distinguish "eval my args first" from "hand me the syntax". The tag protocol is identical across Kernel, CL fexprs, vau-style Schemes, possibly Forth's IMMEDIATE words. + +**Proposed `lib/guest/reflective/env.sx` API** (from Phase 2 chiselling — pending second consumer per the two-consumer rule): +- `(refl-make-env)` / `(refl-extend-env PARENT)` — fresh / chained envs, plain SX dicts so they're easy to introspect. +- `(refl-env? V)` — predicate. +- `(refl-env-bind! ENV NAME VAL)` — local bind; parent is untouched. +- `(refl-env-has? ENV NAME)` — recursive presence check. +- `(refl-env-lookup ENV NAME)` — recursive lookup, raises on miss. +- Representation: `{:refl-tag :env :bindings DICT :parent ENV-OR-NIL}`. Pure-SX dicts so any guest can serialize, diff, snapshot, or rewind environments without help from the host. + +The motivation is that SX's host `make-env` family is registered only in HTTP/site-mode platform setup, so a guest that needs first-class envs in CLI / test contexts has to roll its own anyway. A shared kit means the next reflective consumer (CL macro evaluator? metacircular Scheme?) doesn't need to redo the work. + **What it teaches:** whether SX's recent env-as-value direction generalises to "evaluator-as-value." If Kernel implements cleanly in <2000 lines, env-as-value is real. If it requires substrate fixes at every turn, env-as-value was incomplete and the substrate is telling us what's missing. +**Actual finding (post-loop):** Kernel-on-SX is **1,398 lines** (parser 253 + eval 234 + runtime 911), with **1,747 lines** of tests for **322 passing tests**. Zero substrate fixes were required across 18 commits. The only substrate-shaped friction was that the host's `make-env` family is registered in HTTP/site mode but not CLI mode, so Kernel models envs in pure SX as `{:knl-tag :env :bindings DICT :parent P}` — but that turned out to be a *feature*: it forced the env representation into something serializable, introspectable, and host-agnostic, which is exactly what the proposed `lib/guest/reflective/env.sx` should look like. **Env-as-value generalises to evaluator-as-value.** The Kernel-in-Kernel `m-eval` demo proves it: a Kernel program reproduces enough of Kernel's evaluation semantics that the only thing left for the host to provide is symbol lookup and operative dispatch — both already first-class. The chisel notes accumulated four reflective-API candidate files (`env.sx`, `combiner.sx`, `evaluator.sx`, `hygiene.sx`, `quoting.sx`, `short-circuit.sx`) which are documented in this plan and awaiting a second consumer per the two-consumer stratification rule. + ## References - Shutt, "Fexprs as the basis of Lisp function application" (PhD thesis, 2010). - Kernel Report (R-1RK): https://web.cs.wpi.edu/~jshutt/kernel.html - Klisp implementation (Andres Navarro) — pragmatic reference. ## Progress log -_(awaiting Phase 1 — depends on stable env-as-value substrate state)_ + +- 2026-05-11 — Loop summary (no code change). After 18 feature commits across two days, the Kernel-on-SX implementation totals **1,398 lines** of substrate (parser/eval/runtime), **1,747 lines** of tests, **322 passing tests** in **7 test suites**. Zero substrate fixes required. R-1RK core fully implemented (parser, evaluator, $vau/$lambda/wrap/unwrap, standard env, encapsulations, hygiene helpers) plus extras (reader macros, multi-expression body, quasiquote runtime, $cond/$when/$unless/$and?/$or?/$let*, variadic arithmetic, map/filter/reduce/apply/append/reverse, type predicates, metacircular demo). The chisel discipline accumulated **six proposed `lib/guest/reflective/` files**: `env.sx`, `combiner.sx`, `evaluator.sx`, `hygiene.sx`, `quoting.sx`, `short-circuit.sx` — all sketched with signatures, all gated on a second consumer per the two-consumer stratification rule. Notably, the substrate's env-as-value direction *does* generalise to evaluator-as-value: the Kernel-in-Kernel `m-eval` demo proves it. The next phase of value (whenever it happens) is finding that second consumer — a metacircular Scheme, a CL meta-evaluator, or a Maru port — and extracting the reflective kit. +- 2026-05-11 — Type predicates + metacircular evaluator demo + map/filter/reduce bug fix. Five new applicatives: `number?`, `string?` (which doubles as `symbol?`), `list?`, `boolean?`, `symbol?`. New test file `tests/metacircular.sx`: a Kernel program `m-eval` that walks expressions, recursively meta-evaluates sub-expressions of applicative calls, and delegates to host `eval` for symbol lookup and operatives. 14 tests showing m-eval handles literals, arithmetic, list construction, $if branches via delegation, and user-defined lambdas. **Substantive bug fix surfaced by the demo**: `map`, `filter`, `reduce` were calling `kernel-combine` directly with applicatives, which then re-evaluated the already-evaluated element values; nested-list elements crashed with "not a combiner". Fix: unwrap the applicative first (mirrors `apply`'s approach). New helper `knl-apply-op` for the unwrap-if-applicative pattern, used by all three combinators. chisel: shapes-reflective. **Two reflective findings**: (1) `knl-apply-op` (unwrap-applicative-or-pass-through) is a universal helper that any reflective combinator needs — proposed for the `combiner.sx` API. (2) The metacircular demo proves the substrate is reflective-complete in the meaningful sense: a Kernel program *can* implement a non-trivial subset of Kernel's evaluation semantics, calling back into the host evaluator only for operatives and lookup. 322 tests total. +- 2026-05-11 — `append` (variadic) and `reverse`. Append concatenates any number of lists; empty `(append)` returns `()`. Reverse is unary. 11 new tests. chisel: nothing (textbook list ops). 307 tests total. +- 2026-05-11 — `apply` combinator. `(apply F (list V1 V2 V3))` ≡ `(F V1 V2 V3)` but with the argument list constructed at runtime. Implementation: unwrap an applicative F to its underlying operative, then `kernel-combine` it with the values — skipping the auto-eval pass since args are already values. For a bare operative F, pass through directly. 7 new tests. chisel: shapes-reflective. The unwrap-then-combine pattern is universal across reflective Lisps and should be in the `combiner.sx` API alongside the existing wrap/unwrap pair: `refl-apply F ARGS DYN-ENV` is the third API entry needed for higher-order composition. 296 tests total. +- 2026-05-11 — `map` / `filter` / `reduce` list combinators. Required adding `kernel-make-primitive-applicative-with-env` to `eval.sx`: standard primitive applicatives drop dyn-env, but combinators that re-enter the evaluator (calling user-supplied functions on each element) need it. The three combinators use `kernel-combine` directly with the captured dyn-env. 10 new tests covering map/filter/reduce on numbers, empty lists, closures, and list construction. chisel: shapes-reflective. The "primitive applicatives split into two flavours — env-blind and env-aware" finding goes into the proposed `lib/guest/reflective/combiner.sx` API. Every reflective Lisp must distinguish "I just need values" from "I need to re-enter evaluation" — the with-env constructor pair is universal. 289 tests total. +- 2026-05-11 — Variadic `+ - * /` and chained `< > <=? >=?`. `(+ 1 2 3)` = 6, `(+)` = 0, `(+ 7)` = 7. `(- 10 1 2 3)` = 4 (left fold); single-arg `-` negates. `(* 1 2 3 4)` = 24, `(*)` = 1. Chained comparison: `(< 1 2 3)` ≡ `(< 1 2) ∧ (< 2 3)`. Implementation: `knl-fold-app` for n-ary fold with zero-arity identity and one-arity special-case; `knl-chain-cmp` for chained boolean. 19 new tests. chisel: nothing (mechanical extension of existing arithmetic primitives). 279 tests total. +- 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. +- 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. +- 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. +- 2026-05-11 — Phase 4 standard env landed. `kernel-standard-env` extends `kernel-base-env` with: control (`$if`, `$define!`, `$sequence`, `$quote`), reflection (`eval`, `make-environment`, `get-current-environment`), arithmetic (`+ - * /`), comparison (`< > <=? >=? =? eq? equal?`), list/pair (`cons car cdr list length null? pair?`), boolean (`not`). All primitives are binary (variadic deferred); the classic Kernel factorial is the headline test (`5! = 120`, `10! = 3628800`). 49 tests in `tests/standard.sx`, covering $if branching, $define! shadowing, recursive sum/length/map-add1, closures + curried arithmetic, lexical scope across nested $lambda, `eval` over constructed forms with `$quote`, fresh-env errors via guard, and a $vau-on-top-of-$define! example. chisel: shapes-reflective. Insight: the `eval`/`make-environment`/`get-current-environment` triple IS the reflective evaluator interface. Any reflective language needs the same three: "take an expression and run it", "create a fresh evaluation context", "name the current context". That goes in the proposed `lib/guest/reflective/evaluator.sx` candidate. Second chisel — `$define!` was a one-liner because env-bind! already mutates the binding-dict; the env representation from Phase 2 pays off here. +- 2026-05-11 — Phase 3 operatives landed. `lib/kernel/runtime.sx` adds `$vau` (primitive operative that returns a user operative), `$lambda` (sugar for `wrap ∘ $vau`), `wrap` and `unwrap` (Kernel-level applicatives), plus `operative?` and `applicative?` predicates. `kernel-base-env` wires them all into a fresh env. `kernel-eval.sx` now dispatches in `kernel-call-operative` between primitive ops (carry `:impl`) and user ops (carry `:params :env-param :body :static-env`). Parameter binding is a flat list — destructuring/`&rest` deferred. Env-param sentinel: spell `_` or `#ignore` → `:knl-ignore`, which skips the dyn-env bind. 34 tests in `tests/vau.sx`, including the headline custom-operative + custom-applicative composition. chisel: shapes-reflective. Two further reflective-API candidates surfaced: (a) the operative/applicative tag protocol — `make-primitive-operative`, `make-user-operative`, `wrap`, `unwrap` are general for any Lisp-of-fexprs; (b) the call-dispatch fork (primitive vs user) is a *single decision* that every reflective evaluator hits. Both shape go into the proposed `lib/guest/reflective/combiner.sx` candidate. +- 2026-05-10 — Phase 2 evaluator landed. `lib/kernel/eval.sx` is `lookup-and-combine`: zero hardcoded special forms. `kernel-eval EXPR ENV` dispatches on shape — literals self-evaluate, Kernel strings unwrap, symbols lookup, lists evaluate head and combine. `kernel-combine` distinguishes operatives (impl receives un-evaluated args + dynamic env) from applicatives (eval args, recurse into underlying op). `kernel-wrap`/`kernel-unwrap` round-trip cleanly. 36 tests verify literal evaluation, symbol lookup with parent-chain shadowing, tagged-value predicates, and the operative-vs-applicative contract (notably `$if` only evaluates the chosen branch, `$quote` returns its arg unevaluated). chisel: shapes-reflective. Substrate gap surfaced: SX's `make-env` / `env-bind!` family is only registered in HTTP/site mode (`http_setup_platform_constructors`), not in CLI epoch mode used for tests. So Kernel envs are modelled in pure SX as `{:knl-tag :env :bindings DICT :parent P}` — a binding-dict + parent-pointer + recursive lookup walk. This is exactly the `lib/guest/reflective/env.sx` candidate API: any reflective language needs first-class env values that can be extended, queried, and walked. Recording the shape (constructor, extend, bind!, has?, lookup) here for the eventual Phase 7 extraction. +- 2026-05-10 — Phase 1 parser landed. `lib/kernel/parser.sx` reads R-1RK lexical syntax: numbers (int/float/exp), strings (with escapes), symbols (permissive — anything non-delimiting), booleans `#t`/`#f`, the empty list `()`, nested lists, and `;` line comments. Reader macros (`'` `,` `,@`) deferred per plan. AST: numbers/booleans/lists pass through; strings are wrapped as `{:knl-string …}` to distinguish from symbols which are bare SX strings. 54 tests in `lib/kernel/tests/parse.sx` pass via `sx_server.exe` epoch protocol. chisel: consumes-lex (uses `lex-digit?` and `lex-whitespace?` from `lib/guest/lex.sx` — pratt deliberately not consumed because Kernel is plain s-expressions, no precedence climbing). ## Blockers _(none yet — main risk is substrate gap discovery during Phase 2)_