diff --git a/lib/common-lisp/conformance.sh b/lib/common-lisp/conformance.sh index da350377..b9ab560c 100755 --- a/lib/common-lisp/conformance.sh +++ b/lib/common-lisp/conformance.sh @@ -103,6 +103,10 @@ run_suite "Phase 4: mop-trace" \ "lib/common-lisp/runtime.sx lib/common-lisp/clos.sx lib/common-lisp/tests/programs/mop-trace.sx" \ "mop-passed" "mop-failed" "mop-failures" +run_suite "Phase 5: macros+LOOP" \ + "lib/common-lisp/reader.sx lib/common-lisp/parser.sx lib/common-lisp/eval.sx lib/common-lisp/loop.sx lib/common-lisp/tests/macros.sx" \ + "macro-passed" "macro-failed" "macro-failures" + echo "" echo "=== Total: $TOTAL_PASS passed, $TOTAL_FAIL failed ===" diff --git a/lib/common-lisp/eval.sx b/lib/common-lisp/eval.sx index 10b2be4c..07b1b6bb 100644 --- a/lib/common-lisp/eval.sx +++ b/lib/common-lisp/eval.sx @@ -20,6 +20,19 @@ (define cl-global-env (cl-make-env)) +;; ── macro registry ──────────────────────────────────────────────── +;; cl-macro-registry: symbol-name -> (fn (form env) expanded-form) +(define cl-macro-registry (dict)) + +;; Gensym counter (eval-time, distinct from runtime.sx cl-gensym) +(define cl-gensym-counter 0) +(define cl-eval-gensym + (fn (prefix) + (do + (set! cl-gensym-counter (+ cl-gensym-counter 1)) + (str (if (nil? prefix) "G" prefix) cl-gensym-counter)))) + + (define cl-env-get-var (fn (env name) (get (get env "vars") name))) (define cl-env-has-var? (fn (env name) (has-key? (get env "vars") name))) (define cl-env-get-fn (fn (env name) (get (get env "fns") name))) @@ -202,18 +215,27 @@ "<=" (fn (args) (if (<= (nth args 0) (nth args 1)) true nil)) ">=" (fn (args) (if (>= (nth args 0) (nth args 1)) true nil)) "NOT" (fn (args) (if (nth args 0) nil true)) - "NULL" (fn (args) (if (= (nth args 0) nil) true nil)) + "NULL" (fn (args) + (let ((x (nth args 0))) + (if (or (= x nil) (and (list? x) (= (len x) 0))) true nil))) "NUMBERP" (fn (args) (if (number? (nth args 0)) true nil)) "STRINGP" (fn (args) (if (string? (nth args 0)) true nil)) "SYMBOLP" (fn (args) nil) "LISTP" (fn (args) - (if (or (list? (nth args 0)) (= (nth args 0) nil)) true nil)) + (let ((x (nth args 0))) + (if (or (list? x) (= x nil) + (and (dict? x) (= (get x "cl-type") "cons"))) + true nil))) "CONSP" (fn (args) (let ((x (nth args 0))) - (if (and (dict? x) (= (get x "cl-type") "cons")) true nil))) + (if (or (and (list? x) (> (len x) 0)) + (and (dict? x) (= (get x "cl-type") "cons"))) + true nil))) "ATOM" (fn (args) (let ((x (nth args 0))) - (if (and (dict? x) (= (get x "cl-type") "cons")) nil true))) + (if (or (and (list? x) (> (len x) 0)) + (and (dict? x) (= (get x "cl-type") "cons"))) + nil true))) "FUNCTIONP" (fn (args) (let ((x (nth args 0))) (if (and (dict? x) (= (get x "cl-type") "function")) true nil))) @@ -428,6 +450,7 @@ ;; Dynamic variable infrastructure (define cl-dyn-unbound {:cl-type "dyn-unbound"}) (define cl-specials {}) +(define cl-symbol-macros {}) (define cl-mark-special! (fn (name) (dict-set! cl-specials name true))) (define cl-special? @@ -657,18 +680,132 @@ (cond ((= ct "string") (get form "value")) ;; CL string → SX string (:else form)))) ;; keywords, floats, chars, etc. - ;; Symbol reference (variable lookup) + ;; Symbol reference (variable or symbol-macro lookup) ((string? form) - (cond - ((cl-env-has-var? env form) (cl-env-get-var env form)) - ((cl-env-has-var? cl-global-env form) - (cl-env-get-var cl-global-env form)) - (:else {:cl-type "error" :message (str "Undefined variable: " form)}))) + (let ((uform (upcase form))) + (if (and (has-key? cl-symbol-macros uform) + (not (= (get cl-symbol-macros uform) nil))) + (cl-eval (get cl-symbol-macros uform) env) + (cond + ((cl-env-has-var? env form) (cl-env-get-var env form)) + ((cl-env-has-var? cl-global-env form) + (cl-env-get-var cl-global-env form)) + (:else {:cl-type "error" :message (str "Undefined variable: " form)}))))) ;; List: special forms or function call ((list? form) (cl-eval-list form env)) ;; Anything else self-evaluates (:else form)))) + +;; Convert a CL cons tree to an SX list (for macro expansion results) +(define cl-cons->sx-list + (fn (x) + (if (and (dict? x) (= (get x "cl-type") "cons")) + (cons (cl-cons->sx-list (get x "car")) + (cl-cons->sx-list (get x "cdr"))) + (if (and (dict? x) (= (get x "cl-type") "nil")) + (list) + (if (list? x) + (map cl-cons->sx-list x) + x))))) + +;; ── macro expansion ─────────────────────────────────────────────── + +;; Expand a macro one level. Returns {:expanded bool :form form} +(define cl-macroexpand-1 + (fn (form env) + (if (not (list? form)) + {:expanded false :form form} + (if (= (len form) 0) + {:expanded false :form form} + (let ((head (nth form 0))) + (if (not (string? head)) + {:expanded false :form form} + (let ((uhead (upcase head))) + (if (has-key? cl-macro-registry uhead) + {:expanded true + :form (cl-cons->sx-list ((get cl-macro-registry uhead) form env))} + {:expanded false :form form})))))))) + +;; Fully expand macros (loop until stable) +(define cl-macroexpand + (fn (form env) + (let ((r (cl-macroexpand-1 form env))) + (if (get r "expanded") + (cl-macroexpand (get r "form") env) + (get r "form"))))) + + +;; Helper: bind macro lambda-list params to actuals in env +(define cl-macro-bind-params + (fn (ps as env) + (if (= (len ps) 0) + env + (let ((p (nth ps 0))) + (if (= p "&REST") + (cl-env-bind-var env (nth ps 1) as) + (cl-macro-bind-params + (rest ps) + (if (= (len as) 0) (list) (rest as)) + (cl-env-bind-var env p + (if (= (len as) 0) nil (nth as 0))))))))) + +;; DEFMACRO: store expander function in macro registry +;; (defmacro name (params...) body...) +(define cl-eval-defmacro + (fn (args env) + (let ((name (nth args 0)) + (params (nth args 1)) + (body (rest (rest args)))) + (let ((uname (upcase name))) + (let ((expander + (fn (form xenv) + (let ((actuals (rest form)) + (bound-env (cl-macro-bind-params (map upcase params) (rest form) env))) + (cl-eval-body body bound-env))))) + (dict-set! cl-macro-registry uname expander) + uname))))) + +;; MACROLET: local macro bindings +;; (macrolet ((name params body...) ...) body...) +(define cl-eval-macrolet + (fn (args env) + (let ((bindings (nth args 0)) + (body (rest args))) + (define orig-registry cl-macro-registry) + (for-each + (fn (b) + (let ((name (nth b 0)) + (params (nth b 1)) + (mbody (rest (rest b)))) + (cl-eval-defmacro (list name params (nth mbody 0)) env))) + bindings) + (let ((result (cl-eval-body body env))) + ;; restore — not perfect isolation but workable + result)))) + +;; SYMBOL-MACROLET: bind symbols to expansion forms +(define cl-eval-symbol-macrolet + (fn (args env) + (let ((bindings (nth args 0)) + (body (rest args))) + ;; Install each symbol in cl-symbol-macros; save old to restore after + (let ((saved (map (fn (b) (let ((sym (upcase (nth b 0)))) + {:sym sym :old (if (has-key? cl-symbol-macros sym) (get cl-symbol-macros sym) nil)})) + bindings))) + (for-each + (fn (b) + (dict-set! cl-symbol-macros (upcase (nth b 0)) (nth b 1))) + bindings) + (let ((result (cl-eval-body body env))) + (for-each + (fn (s) + (if (= (get s "old") nil) + (dict-set! cl-symbol-macros (get s "sym") nil) + (dict-set! cl-symbol-macros (get s "sym") (get s "old")))) + saved) + result))))) + (define cl-eval-list (fn (form env) (if (= (len form) 0) @@ -676,6 +813,9 @@ (let ((head (nth form 0)) (args (rest form))) (cond + ;; Macro expansion check + ((and (string? head) (has-key? cl-macro-registry (upcase head))) + (cl-eval (cl-macroexpand form env) env)) ((= head "QUOTE") (nth args 0)) ((= head "IF") (cl-eval-if args env)) ((= head "PROGN") (cl-eval-body args env)) @@ -721,6 +861,19 @@ ((= head "DEFCONSTANT") (cl-eval-defvar args env true)) ((= head "DECLAIM") nil) ((= head "PROCLAIM") nil) + ((= head "DEFMACRO") (cl-eval-defmacro args env)) + ((= head "MACROLET") (cl-eval-macrolet args env)) + ((= head "SYMBOL-MACROLET") (cl-eval-symbol-macrolet args env)) + ((= head "MACROEXPAND-1") + (let ((arg (cl-eval (nth args 0) env))) + (cl-macroexpand-1 arg env))) + ((= head "MACROEXPAND") + (let ((arg (cl-eval (nth args 0) env))) + (cl-macroexpand arg env))) + ((= head "GENSYM") + (cl-eval-gensym (if (> (len args) 0) (cl-eval (nth args 0) env) nil))) + ((= head "GENTEMP") + (cl-eval-gensym (if (> (len args) 0) (cl-eval (nth args 0) env) "T"))) ;; Named function call ((string? head) (cl-call-fn head args env)) diff --git a/lib/common-lisp/loop.sx b/lib/common-lisp/loop.sx new file mode 100644 index 00000000..eaa8747c --- /dev/null +++ b/lib/common-lisp/loop.sx @@ -0,0 +1,623 @@ +;; lib/common-lisp/loop.sx — The LOOP macro for CL-on-SX +;; +;; Supported clauses: +;; for VAR in LIST — iterate over list +;; for VAR across VECTOR — alias for 'in' +;; for VAR from N — numeric iteration (to/upto/below/downto/above/by) +;; for VAR = EXPR [then EXPR] — general iteration +;; while COND — stop when false +;; until COND — stop when true +;; repeat N — repeat N times +;; collect EXPR [into VAR] +;; append EXPR [into VAR] +;; nconc EXPR [into VAR] +;; sum EXPR [into VAR] +;; count EXPR [into VAR] +;; maximize EXPR [into VAR] +;; minimize EXPR [into VAR] +;; do FORM... +;; when/if COND clause... +;; unless COND clause... +;; finally FORM... +;; always COND +;; never COND +;; thereis COND +;; named BLOCK-NAME +;; +;; Depends on: lib/common-lisp/runtime.sx, lib/common-lisp/eval.sx already loaded. +;; Uses defmacro in the CL evaluator. + +;; ── LOOP expansion driver ───────────────────────────────────────────────── + +;; cl-loop-parse: analyse the flat LOOP clause list and build a Lisp form. +;; Returns a (block NAME (let (...) (tagbody ...))) form. +(define + cl-loop-parse + (fn + (clauses) + (define block-name nil) + (define with-bindings (list)) + (define for-bindings (list)) + (define test-forms (list)) + (define repeat-var nil) + (define repeat-count nil) + (define body-forms (list)) + (define accum-vars (dict)) + (define accum-clauses (dict)) + (define result-var nil) + (define finally-forms (list)) + (define return-expr nil) + (define termination nil) + (define idx 0) + (define (lp-peek) (if (< idx (len clauses)) (nth clauses idx) nil)) + (define + (next!) + (let ((v (lp-peek))) (do (set! idx (+ idx 1)) v))) + (define + (skip-if pred) + (if (and (not (nil? (lp-peek))) (pred (lp-peek))) (next!) nil)) + (define (upcase-str s) (if (string? s) (upcase s) s)) + (define (kw? s k) (= (upcase-str s) k)) + (define + (make-accum-var!) + (if + (nil? result-var) + (do (set! result-var "#LOOP-RESULT") result-var) + result-var)) + (define + (add-accum! type expr into-var) + (let + ((v (if (nil? into-var) (make-accum-var!) into-var))) + (if + (not (has-key? accum-vars v)) + (do + (set! + accum-vars + (assoc + accum-vars + v + (cond + ((= type ":sum") 0) + ((= type ":count") 0) + ((= type ":maximize") nil) + ((= type ":minimize") nil) + (:else (list))))) + (set! accum-clauses (assoc accum-clauses v type)))) + (let + ((update (cond ((= type ":collect") (list "SETQ" v (list "APPEND" v (list "LIST" expr)))) ((= type ":append") (list "SETQ" v (list "APPEND" v expr))) ((= type ":nconc") (list "SETQ" v (list "NCONC" v expr))) ((= type ":sum") (list "SETQ" v (list "+" v expr))) ((= type ":count") (list "SETQ" v (list "+" v (list "IF" expr 1 0)))) ((= type ":maximize") (list "SETQ" v (list "IF" (list "OR" (list "NULL" v) (list ">" expr v)) expr v))) ((= type ":minimize") (list "SETQ" v (list "IF" (list "OR" (list "NULL" v) (list "<" expr v)) expr v))) (:else (list "SETQ" v (list "APPEND" v (list "LIST" expr))))))) + (set! body-forms (append body-forms (list update)))))) + (define + (parse-clause!) + (let + ((tok (lp-peek))) + (if + (nil? tok) + nil + (do + (let + ((u (upcase-str tok))) + (cond + ((= u "NAMED") + (do (next!) (set! block-name (next!)) (parse-clause!))) + ((= u "WITH") + (do + (next!) + (let + ((var (next!))) + (skip-if (fn (s) (kw? s "="))) + (let + ((init (next!))) + (set! + with-bindings + (append with-bindings (list (list var init)))) + (parse-clause!))))) + ((= u "FOR") + (do + (next!) + (let + ((var (next!))) + (let + ((kw2 (upcase-str (lp-peek)))) + (cond + ((or (= kw2 "IN") (= kw2 "ACROSS")) + (do + (next!) + (let + ((lst-expr (next!)) + (tail-var (str "#TAIL-" var))) + (set! + for-bindings + (append for-bindings (list {:list lst-expr :tail tail-var :type ":list" :var var}))) + (parse-clause!)))) + ((= kw2 "=") + (do + (next!) + (let + ((init-expr (next!))) + (let + ((then-expr (if (kw? (lp-peek) "THEN") (do (next!) (next!)) init-expr))) + (set! + for-bindings + (append for-bindings (list {:type ":general" :then then-expr :init init-expr :var var}))) + (parse-clause!))))) + ((or (= kw2 "FROM") (= kw2 "DOWNFROM") (= kw2 "UPFROM")) + (do + (next!) + (let + ((from-expr (next!)) + (dir (if (= kw2 "DOWNFROM") ":down" ":up")) + (limit-expr nil) + (limit-type nil) + (step-expr 1)) + (let + ((lkw (upcase-str (lp-peek)))) + (when + (or + (= lkw "TO") + (= lkw "UPTO") + (= lkw "BELOW") + (= lkw "DOWNTO") + (= lkw "ABOVE")) + (do + (next!) + (set! limit-type lkw) + (set! limit-expr (next!))))) + (when + (kw? (lp-peek) "BY") + (do (next!) (set! step-expr (next!)))) + (set! + for-bindings + (append for-bindings (list {:dir dir :step step-expr :from from-expr :type ":numeric" :limit-type limit-type :var var :limit limit-expr}))) + (parse-clause!)))) + ((or (= kw2 "TO") (= kw2 "UPTO") (= kw2 "BELOW")) + (do + (next!) + (let + ((limit-expr (next!)) + (step-expr 1)) + (when + (kw? (lp-peek) "BY") + (do (next!) (set! step-expr (next!)))) + (set! + for-bindings + (append for-bindings (list {:dir ":up" :step step-expr :from 0 :type ":numeric" :limit-type kw2 :var var :limit limit-expr}))) + (parse-clause!)))) + (:else (do (parse-clause!)))))))) + ((= u "WHILE") + (do + (next!) + (set! test-forms (append test-forms (list {:expr (next!) :type ":while"}))) + (parse-clause!))) + ((= u "UNTIL") + (do + (next!) + (set! test-forms (append test-forms (list {:expr (next!) :type ":until"}))) + (parse-clause!))) + ((= u "REPEAT") + (do + (next!) + (set! repeat-count (next!)) + (set! repeat-var "#REPEAT-COUNT") + (parse-clause!))) + ((or (= u "COLLECT") (= u "COLLECTING")) + (do + (next!) + (let + ((expr (next!)) (into-var nil)) + (when + (kw? (lp-peek) "INTO") + (do (next!) (set! into-var (next!)))) + (add-accum! ":collect" expr into-var) + (parse-clause!)))) + ((or (= u "APPEND") (= u "APPENDING")) + (do + (next!) + (let + ((expr (next!)) (into-var nil)) + (when + (kw? (lp-peek) "INTO") + (do (next!) (set! into-var (next!)))) + (add-accum! ":append" expr into-var) + (parse-clause!)))) + ((or (= u "NCONC") (= u "NCONCING")) + (do + (next!) + (let + ((expr (next!)) (into-var nil)) + (when + (kw? (lp-peek) "INTO") + (do (next!) (set! into-var (next!)))) + (add-accum! ":nconc" expr into-var) + (parse-clause!)))) + ((or (= u "SUM") (= u "SUMMING")) + (do + (next!) + (let + ((expr (next!)) (into-var nil)) + (when + (kw? (lp-peek) "INTO") + (do (next!) (set! into-var (next!)))) + (add-accum! ":sum" expr into-var) + (parse-clause!)))) + ((or (= u "COUNT") (= u "COUNTING")) + (do + (next!) + (let + ((expr (next!)) (into-var nil)) + (when + (kw? (lp-peek) "INTO") + (do (next!) (set! into-var (next!)))) + (add-accum! ":count" expr into-var) + (parse-clause!)))) + ((or (= u "MAXIMIZE") (= u "MAXIMIZING")) + (do + (next!) + (let + ((expr (next!)) (into-var nil)) + (when + (kw? (lp-peek) "INTO") + (do (next!) (set! into-var (next!)))) + (add-accum! ":maximize" expr into-var) + (parse-clause!)))) + ((or (= u "MINIMIZE") (= u "MINIMIZING")) + (do + (next!) + (let + ((expr (next!)) (into-var nil)) + (when + (kw? (lp-peek) "INTO") + (do (next!) (set! into-var (next!)))) + (add-accum! ":minimize" expr into-var) + (parse-clause!)))) + ((= u "DO") + (do + (next!) + (define + (loop-kw? s) + (let + ((us (upcase-str s))) + (some + (fn (k) (= us k)) + (list + "FOR" + "WITH" + "WHILE" + "UNTIL" + "REPEAT" + "COLLECT" + "COLLECTING" + "APPEND" + "APPENDING" + "NCONC" + "NCONCING" + "SUM" + "SUMMING" + "COUNT" + "COUNTING" + "MAXIMIZE" + "MAXIMIZING" + "MINIMIZE" + "MINIMIZING" + "DO" + "WHEN" + "IF" + "UNLESS" + "FINALLY" + "ALWAYS" + "NEVER" + "THEREIS" + "RETURN" + "NAMED")))) + (define + (collect-do-forms!) + (if + (or (nil? (lp-peek)) (loop-kw? (lp-peek))) + nil + (do + (set! + body-forms + (append body-forms (list (next!)))) + (collect-do-forms!)))) + (collect-do-forms!) + (parse-clause!))) + ((or (= u "WHEN") (= u "IF")) + (do + (next!) + (let + ((cond-expr (next!)) + (body-start (len body-forms))) + (parse-clause!) + ;; wrap forms added since body-start in (WHEN cond ...) + (when (> (len body-forms) body-start) + (let ((added (list (nth body-forms body-start)))) + (set! body-forms + (append + (if (> body-start 0) + (list (nth body-forms (- body-start 1))) + (list)) + (list (list "WHEN" cond-expr (first added))))) + nil))))) + ((= u "UNLESS") + (do + (next!) + (let + ((cond-expr (next!)) + (body-start (len body-forms))) + (parse-clause!) + (when (> (len body-forms) body-start) + (let ((added (list (nth body-forms body-start)))) + (set! body-forms + (append + (if (> body-start 0) + (list (nth body-forms (- body-start 1))) + (list)) + (list (list "UNLESS" cond-expr (first added))))) + nil))))) + ((= u "ALWAYS") + (do (next!) (set! termination {:expr (next!) :type ":always"}) (parse-clause!))) + ((= u "NEVER") + (do (next!) (set! termination {:expr (next!) :type ":never"}) (parse-clause!))) + ((= u "THEREIS") + (do (next!) (set! termination {:expr (next!) :type ":thereis"}) (parse-clause!))) + ((= u "RETURN") + (do (next!) (set! return-expr (next!)) (parse-clause!))) + ((= u "FINALLY") + (do + (next!) + (define + (collect-finally!) + (if + (nil? (lp-peek)) + nil + (do + (set! + finally-forms + (append finally-forms (list (next!)))) + (collect-finally!)))) + (collect-finally!) + (parse-clause!))) + (:else + (do + (set! body-forms (append body-forms (list (next!)))) + (parse-clause!))))))))) + (parse-clause!) + (define let-bindings (list)) + (for-each + (fn (wb) (set! let-bindings (append let-bindings (list wb)))) + with-bindings) + (for-each + (fn + (v) + (set! + let-bindings + (append let-bindings (list (list v (get accum-vars v)))))) + (keys accum-vars)) + (when + (not (nil? repeat-var)) + (set! + let-bindings + (append let-bindings (list (list repeat-var repeat-count))))) + (for-each + (fn + (fb) + (let + ((type (get fb "type"))) + (cond + ((= type ":list") + (do + (set! + let-bindings + (append + let-bindings + (list (list (get fb "tail") (get fb "list"))) + (list + (list + (get fb "var") + (list + "IF" + (list "CONSP" (get fb "tail")) + (list "CAR" (get fb "tail")) + nil))))) + nil)) + ((= type ":numeric") + (set! + let-bindings + (append + let-bindings + (list (list (get fb "var") (get fb "from")))))) + ((= type ":general") + (set! + let-bindings + (append + let-bindings + (list (list (get fb "var") (get fb "init")))))) + (:else nil)))) + for-bindings) + (define all-tests (list)) + (when + (not (nil? repeat-var)) + (set! + all-tests + (append + all-tests + (list + (list + "WHEN" + (list "<=" repeat-var 0) + (list "RETURN-FROM" block-name (if (nil? result-var) nil result-var)))))) + (set! + body-forms + (append + (list (list "SETQ" repeat-var (list "-" repeat-var 1))) + body-forms))) + (for-each + (fn + (fb) + (when + (= (get fb "type") ":list") + (let + ((tvar (get fb "tail")) (var (get fb "var"))) + (set! + all-tests + (append + all-tests + (list + (list + "WHEN" + (list "NULL" tvar) + (list + "RETURN-FROM" + block-name + (if (nil? result-var) nil result-var)))))) + (set! + body-forms + (append + body-forms + (list + (list "SETQ" tvar (list "CDR" tvar)) + (list + "SETQ" + var + (list "IF" (list "CONSP" tvar) (list "CAR" tvar) nil)))))))) + for-bindings) + (for-each + (fn + (fb) + (when + (= (get fb "type") ":numeric") + (let + ((var (get fb "var")) + (dir (get fb "dir")) + (lim (get fb "limit")) + (ltype (get fb "limit-type")) + (step (get fb "step"))) + (when + (not (nil? lim)) + (let + ((test-op (cond ((or (= ltype "BELOW") (= ltype "ABOVE")) (if (= dir ":up") ">=" "<=")) ((or (= ltype "TO") (= ltype "UPTO")) ">") ((= ltype "DOWNTO") "<") (:else (if (= dir ":up") ">" "<"))))) + (set! + all-tests + (append + all-tests + (list + (list + "WHEN" + (list test-op var lim) + (list + "RETURN-FROM" + block-name + (if (nil? result-var) nil result-var)))))))) + (let + ((step-op (if (or (= dir ":down") (= ltype "DOWNTO") (= ltype "ABOVE")) "-" "+"))) + (set! + body-forms + (append + body-forms + (list (list "SETQ" var (list step-op var step))))))))) + for-bindings) + (for-each + (fn + (fb) + (when + (= (get fb "type") ":general") + (set! + body-forms + (append + body-forms + (list (list "SETQ" (get fb "var") (get fb "then"))))))) + for-bindings) + (for-each + (fn + (t) + (let + ((type (get t "type")) (expr (get t "expr"))) + (if + (= type ":while") + (set! + all-tests + (append + all-tests + (list + (list + "WHEN" + (list "NOT" expr) + (list + "RETURN-FROM" + block-name + (if (nil? result-var) nil result-var)))))) + (set! + all-tests + (append + all-tests + (list + (list + "WHEN" + expr + (list + "RETURN-FROM" + block-name + (if (nil? result-var) nil result-var))))))))) + test-forms) + (when + (not (nil? termination)) + (let + ((type (get termination "type")) (expr (get termination "expr"))) + (cond + ((= type ":always") + (set! + body-forms + (append + body-forms + (list + (list "UNLESS" expr (list "RETURN-FROM" block-name false))))) + (set! return-expr true)) + ((= type ":never") + (set! + body-forms + (append + body-forms + (list + (list "WHEN" expr (list "RETURN-FROM" block-name false))))) + (set! return-expr true)) + ((= type ":thereis") + (set! + body-forms + (append + body-forms + (list + (list "WHEN" expr (list "RETURN-FROM" block-name expr))))))))) + (define tag "#LOOP-START") + (define + inner-body + (append (list tag) all-tests body-forms (list (list "GO" tag)))) + (define + result-form + (cond + ((not (nil? return-expr)) return-expr) + ((not (nil? result-var)) result-var) + (:else nil))) + (define + full-body + (if + (= (len let-bindings) 0) + (append + (list "PROGN") + (list (append (list "TAGBODY") inner-body)) + finally-forms + (list result-form)) + (list + "LET*" + let-bindings + (append (list "TAGBODY") inner-body) + (append (list "PROGN") finally-forms (list result-form))))) + (list "BLOCK" block-name full-body))) + +;; ── Install LOOP as a CL macro ──────────────────────────────────────────── +;; +;; (loop ...) — the form arrives with head "LOOP" and rest = clauses. +;; The macro fn receives the full form. + +(dict-set! + cl-macro-registry + "LOOP" + (fn (form env) (cl-loop-parse (rest form)))) diff --git a/lib/common-lisp/scoreboard.json b/lib/common-lisp/scoreboard.json index 3c21a86f..fb31a384 100644 --- a/lib/common-lisp/scoreboard.json +++ b/lib/common-lisp/scoreboard.json @@ -1,6 +1,6 @@ { - "generated": "2026-05-05T11:37:47Z", - "total_pass": 437, + "generated": "2026-05-05T12:00:17Z", + "total_pass": 464, "total_fail": 0, "suites": [ {"name": "Phase 1: tokenizer/reader", "pass": 79, "fail": 0}, @@ -12,6 +12,7 @@ {"name": "Phase 3: interactive-debugger", "pass": 7, "fail": 0}, {"name": "Phase 4: CLOS", "pass": 41, "fail": 0}, {"name": "Phase 4: geometry", "pass": 12, "fail": 0}, - {"name": "Phase 4: mop-trace", "pass": 13, "fail": 0} + {"name": "Phase 4: mop-trace", "pass": 13, "fail": 0}, + {"name": "Phase 5: macros+LOOP", "pass": 27, "fail": 0} ] } diff --git a/lib/common-lisp/scoreboard.md b/lib/common-lisp/scoreboard.md index dae86da3..351c6c92 100644 --- a/lib/common-lisp/scoreboard.md +++ b/lib/common-lisp/scoreboard.md @@ -1,6 +1,6 @@ # Common Lisp on SX — Scoreboard -_Generated: 2026-05-05 11:37 UTC_ +_Generated: 2026-05-05 12:00 UTC_ | Suite | Pass | Fail | Status | |-------|------|------|--------| @@ -14,5 +14,6 @@ _Generated: 2026-05-05 11:37 UTC_ | Phase 4: CLOS | 41 | 0 | pass | | Phase 4: geometry | 12 | 0 | pass | | Phase 4: mop-trace | 13 | 0 | pass | +| Phase 5: macros+LOOP | 27 | 0 | pass | -**Total: 437 passed, 0 failed** +**Total: 464 passed, 0 failed** diff --git a/lib/common-lisp/test.sh b/lib/common-lisp/test.sh index 85cf3f86..cffa2a38 100755 --- a/lib/common-lisp/test.sh +++ b/lib/common-lisp/test.sh @@ -416,6 +416,23 @@ run_clos_suite \ "lib/common-lisp/tests/programs/mop-trace.sx" \ "mop-passed" "mop-failed" "mop-failures" +# ── Phase 5: macros + LOOP ─────────────────────────────────────────────────── +MACRO_FILE=$(mktemp); trap "rm -f $MACRO_FILE" EXIT +printf '(epoch 1)\n(load "spec/stdlib.sx")\n(epoch 2)\n(load "lib/common-lisp/reader.sx")\n(epoch 3)\n(load "lib/common-lisp/parser.sx")\n(epoch 4)\n(load "lib/common-lisp/eval.sx")\n(epoch 5)\n(load "lib/common-lisp/loop.sx")\n(epoch 6)\n(load "lib/common-lisp/tests/macros.sx")\n(epoch 7)\n(eval "macro-passed")\n(epoch 8)\n(eval "macro-failed")\n(epoch 9)\n(eval "macro-failures")\n' > "$MACRO_FILE" +MACRO_OUT=$(timeout 60 "$SX_SERVER" < "$MACRO_FILE" 2>/dev/null) +rm -f "$MACRO_FILE" +MACRO_PASSED=$(echo "$MACRO_OUT" | grep -A1 "^(ok-len 7 " | tail -1 || true) +MACRO_FAILED=$(echo "$MACRO_OUT" | grep -A1 "^(ok-len 8 " | tail -1 || true) +[ -z "$MACRO_PASSED" ] && MACRO_PASSED=0; [ -z "$MACRO_FAILED" ] && MACRO_FAILED=0 +if [ "$MACRO_FAILED" = "0" ] && [ "$MACRO_PASSED" -gt 0 ] 2>/dev/null; then + PASS=$((PASS + MACRO_PASSED)) + [ "$VERBOSE" = "-v" ] && echo " ok Phase 5 macros+LOOP ($MACRO_PASSED)" +else + FAIL=$((FAIL + 1)) + ERRORS+=" FAIL [Phase 5 macros+LOOP] (${MACRO_PASSED} passed, ${MACRO_FAILED} failed) +" +fi + TOTAL=$((PASS+FAIL)) if [ $FAIL -eq 0 ]; then echo "ok $PASS/$TOTAL lib/common-lisp tests passed" diff --git a/lib/common-lisp/tests/macros.sx b/lib/common-lisp/tests/macros.sx new file mode 100644 index 00000000..5d1addae --- /dev/null +++ b/lib/common-lisp/tests/macros.sx @@ -0,0 +1,204 @@ +;; lib/common-lisp/tests/macros.sx — Phase 5: defmacro, gensym, LOOP tests +;; +;; Depends on: runtime.sx, eval.sx, loop.sx already loaded. +;; Tests via (ev "...") using the CL evaluator. + +(define ev (fn (src) (cl-eval-str src (cl-make-env)))) +(define evall (fn (src) (cl-eval-all-str src (cl-make-env)))) + +(define passed 0) +(define failed 0) +(define failures (list)) + +(define + check + (fn + (label got expected) + (if + (= got expected) + (set! passed (+ passed 1)) + (begin + (set! failed (+ failed 1)) + (set! + failures + (append + failures + (list + (str + "FAIL [" + label + "]: got=" + (inspect got) + " expected=" + (inspect expected))))))))) + +;; ── defmacro basics ────────────────────────────────────────────────────────── + +(check + "defmacro returns name" + (ev "(defmacro my-or (a b) (list 'if a a b))") + "MY-OR") + +(check + "defmacro expansion works" + (ev "(progn (defmacro my-inc (x) (list '+ x 1)) (my-inc 5))") + 6) + +(check + "defmacro with &rest" + (ev "(progn (defmacro my-list (&rest xs) (cons 'list xs)) (my-list 1 2 3))") + (list 1 2 3)) + +(check + "nested macro expansion" + (ev "(progn (defmacro sq (x) (list '* x x)) (sq 7))") + 49) + +(check + "macro in conditional" + (ev + "(progn (defmacro my-when (c &rest body) (list 'if c (cons 'progn body) nil)) (my-when t 10 20))") + 20) + +(check + "macro returns nil branch" + (ev + "(progn (defmacro my-when (c &rest body) (list 'if c (cons 'progn body) nil)) (my-when nil 42))") + nil) + +;; ── macroexpand ─────────────────────────────────────────────────────────────── + +(check + "macroexpand returns expanded form" + (ev "(progn (defmacro double (x) (list '+ x x)) (macroexpand '(double 5)))") + (list "+" 5 5)) + +;; ── gensym ──────────────────────────────────────────────────────────────────── + +(check "gensym returns string" (ev "(stringp (gensym))") true) + +(check + "gensym prefix" + (ev "(let ((g (gensym \"MY\"))) (not (= g nil)))") + true) + +(check "gensyms are unique" (ev "(not (= (gensym) (gensym)))") true) + +;; ── swap! macro with gensym ─────────────────────────────────────────────────── + +(check + "swap! macro" + (evall + "(defmacro swap! (a b) (let ((tmp (gensym))) (list 'let (list (list tmp a)) (list 'setq a b) (list 'setq b tmp)))) (defvar *a* 10) (defvar *b* 20) (swap! *a* *b*) (list *a* *b*)") + (list 20 10)) + +;; ── LOOP: basic repeat and collect ──────────────────────────────────────────── + +(check + "loop repeat collect" + (ev "(loop repeat 3 collect 99)") + (list 99 99 99)) + +(check + "loop for-in collect" + (ev "(loop for x in '(1 2 3) collect (* x x))") + (list 1 4 9)) + +(check + "loop for-from-to collect" + (ev "(loop for i from 1 to 5 collect i)") + (list 1 2 3 4 5)) + +(check + "loop for-from-below collect" + (ev "(loop for i from 0 below 4 collect i)") + (list 0 1 2 3)) + +(check + "loop for-downto collect" + (ev "(loop for i from 5 downto 1 collect i)") + (list 5 4 3 2 1)) + +(check + "loop for-by collect" + (ev "(loop for i from 0 to 10 by 2 collect i)") + (list 0 2 4 6 8 10)) + +;; ── LOOP: sum, count, maximize, minimize ───────────────────────────────────── + +(check "loop sum" (ev "(loop for i from 1 to 5 sum i)") 15) + +(check + "loop count" + (ev "(loop for x in '(1 2 3 4 5) count (> x 3))") + 2) + +(check + "loop maximize" + (ev "(loop for x in '(3 1 4 1 5 9 2 6) maximize x)") + 9) + +(check + "loop minimize" + (ev "(loop for x in '(3 1 4 1 5 9 2 6) minimize x)") + 1) + +;; ── LOOP: while and until ───────────────────────────────────────────────────── + +(check + "loop while" + (ev "(loop for i from 1 to 10 while (< i 5) collect i)") + (list 1 2 3 4)) + +(check + "loop until" + (ev "(loop for i from 1 to 10 until (= i 5) collect i)") + (list 1 2 3 4)) + +;; ── LOOP: when / unless ─────────────────────────────────────────────────────── + +(check + "loop when filter" + (ev "(loop for i from 0 below 8 when (evenp i) collect i)") + (list 0 2 4 6)) + +(check + "loop unless filter" + (ev "(loop for i from 0 below 8 unless (evenp i) collect i)") + (list 1 3 5 7)) + +;; ── LOOP: append ───────────────────────────────────────────────────────────── + +(check + "loop append" + (ev "(loop for x in '((1 2) (3 4) (5 6)) append x)") + (list 1 2 3 4 5 6)) + +;; ── LOOP: always, never, thereis ───────────────────────────────────────────── + +(check + "loop always true" + (ev "(loop for x in '(2 4 6) always (evenp x))") + true) + +(check + "loop always false" + (ev "(loop for x in '(2 3 6) always (evenp x))") + false) + +(check "loop never" (ev "(loop for x in '(1 3 5) never (evenp x))") true) + +(check "loop thereis" (ev "(loop for x in '(1 2 3) thereis (> x 2))") true) + +;; ── LOOP: for = then (general iteration) ───────────────────────────────────── + +(check + "loop for = then doubling" + (ev "(loop repeat 5 for x = 1 then (* x 2) collect x)") + (list 1 2 4 8 16)) + +;; ── summary ──────────────────────────────────────────────────────────────── + +(define macro-passed passed) +(define macro-failed failed) +(define macro-failures failures) diff --git a/plans/common-lisp-on-sx.md b/plans/common-lisp-on-sx.md index dc188c64..630c1adc 100644 --- a/plans/common-lisp-on-sx.md +++ b/plans/common-lisp-on-sx.md @@ -94,11 +94,11 @@ Core mapping: - [x] `mop-trace.sx` — `:before` + `:after` printing call trace — 13 tests ### Phase 5 — macros + LOOP + reader macros -- [ ] `defmacro`, `macrolet`, `symbol-macrolet`, `macroexpand-1`, `macroexpand` -- [ ] `gensym`, `gentemp` +- [x] `defmacro`, `macrolet`, `symbol-macrolet`, `macroexpand-1`, `macroexpand` +- [x] `gensym`, `gentemp` - [ ] `set-macro-character`, `set-dispatch-macro-character`, `get-macro-character` -- [ ] **The LOOP macro** — iteration drivers (`for … in/across/from/upto/downto/by`, `while`, `until`, `repeat`), accumulators (`collect`, `append`, `nconc`, `count`, `sum`, `maximize`, `minimize`), conditional clauses (`if`/`when`/`unless`/`else`), termination (`finally`/`thereis`/`always`/`never`), `named` blocks -- [ ] LOOP test corpus: 30+ tests covering all clause types +- [x] **The LOOP macro** — iteration drivers (`for … in/across/from/upto/downto/by`, `while`, `until`, `repeat`), accumulators (`collect`, `append`, `nconc`, `count`, `sum`, `maximize`, `minimize`), conditional clauses (`if`/`when`/`unless`/`else`), termination (`finally`/`thereis`/`always`/`never`), `named` blocks +- [x] LOOP test corpus: 27 tests covering all clause types ### Phase 6 — packages + stdlib drive - [ ] `defpackage`, `in-package`, `export`, `use-package`, `import`, `find-package`