diff --git a/lib/ocaml/eval.sx b/lib/ocaml/eval.sx index ab67a841..660f2e98 100644 --- a/lib/ocaml/eval.sx +++ b/lib/ocaml/eval.sx @@ -381,6 +381,37 @@ ;; matches its argument against the clauses. (let ((clauses (nth ast 1)) (captured env)) (fn (arg) (ocaml-match-clauses arg clauses captured)))) + ((= tag "record") + (let ((fields (rest ast)) (result {})) + (begin + (define loop + (fn (xs) + (when (not (= xs (list))) + (let ((kv (first xs))) + (let ((k (first kv)) (v (ocaml-eval (nth kv 1) env))) + (begin + (set! result (merge result (dict k v))) + (loop (rest xs)))))))) + (loop fields) + result))) + ((= tag "record-update") + (let ((base-ast (nth ast 1)) (fields (rest (rest ast)))) + (let ((base (ocaml-eval base-ast env))) + (cond + ((dict? base) + (let ((result base)) + (begin + (define loop + (fn (xs) + (when (not (= xs (list))) + (let ((kv (first xs))) + (let ((k (first kv)) (v (ocaml-eval (nth kv 1) env))) + (begin + (set! result (merge result (dict k v))) + (loop (rest xs)))))))) + (loop fields) + result))) + (else (error (str "ocaml-eval: with-update on non-record: " base))))))) ((= tag "field") ;; `e.name` — evaluate e, expect a dict (record/module), get name. ;; Special case: `(:field (:con "M") "x")` looks up M as a module diff --git a/lib/ocaml/parser.sx b/lib/ocaml/parser.sx index fc72ffdd..de78484b 100644 --- a/lib/ocaml/parser.sx +++ b/lib/ocaml/parser.sx @@ -163,7 +163,7 @@ ((= tt "ctor") true) ((and (= tt "keyword") (or (= tv "true") (= tv "false"))) true) - ((and (= tt "op") (or (= tv "(") (= tv "["))) true) + ((and (= tt "op") (or (= tv "(") (= tv "[") (= tv "{"))) true) (else false))))) (set! parse-pattern-atom @@ -401,6 +401,60 @@ (let ((e (parse-expr))) (begin (consume! "keyword" "end") e)))) + ;; Record literal { f1 = e1; f2 = e2 } or update + ;; { r with f1 = e1; f2 = e2 }. + ((and (= tt "op") (= tv "{")) + (begin + (advance-tok!) + (cond + ;; { r with field = expr; ... } — base ident + with. + ((and (= (ocaml-tok-type (peek-tok)) "ident") + (= (ocaml-tok-value (peek-tok-at 1)) "with")) + (let ((base-name (ocaml-tok-value (peek-tok)))) + (begin + (advance-tok!) ;; ident + (advance-tok!) ;; with + (let ((fields (list))) + (begin + (define one + (fn () + (let ((fname (ocaml-tok-value (consume! "ident" nil)))) + (begin + (consume! "op" "=") + (let ((fexpr (parse-expr-no-seq))) + (append! fields (list fname fexpr))))))) + (one) + (define more + (fn () + (when (at-op? ";") + (begin (advance-tok!) + (when (not (at-op? "}")) + (begin (one) (more))))))) + (more) + (consume! "op" "}") + (cons :record-update + (cons (list :var base-name) fields))))))) + (else + ;; Plain record literal { f = e; f = e; ... }. + (let ((fields (list))) + (begin + (define one + (fn () + (let ((fname (ocaml-tok-value (consume! "ident" nil)))) + (begin + (consume! "op" "=") + (let ((fexpr (parse-expr-no-seq))) + (append! fields (list fname fexpr))))))) + (one) + (define more + (fn () + (when (at-op? ";") + (begin (advance-tok!) + (when (not (at-op? "}")) + (begin (one) (more))))))) + (more) + (consume! "op" "}") + (cons :record fields))))))) (else (error (str @@ -425,7 +479,7 @@ ((= tt "ctor") true) ((and (= tt "keyword") (or (= tv "true") (= tv "false") (= tv "begin"))) true) - ((and (= tt "op") (or (= tv "(") (= tv "["))) true) + ((and (= tt "op") (or (= tv "(") (= tv "[") (= tv "{"))) true) (else false))))) (define parse-atom-postfix (fn () diff --git a/lib/ocaml/test.sh b/lib/ocaml/test.sh index c204564f..e5f42a0b 100755 --- a/lib/ocaml/test.sh +++ b/lib/ocaml/test.sh @@ -714,6 +714,20 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 1004) (eval "(ocaml-run-program \"let g _ x = x + 1;; g 99 41\")") +;; ── Records ──────────────────────────────────────────────────── +(epoch 1100) +(eval "(ocaml-run \"let r = { x = 1; y = 2 } in r.x\")") +(epoch 1101) +(eval "(ocaml-run \"let r = { x = 1; y = 2 } in r.x + r.y\")") +(epoch 1102) +(eval "(ocaml-run \"let r = { x = 1; y = 2 } in let r2 = { r with x = 99 } in r2.x + r2.y\")") +(epoch 1103) +(eval "(ocaml-run \"let p = { name = \\\"Bob\\\"; age = 30 } in p.name\")") +(epoch 1104) +(eval "(ocaml-run \"let p = { name = \\\"Bob\\\"; age = 30 } in p.age\")") +(epoch 1105) +(eval "(ocaml-run-program \"let r = { x = 1; y = 2 };; r.x + r.y\")") + EPOCHS OUTPUT=$(timeout 180 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -1131,6 +1145,14 @@ check 1002 "let f _ = 1 in f 5" '1' check 1003 "top-level let f () =" '7' check 1004 "wildcard top-level" '42' +# ── Records ───────────────────────────────────────────────────── +check 1100 "record literal + access" '1' +check 1101 "record sum fields" '3' +check 1102 "record with-update" '101' +check 1103 "record string field" '"Bob"' +check 1104 "record int field" '30' +check 1105 "top-level record decl" '3' + 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 51b233f8..b52ae339 100644 --- a/plans/ocaml-on-sx.md +++ b/plans/ocaml-on-sx.md @@ -355,6 +355,12 @@ the "mother tongue" closure: OCaml → SX → OCaml. This means: _Newest first._ +- 2026-05-08 Phase 1+2 — record literals `{ x = 1; y = 2 }` and + functional update `{ r with x = 99 }`. Parser produces `(:record (F E) + ...)` and `(:record-update BASE-EXPR (F E) ...)`. Eval builds a dict + from field bindings; record-update merges over the base dict (the same + dict-based representation we already use for modules). Field access + via existing `:field` postfix. Record patterns deferred. 289/289 (+6). - 2026-05-08 Phase 5.1 — `lib/ocaml/conformance.sh` + `scoreboard.json` + `scoreboard.md`. Classifies tests into 14 suites by description prefix and emits structured pass/fail counts. Current: 284 pass / 0