From cdee007185fd62e9b81c1c633ebfad81a794f302 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 25 Apr 2026 18:26:58 +0000 Subject: [PATCH] cl: Phase 1 lambda-list parser + 31 tests (172 total green) Co-Authored-By: Claude Sonnet 4.6 --- lib/common-lisp/parser.sx | 118 ++++++++++++++++++ lib/common-lisp/tests/lambda.sx | 204 ++++++++++++++++++++++++++++++++ plans/common-lisp-on-sx.md | 5 +- 3 files changed, 325 insertions(+), 2 deletions(-) create mode 100644 lib/common-lisp/tests/lambda.sx diff --git a/lib/common-lisp/parser.sx b/lib/common-lisp/parser.sx index b34867fa..c5724aa1 100644 --- a/lib/common-lisp/parser.sx +++ b/lib/common-lisp/parser.sx @@ -227,6 +227,124 @@ (get item "rest") (concat acc (list (get item "form")))))))))))) +;; ── lambda-list parser ─────────────────────────────────────────── +;; +;; (cl-parse-lambda-list forms) — parse a list of CL forms (already read) +;; into a structured dict: +;; {:required (list sym ...) +;; :optional (list {:name N :default D :supplied S} ...) +;; :rest nil | "SYM" +;; :key (list {:name N :keyword K :default D :supplied S} ...) +;; :allow-other-keys false | true +;; :aux (list {:name N :init I} ...)} +;; +;; Symbols arrive as SX strings (upcase). &-markers are strings like "&OPTIONAL". +;; Key params: keyword is the upcase name string; caller uses it as :keyword. +;; Supplied-p: nil when absent. + +(define + cl-parse-opt-spec + (fn + (spec) + (if + (list? spec) + {:name (nth spec 0) + :default (if (> (len spec) 1) (nth spec 1) nil) + :supplied (if (> (len spec) 2) (nth spec 2) nil)} + {:name spec :default nil :supplied nil}))) + +(define + cl-parse-key-spec + (fn + (spec) + (if + (list? spec) + (let + ((first (nth spec 0))) + (if + (list? first) + ;; ((:keyword var) default supplied-p) + {:name (nth first 1) + :keyword (get first "name") + :default (if (> (len spec) 1) (nth spec 1) nil) + :supplied (if (> (len spec) 2) (nth spec 2) nil)} + ;; (var default supplied-p) + {:name first + :keyword first + :default (if (> (len spec) 1) (nth spec 1) nil) + :supplied (if (> (len spec) 2) (nth spec 2) nil)})) + {:name spec :keyword spec :default nil :supplied nil}))) + +(define + cl-parse-aux-spec + (fn + (spec) + (if + (list? spec) + {:name (nth spec 0) :init (if (> (len spec) 1) (nth spec 1) nil)} + {:name spec :init nil}))) + +(define + cl-parse-lambda-list + (fn + (forms) + (let + ((state "required") + (required (list)) + (optional (list)) + (rest-name nil) + (key (list)) + (allow-other-keys false) + (aux (list))) + + (define + scan + (fn + (items) + (when + (> (len items) 0) + (let + ((item (nth items 0)) (tail (rest items))) + (cond + ((= item "&OPTIONAL") + (do (set! state "optional") (scan tail))) + ((= item "&REST") + (do (set! state "rest") (scan tail))) + ((= item "&BODY") + (do (set! state "rest") (scan tail))) + ((= item "&KEY") + (do (set! state "key") (scan tail))) + ((= item "&AUX") + (do (set! state "aux") (scan tail))) + ((= item "&ALLOW-OTHER-KEYS") + (do (set! allow-other-keys true) (scan tail))) + ((= state "required") + (do (append! required item) (scan tail))) + ((= state "optional") + (do (append! optional (cl-parse-opt-spec item)) (scan tail))) + ((= state "rest") + (do (set! rest-name item) (set! state "done") (scan tail))) + ((= state "key") + (do (append! key (cl-parse-key-spec item)) (scan tail))) + ((= state "aux") + (do (append! aux (cl-parse-aux-spec item)) (scan tail))) + (:else (scan tail))))))) + + (scan forms) + {:required required + :optional optional + :rest rest-name + :key key + :allow-other-keys allow-other-keys + :aux aux}))) + +;; Convenience: parse lambda list from a CL source string +(define + cl-parse-lambda-list-str + (fn + (src) + (cl-parse-lambda-list (cl-read src)))) + ;; ── public API ──────────────────────────────────────────────────── (define diff --git a/lib/common-lisp/tests/lambda.sx b/lib/common-lisp/tests/lambda.sx new file mode 100644 index 00000000..134f3963 --- /dev/null +++ b/lib/common-lisp/tests/lambda.sx @@ -0,0 +1,204 @@ +;; Lambda list parser tests + +(define cl-test-pass 0) +(define cl-test-fail 0) +(define cl-test-fails (list)) + +;; Deep structural equality for dicts and lists +(define + cl-deep= + (fn + (a b) + (cond + ((= a b) true) + ((and (dict? a) (dict? b)) + (let + ((ak (keys a)) (bk (keys b))) + (if + (not (= (len ak) (len bk))) + false + (every? + (fn (k) (and (has-key? b k) (cl-deep= (get a k) (get b k)))) + ak)))) + ((and (list? a) (list? b)) + (if + (not (= (len a) (len b))) + false + (let + ((i 0) (ok true)) + (define + chk + (fn + () + (when + (and ok (< i (len a))) + (do + (when + (not (cl-deep= (nth a i) (nth b i))) + (set! ok false)) + (set! i (+ i 1)) + (chk))))) + (chk) + ok))) + (:else false)))) + +(define + cl-test + (fn + (name actual expected) + (if + (cl-deep= actual expected) + (set! cl-test-pass (+ cl-test-pass 1)) + (do + (set! cl-test-fail (+ cl-test-fail 1)) + (append! cl-test-fails {:name name :expected expected :actual actual}))))) + +;; Helper: parse lambda list from string "(x y ...)" +(define ll (fn (src) (cl-parse-lambda-list-str src))) +(define ll-req (fn (src) (get (ll src) "required"))) +(define ll-opt (fn (src) (get (ll src) "optional"))) +(define ll-rest (fn (src) (get (ll src) "rest"))) +(define ll-key (fn (src) (get (ll src) "key"))) +(define ll-aok (fn (src) (get (ll src) "allow-other-keys"))) +(define ll-aux (fn (src) (get (ll src) "aux"))) + +;; ── required parameters ─────────────────────────────────────────── + +(cl-test "required: empty" (ll-req "()") (list)) +(cl-test "required: one" (ll-req "(x)") (list "X")) +(cl-test "required: two" (ll-req "(x y)") (list "X" "Y")) +(cl-test "required: three" (ll-req "(a b c)") (list "A" "B" "C")) +(cl-test "required: upcased" (ll-req "(foo bar)") (list "FOO" "BAR")) + +;; ── &optional ───────────────────────────────────────────────────── + +(cl-test "optional: none" (ll-opt "(x)") (list)) + +(cl-test + "optional: bare symbol" + (ll-opt "(x &optional z)") + (list {:name "Z" :default nil :supplied nil})) + +(cl-test + "optional: with default" + (ll-opt "(x &optional (z 0))") + (list {:name "Z" :default 0 :supplied nil})) + +(cl-test + "optional: with supplied-p" + (ll-opt "(x &optional (z 0 z-p))") + (list {:name "Z" :default 0 :supplied "Z-P"})) + +(cl-test + "optional: two params" + (ll-opt "(&optional a (b 1))") + (list {:name "A" :default nil :supplied nil} {:name "B" :default 1 :supplied nil})) + +(cl-test + "optional: string default" + (ll-opt "(&optional (name \"world\"))") + (list {:name "NAME" :default "world" :supplied nil})) + +;; ── &rest ───────────────────────────────────────────────────────── + +(cl-test "rest: none" (ll-rest "(x)") nil) +(cl-test "rest: present" (ll-rest "(x &rest args)") "ARGS") +(cl-test "rest: with required" (ll-rest "(a b &rest tail)") "TAIL") + +;; &body is an alias for &rest +(cl-test "body: alias for rest" (ll-rest "(&body forms)") "FORMS") + +;; rest doesn't consume required params +(cl-test "rest: required still there" (ll-req "(a b &rest rest)") (list "A" "B")) + +;; ── &key ────────────────────────────────────────────────────────── + +(cl-test "key: none" (ll-key "(x)") (list)) + +(cl-test + "key: bare symbol" + (ll-key "(&key x)") + (list {:name "X" :keyword "X" :default nil :supplied nil})) + +(cl-test + "key: with default" + (ll-key "(&key (x 42))") + (list {:name "X" :keyword "X" :default 42 :supplied nil})) + +(cl-test + "key: with supplied-p" + (ll-key "(&key (x 42 x-p))") + (list {:name "X" :keyword "X" :default 42 :supplied "X-P"})) + +(cl-test + "key: two params" + (ll-key "(&key a b)") + (list + {:name "A" :keyword "A" :default nil :supplied nil} + {:name "B" :keyword "B" :default nil :supplied nil})) + +;; ── &allow-other-keys ───────────────────────────────────────────── + +(cl-test "aok: absent" (ll-aok "(x)") false) +(cl-test "aok: present" (ll-aok "(&key x &allow-other-keys)") true) + +;; ── &aux ────────────────────────────────────────────────────────── + +(cl-test "aux: none" (ll-aux "(x)") (list)) + +(cl-test + "aux: bare symbol" + (ll-aux "(&aux temp)") + (list {:name "TEMP" :init nil})) + +(cl-test + "aux: with init" + (ll-aux "(&aux (count 0))") + (list {:name "COUNT" :init 0})) + +(cl-test + "aux: two vars" + (ll-aux "(&aux a (b 1))") + (list {:name "A" :init nil} {:name "B" :init 1})) + +;; ── combined ────────────────────────────────────────────────────── + +(cl-test + "combined: full lambda list" + (let + ((parsed (ll "(x y &optional (z 0 z-p) &rest args &key a (b nil b-p) &aux temp)"))) + (list + (get parsed "required") + (get (nth (get parsed "optional") 0) "name") + (get (nth (get parsed "optional") 0) "default") + (get (nth (get parsed "optional") 0) "supplied") + (get parsed "rest") + (get (nth (get parsed "key") 0) "name") + (get (nth (get parsed "key") 1) "supplied") + (get (nth (get parsed "aux") 0) "name"))) + (list + (list "X" "Y") + "Z" + 0 + "Z-P" + "ARGS" + "A" + "B-P" + "TEMP")) + +(cl-test + "combined: required only stops before &" + (ll-req "(a b &optional c)") + (list "A" "B")) + +(cl-test + "combined: required only with &key" + (ll-req "(x &key y)") + (list "X")) + +(cl-test + "combined: &rest and &key together" + (let + ((parsed (ll "(&rest args &key verbose)"))) + (list (get parsed "rest") (get (nth (get parsed "key") 0) "name"))) + (list "ARGS" "VERBOSE")) diff --git a/plans/common-lisp-on-sx.md b/plans/common-lisp-on-sx.md index d8c4aa67..c468f708 100644 --- a/plans/common-lisp-on-sx.md +++ b/plans/common-lisp-on-sx.md @@ -52,8 +52,8 @@ Core mapping: ### Phase 1 — reader + parser - [x] Tokenizer: symbols (with package qualification `pkg:sym` / `pkg::sym`), numbers (int, float, ratio `1/3`, `#xFF`, `#b1010`, `#o17`), strings `"…"` with `\` escapes, characters `#\Space` `#\Newline` `#\a`, comments `;`, block comments `#| … |#` - [x] Reader: list, dotted pair, quote `'`, function `#'`, quasiquote `` ` ``, unquote `,`, splice `,@`, vector `#(…)`, uninterned `#:foo`, nil/t literals -- [ ] Parser: lambda lists with `&optional` `&rest` `&key` `&aux` `&allow-other-keys`, defaults, supplied-p variables -- [ ] Unit tests in `lib/common-lisp/tests/read.sx` +- [x] Parser: lambda lists with `&optional` `&rest` `&key` `&aux` `&allow-other-keys`, defaults, supplied-p variables +- [x] Unit tests in `lib/common-lisp/tests/read.sx` ### Phase 2 — sequential eval + special forms - [ ] `cl-eval-ast`: `quote`, `if`, `progn`, `let`, `let*`, `flet`, `labels`, `setq`, `setf` (subset), `function`, `lambda`, `the`, `locally`, `eval-when` @@ -114,6 +114,7 @@ Core mapping: _Newest first._ +- 2026-04-25: Phase 1 lambda-list parser — 31 new tests, 172 total green. `cl-parse-lambda-list` in `parser.sx` + `tests/lambda.sx`. Handles &optional/&rest/&body/&key/&aux/&allow-other-keys, defaults, supplied-p. Key gotchas: `(when (> (len items) 0) ...)` not `(when items ...)` (empty list is truthy); custom `cl-deep=` needed for dict/list structural equality in tests. - 2026-04-25: Phase 1 reader/parser — 62 new tests, 141 total green. `lib/common-lisp/parser.sx`: cl-read/cl-read-all, lists, dotted pairs, quote/backquote/unquote/splice/#', vectors, #:uninterned, NIL→nil, T→true, reader macro wrappers. - 2026-04-25: Phase 1 tokenizer — 79 tests green. `lib/common-lisp/reader.sx` + `tests/read.sx` + `test.sh`. Handles symbols (pkg:sym, pkg::sym), integers, floats, ratios, hex/binary/octal, strings, #\ chars, reader macros (#' #( #: ,@), line/block comments. Key gotcha: SX `str` for string concat (not `concat`), substring-based read-while.