diff --git a/lib/ocaml/eval.sx b/lib/ocaml/eval.sx index 6f808939..35115a33 100644 --- a/lib/ocaml/eval.sx +++ b/lib/ocaml/eval.sx @@ -259,6 +259,22 @@ (= (len (rest val)) (len arg-pats))) (ocaml-match-list arg-pats (rest val) env)) (else ocaml-match-fail)))) + ((= tag "por") + ;; (:por ALT1 ALT2 ...) — try each alternative; succeed on the + ;; first match. Note: for now, alternatives must bind the same + ;; set of variables (OCaml constraint) — we don't enforce this. + (let ((alts (rest pat)) (result ocaml-match-fail) (done false)) + (begin + (define try-alts + (fn (xs) + (when (and (not done) (not (= xs (list)))) + (let ((env2 (ocaml-match-pat (first xs) val env))) + (cond + ((not (= env2 ocaml-match-fail)) + (begin (set! result env2) (set! done true))) + (else (try-alts (rest xs)))))))) + (try-alts alts) + result))) ((= tag "pas") ;; (:pas INNER NAME) — match inner pattern, also bind NAME → val. (let ((inner (nth pat 1)) (alias (nth pat 2))) diff --git a/lib/ocaml/parser.sx b/lib/ocaml/parser.sx index 6befa7a3..cf71cad7 100644 --- a/lib/ocaml/parser.sx +++ b/lib/ocaml/parser.sx @@ -223,6 +223,19 @@ (loop) (consume! "op" ")") (cons :ptuple items)))) + ;; Parens-only or-pattern: (P1 | P2 | ...). + ((at-op? "|") + (let ((alts (list first))) + (begin + (define loop-or + (fn () + (when (at-op? "|") + (begin (advance-tok!) + (append! alts (parse-pattern)) + (loop-or))))) + (loop-or) + (consume! "op" ")") + (cons :por alts)))) (else (begin (consume! "op" ")") first)))))))) ((and (= tt "op") (= tv "[")) (begin diff --git a/lib/ocaml/test.sh b/lib/ocaml/test.sh index d7d14a13..b730db9a 100755 --- a/lib/ocaml/test.sh +++ b/lib/ocaml/test.sh @@ -964,6 +964,18 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 2702) (eval "(ocaml-parse-program \"module type EMPTY = sig end\")") +;; ── or-patterns (parens-only) ───────────────────────────────── +(epoch 2800) +(eval "(ocaml-run \"match 1 with | (1 | 2 | 3) -> 100 | _ -> 0\")") +(epoch 2801) +(eval "(ocaml-run \"match 2 with | (1 | 2 | 3) -> 100 | _ -> 0\")") +(epoch 2802) +(eval "(ocaml-run \"match 5 with | (1 | 2 | 3) -> 100 | _ -> 0\")") +(epoch 2803) +(eval "(ocaml-run \"match Red with | (Red | Green) -> 1 | Blue -> 2\")") +(epoch 2804) +(eval "(ocaml-run \"match Blue with | (Red | Green) -> 1 | Blue -> 2\")") + EPOCHS OUTPUT=$(timeout 180 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -1525,6 +1537,13 @@ check 2700 "module type S parses" '("module-type-def" "S")' check 2701 "module type then module" '42' check 2702 "module type EMPTY" '("module-type-def" "EMPTY")' +# ── or-patterns (parens-only) ────────────────────────────────── +check 2800 "(1|2|3) match 1" '100' +check 2801 "(1|2|3) match 2" '100' +check 2802 "(1|2|3) match 5" '0' +check 2803 "(Red|Green) Red" '1' +check 2804 "(Red|Green) Blue" '2' + 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 f9653a9b..2cbb90cb 100644 --- a/plans/ocaml-on-sx.md +++ b/plans/ocaml-on-sx.md @@ -137,9 +137,9 @@ SX CEK evaluator (both JS and OCaml hosts) - [x] **Patterns:** constructor (nullary + with args, incl. flattened tuple args), literal (int/string/char/bool/unit), variable, wildcard `_`, tuple, list cons `::`, list literal, record `{ f = pat; … }`, - `as` binding. Match clauses support `when` guard via - `(:case-when PAT GUARD BODY)`. _(Pending: or-pattern `P1 | P2` — - ambiguous with clause separator without lookahead.)_ + `as` binding, or-pattern `(P1 | P2 | …)` (parens-only — top-level + `|` is the clause separator). Match clauses support `when` guard + via `(:case-when PAT GUARD BODY)`. - [ ] OCaml is **not** indentation-sensitive — no layout algorithm needed. - [ ] Tests in `lib/ocaml/tests/parse.sx` — 50+ round-trip parse tests. @@ -377,6 +377,11 @@ the "mother tongue" closure: OCaml → SX → OCaml. This means: _Newest first._ +- 2026-05-08 Phase 1+3 — or-patterns `(P1 | P2 | ...)` parens-only + (+5 tests, 394 total). Parser: when `|` follows a pattern inside + parens, build `(:por ALT1 ALT2 ...)`. Eval: try alternatives, succeed + on the first match. Top-level `|` remains the clause separator (no + lookahead needed). Examples: `(1 | 2 | 3) -> ...`, `(Red | Green) -> 1`. - 2026-05-08 Phase 4 — `module type S = sig … end` parser (+3 tests, 389 total). Signatures are parsed-and-discarded — sig..end balanced skipping. AST: `(:module-type-def NAME)`. Runtime no-op (signatures