diff --git a/lib/ocaml/parser.sx b/lib/ocaml/parser.sx index d1a56dff..6c48ee9b 100644 --- a/lib/ocaml/parser.sx +++ b/lib/ocaml/parser.sx @@ -17,6 +17,7 @@ ;; let let x = e in body (no rec, function shorthand) ;; let rec f x = e in body ;; match match e with [|] p -> body | p -> body | ... +;; sequence e1 ; e2 → (:seq e1 e2 …) (lowest-precedence binary) ;; ;; Pattern scope: ;; _ (wildcard), int/string/char/bool literals, ident (var binding), @@ -30,6 +31,7 @@ ;; (:op OP LHS RHS) ;; (:neg E) (:not E) ;; (:tuple ITEMS) (:list ITEMS) +;; (:seq EXPRS) ;; (:if C T E) ;; (:fun PARAMS BODY) ;; (:let NAME PARAMS EXPR BODY) (:let-rec NAME PARAMS EXPR BODY) @@ -280,6 +282,7 @@ (else lhs))))) (set! parse-pattern (fn () (parse-pattern-cons))) (define parse-expr (fn () nil)) + (define parse-expr-no-seq (fn () nil)) (define parse-tuple (fn () nil)) (define parse-binop-rhs (fn (lhs min-prec) lhs)) (define parse-prefix (fn () nil)) @@ -324,7 +327,7 @@ (let ((items (list))) (begin - (append! items (parse-expr)) + (append! items (parse-expr-no-seq)) (define loop (fn @@ -336,7 +339,7 @@ (when (not (at-op? "]")) (begin - (append! items (parse-expr)) + (append! items (parse-expr-no-seq)) (loop))))))) (loop) (consume! "op" "]") @@ -521,17 +524,17 @@ (fn () (let - ((cond-expr (parse-expr))) + ((cond-expr (parse-expr-no-seq))) (begin (consume! "keyword" "then") (let - ((then-expr (parse-expr))) + ((then-expr (parse-expr-no-seq))) (cond ((at-kw? "else") (begin (advance-tok!) (let - ((else-expr (parse-expr))) + ((else-expr (parse-expr-no-seq))) (list :if cond-expr then-expr else-expr)))) (else (list :if cond-expr then-expr (list :unit))))))))) (define @@ -539,7 +542,7 @@ (fn () (let - ((scrut (parse-expr))) + ((scrut (parse-expr-no-seq))) (begin (consume! "keyword" "with") (when (at-op? "|") (advance-tok!)) @@ -555,7 +558,7 @@ (begin (consume! "op" "->") (let - ((body (parse-match-body))) + ((body (parse-expr))) (append! cases (list :case p body))))))) (one) (define @@ -567,9 +570,8 @@ (begin (advance-tok!) (one) (loop))))) (loop) (cons :match (cons scrut (list cases))))))))) - (define parse-match-body (fn () (parse-expr))) (set! - parse-expr + parse-expr-no-seq (fn () (cond @@ -578,6 +580,40 @@ ((at-kw? "if") (begin (advance-tok!) (parse-if))) ((at-kw? "match") (begin (advance-tok!) (parse-match))) (else (parse-tuple))))) + (set! + parse-expr + (fn + () + (let + ((lhs (parse-expr-no-seq))) + (cond + ((at-op? ";") + (let + ((items (list lhs))) + (begin + (define + loop + (fn + () + (when + (at-op? ";") + (begin + (advance-tok!) + (cond + ((at-kw? "end") nil) + ((at-op? ")") nil) + ((at-op? "|") nil) + ((at-kw? "in") nil) + ((at-kw? "then") nil) + ((at-kw? "else") nil) + ((= (ocaml-tok-type (peek-tok)) "eof") nil) + (else + (begin + (append! items (parse-expr-no-seq)) + (loop)))))))) + (loop) + (cons :seq items)))) + (else lhs))))) (let ((result (parse-expr))) (begin diff --git a/lib/ocaml/test.sh b/lib/ocaml/test.sh index bb6bd860..b6e0821d 100755 --- a/lib/ocaml/test.sh +++ b/lib/ocaml/test.sh @@ -300,6 +300,28 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 308) (eval "(ocaml-parse \"match x with | () -> 0\")") +;; ── Sequences (;) ────────────────────────────────────────────── +(epoch 320) +(eval "(ocaml-parse \"1; 2\")") +(epoch 321) +(eval "(ocaml-parse \"1; 2; 3\")") +(epoch 322) +(eval "(ocaml-parse \"(1; 2)\")") +(epoch 323) +(eval "(ocaml-parse \"begin a; b; c end\")") +(epoch 324) +(eval "(ocaml-parse \"let x = 1 in x; x\")") +(epoch 325) +(eval "(ocaml-parse \"if c then (a; b) else c\")") +(epoch 326) +(eval "(ocaml-parse \"[1; 2; 3]\")") +(epoch 327) +(eval "(ocaml-parse \"1; 2;\")") +(epoch 328) +(eval "(ocaml-parse \"begin a; end\")") +(epoch 329) +(eval "(ocaml-parse \"match x with | _ -> a; b\")") + EPOCHS OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -476,6 +498,18 @@ check 306 "match ctor with tuple arg" '("pcon" "Pair" ("pvar" "a") ("pvar" "b check 307 "match string literal" '("plit" ("string" "hi"))' check 308 "match unit pattern" '("plit" ("unit"))' +# ── Sequences ─────────────────────────────────────────────────── +check 320 "seq 1;2" '("seq" ("int" 1) ("int" 2))' +check 321 "seq 1;2;3" '("seq" ("int" 1) ("int" 2) ("int" 3))' +check 322 "seq in parens" '("seq" ("int" 1) ("int" 2))' +check 323 "seq in begin/end" '("seq" ("var" "a") ("var" "b") ("var" "c"))' +check 324 "let body absorbs seq" '("let" "x" () ("int" 1) ("seq" ("var" "x") ("var" "x")))' +check 325 "if-branch parens for seq" '("if" ("var" "c") ("seq" ("var" "a") ("var" "b"))' +check 326 "list ; is separator" '("list" ("int" 1) ("int" 2) ("int" 3))' +check 327 "trailing ; OK" '("seq" ("int" 1) ("int" 2))' +check 328 "begin a; end singleton seq" '("seq" ("var" "a"))' +check 329 "match clause body absorbs ;" '("case" ("pwild") ("seq" ("var" "a") ("var" "b")))' + 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 7294cd83..04c61365 100644 --- a/plans/ocaml-on-sx.md +++ b/plans/ocaml-on-sx.md @@ -128,11 +128,12 @@ SX CEK evaluator (both JS and OCaml hosts) - [~] **Parser:** expressions: literals, identifiers, constructor application, lambda, application (left-assoc), binary ops with precedence (29 ops via `lib/guest/pratt.sx`), `if`/`then`/`else`, `let`/`in`, `let rec`, - `fun`/`->`, tuples, list literals, `begin`/`end`, unit `()`. Top-level - decls: `let [rec] name params* = expr` and bare expressions, `;;`-separated - via `ocaml-parse-program`. _(Pending: `type`/`module`/`exception`/`open`/ - `include` decls, `match`/`with`, `try`/`with`, `function`, record literals/ - updates, field access, sequences `;`, `and` mutually-recursive bindings.)_ + `fun`/`->`, `match`/`with`, tuples, list literals, sequences `;`, + `begin`/`end`, unit `()`. Top-level decls: `let [rec] name params* = expr` + and bare expressions, `;;`-separated via `ocaml-parse-program`. _(Pending: + `type`/`module`/`exception`/`open`/`include` decls, `try`/`with`, + `function`, record literals/updates, field access, `and` mutually-recursive + bindings.)_ - [~] **Patterns:** constructor (nullary + with args, incl. flattened tuple args `Pair (a, b)` → `(:pcon "Pair" PA PB)`), literal (int/string/char/ bool/unit), variable, wildcard `_`, tuple, list cons `::`, list literal. @@ -314,6 +315,14 @@ the "mother tongue" closure: OCaml → SX → OCaml. This means: _Newest first._ +- 2026-05-07 Phase 1 — sequence operator `;`. Lowest-precedence binary; + `e1; e2; e3` → `(:seq e1 e2 e3)`. Two-phase grammar: `parse-expr-no-seq` + is the prior expression entry point; new `parse-expr` wraps it with + `;` chaining. List-literal items still use `parse-expr-no-seq` so `;` + retains its separator role inside `[…]`. Match-clause bodies use the + seq variant and stop at `|`, matching real OCaml semantics. Trailing `;` + before `end`/`)`/`|`/`in`/`then`/`else`/eof is permitted. 123/123 tests + passing (+10). - 2026-05-07 Phase 1 — `match`/`with` + pattern parser. Patterns: wildcard, literal, var, ctor (nullary + with arg, with tuple-arg flattening so `Pair (a, b)` → `(:pcon "Pair" PA PB)`), tuple, list literal, cons `::`