diff --git a/lib/ocaml/parser.sx b/lib/ocaml/parser.sx index b188298a..0b539910 100644 --- a/lib/ocaml/parser.sx +++ b/lib/ocaml/parser.sx @@ -826,13 +826,33 @@ (define parse-one! (fn () (let ((nm (ocaml-tok-value (consume! "ident" nil))) - (ps (list))) + (ps (list)) + (tuple-binds (list))) (begin (define collect-params (fn () - (let ((p (try-consume-param!))) - (when (not (= p nil)) - (begin (append! ps p) (collect-params)))))) + (cond + ;; `(IDENT, ...)` — tuple param. + ((and (at-op? "(") + (= (ocaml-tok-type (peek-tok-at 1)) "ident") + (or (= (ocaml-tok-value (peek-tok-at 2)) ",") + (= (ocaml-tok-value (peek-tok-at 2)) ")"))) + (cond + ((= (ocaml-tok-value (peek-tok-at 2)) ":") + (let ((p (try-consume-param!))) + (when (not (= p nil)) + (begin (append! ps p) (collect-params))))) + (else + (let ((pat (parse-pattern))) + (let ((sn (str "__pat_" (len tuple-binds)))) + (begin + (append! tuple-binds (list sn pat)) + (append! ps sn) + (collect-params))))))) + (else + (let ((p (try-consume-param!))) + (when (not (= p nil)) + (begin (append! ps p) (collect-params)))))))) (collect-params) ;; Optional type annotation: skip `: TYPE` before `=`. (when (at-op? ":") @@ -848,7 +868,23 @@ (skip-tann))) (consume! "op" "=") (let ((rhs (parse-expr))) - (append! bindings (list nm ps rhs))))))) + (begin + ;; Wrap rhs with `match __pat_N with PAT -> ...` + ;; for each tuple-param, innermost first. + (let ((wrapped rhs)) + (begin + (define wrap-binds + (fn (xs) + (when (not (= xs (list))) + (begin + (let ((sn (nth (first xs) 0)) + (pat (nth (first xs) 1))) + (set! wrapped + (list :match (list :var sn) + (list (list :case pat wrapped))))) + (wrap-binds (rest xs)))))) + (wrap-binds (reverse tuple-binds)) + (append! bindings (list nm ps wrapped)))))))))) (parse-one!) (define more (fn () diff --git a/lib/ocaml/test.sh b/lib/ocaml/test.sh index 557461d0..0932e6e0 100755 --- a/lib/ocaml/test.sh +++ b/lib/ocaml/test.sh @@ -1380,6 +1380,14 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 5123) (eval "(ocaml-run \"(fun a (b, c) d -> a + b + c + d) 1 (2, 3) 4\")") +;; ── let f (a, b) = body — tuple param on inner-let ──────────── +(epoch 5130) +(eval "(ocaml-run \"let f (a, b) = a + b in f (3, 7)\")") +(epoch 5131) +(eval "(ocaml-run \"let g x (a, b) = x + a + b in g 1 (2, 3)\")") +(epoch 5132) +(eval "(ocaml-run \"let h (a, b) (c, d) = a * b + c * d in h (1, 2) (3, 4)\")") + EPOCHS OUTPUT=$(timeout 360 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -2193,6 +2201,11 @@ check 5121 "List.map fun (a, b)" '(2 12 30)' check 5122 "List.map fun (k, _)" '("a" "b")' check 5123 "fun a (b, c) d mixed" '10' +# ── let f (a, b) = body — let with tuple param ────────────────── +check 5130 "let f (a, b) = a + b" '10' +check 5131 "let g x (a, b) mixed" '6' +check 5132 "let h (a, b) (c, d) curried" '14' + TOTAL=$((PASS + FAIL)) if [ $FAIL -eq 0 ]; then echo "ok $PASS/$TOTAL OCaml-on-SX tests passed" diff --git a/plans/ocaml-on-sx.md b/plans/ocaml-on-sx.md index 445d72ca..3374b968 100644 --- a/plans/ocaml-on-sx.md +++ b/plans/ocaml-on-sx.md @@ -407,6 +407,14 @@ _Newest first._ binary search tree (`type 'a tree = Leaf | Node of 'a * 'a tree * 'a tree`) with insert + in-order traversal. Tests parametric ADT, recursive match, List.append, List.fold_left. +- 2026-05-09 Phase 4 — `let f (a, b) = body in body2` tuple-param on + inner-let bindings (+3 tests, 556 total). Mirrors iteration 101's + parse-fun change inside parse-let's parse-one!: same `(IDENT, ...)` + detection, same `__pat_N` synth name, same innermost-first match + wrapping — but applied to the rhs of the let-binding (which is the + function value). Lets us write `let f (a, b) = a + b in f (3, 7)`, + `let g x (a, b) = x + a + b in g 1 (2, 3)`, and `let h (a, b) + (c, d) = a * b + c * d in h (1, 2) (3, 4)`. - 2026-05-09 Phase 4 — `fun (a, b) -> body` tuple-param destructuring (+4 tests, 553 total). parse-fun's collect-params now detects `(IDENT, ...)` (lookahead at peek-tok-at 1/2 to distinguish from