ocaml: phase 4 modules + field access (+11 tests, 215 total)
Some checks failed
Test, Build, and Deploy / test-build-deploy (push) Failing after 54s

module M = struct DECLS end parsed by sub-tokenising the body source
between struct and the matching end (nesting tracked via struct/begin/
sig/end). Field access is a postfix layer above parse-atom, binding
tighter than application: f r.x -> (:app f (:field r "x")).

Eval (:module-def NAME DECLS) builds a dict via ocaml-eval-module
running decls in a sub-env. (:field EXPR NAME) looks up dict fields,
treating (:con NAME) heads as module-name lookups instead of nullary
ctors so M.x works with M as a module.

Phase 4 LOC so far: ~110 lines (well under 2000 budget).
This commit is contained in:
2026-05-08 08:33:34 +00:00
parent 6a1f63f0d1
commit 317f93b2af
4 changed files with 199 additions and 4 deletions

View File

@@ -314,6 +314,23 @@
;; matches its argument against the clauses.
(let ((clauses (nth ast 1)) (captured env))
(fn (arg) (ocaml-match-clauses arg clauses captured))))
((= 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
;; binding rather than evaluating it as a nullary ctor.
(let ((target-ast (nth ast 1)) (fname (nth ast 2)))
(let ((target
(cond
((= (ocaml-tag-of target-ast) "con")
(cond
((ocaml-env-has? env (nth target-ast 1))
(ocaml-env-lookup env (nth target-ast 1)))
(else (list (nth target-ast 1)))))
(else (ocaml-eval target-ast env)))))
(cond
((dict? target) (get target fname))
(else (error
(str "ocaml-eval: not a record/module on .field: " target)))))))
((= tag "for")
;; (:for NAME LO HI DIR BODY) — DIR is "ascend" or "descend".
(let ((name (nth ast 1))
@@ -417,6 +434,64 @@
(else (error
(str "ocaml-eval: unknown AST tag " tag)))))))
;; ocaml-eval-module — evaluate a list of decls in a fresh sub-env layered
;; on top of the parent. Returns a dict mapping each declared name to its
;; value. Used by `module M = struct DECLS end`.
(define ocaml-eval-module
(fn (decls parent-env)
(let ((env parent-env) (result {}))
(begin
(define run-decl
(fn (decl)
(let ((tag (ocaml-tag-of decl)))
(cond
((= tag "def")
(let ((name (nth decl 1)) (params (nth decl 2)) (rhs (nth decl 3)))
(let ((v (if (= (len params) 0)
(ocaml-eval rhs env)
(ocaml-make-curried params rhs env))))
(begin
(set! env (ocaml-env-extend env name v))
(set! result (merge result (dict name v)))))))
((= tag "def-rec")
(let ((name (nth decl 1)) (params (nth decl 2)) (rhs (nth decl 3)))
(let ((rhs-fn?
(or (> (len params) 0)
(= (ocaml-tag-of rhs) "fun")
(= (ocaml-tag-of rhs) "function"))))
(cond
(rhs-fn?
(let ((cell (list nil)))
(let ((env2 (ocaml-env-extend env name
(fn (arg) ((nth cell 0) arg)))))
(let ((v (if (= (len params) 0)
(ocaml-eval rhs env2)
(ocaml-make-curried params rhs env2))))
(begin
(set-nth! cell 0 v)
(set! env env2)
(set! result (merge result (dict name v))))))))
(else
(let ((v (ocaml-eval rhs env)))
(begin
(set! env (ocaml-env-extend env name v))
(set! result (merge result (dict name v))))))))))
((= tag "expr")
(ocaml-eval (nth decl 1) env))
((= tag "module-def")
(let ((mname (nth decl 1)) (mdecls (nth decl 2)))
(let ((mod-val (ocaml-eval-module mdecls env)))
(begin
(set! env (ocaml-env-extend env mname mod-val))
(set! result (merge result (dict mname mod-val)))))))
(else (error (str "ocaml-eval-module: bad decl " tag)))))))
(define loop
(fn (xs)
(when (not (= xs (list)))
(begin (run-decl (first xs)) (loop (rest xs))))))
(loop decls)
result))))
;; ocaml-run — convenience wrapper: parse + eval.
(define ocaml-run
(fn (src)
@@ -466,6 +541,16 @@
(set! last v))))))))
((= tag "expr")
(set! last (ocaml-eval (nth decl 1) env)))
((= tag "module-def")
;; module M = struct DECLS end — evaluate the inner
;; decls in a fresh sub-env layered on the current
;; one, then collect the new bindings into a dict that
;; we bind under M.
(let ((mname (nth decl 1)) (mdecls (nth decl 2)))
(let ((mod-val (ocaml-eval-module mdecls env)))
(begin
(set! env (ocaml-env-extend env mname mod-val))
(set! last mod-val)))))
(else (error (str "ocaml-run-program: bad decl " tag)))))))
(define loop
(fn (xs)

View File

@@ -377,12 +377,32 @@
true)
((and (= tt "op") (or (= tv "(") (= tv "["))) true)
(else false)))))
(define parse-atom-postfix
(fn ()
;; After a primary atom, consume `.field` chains. Field name
;; may be lower (record field, module value) or upper (module
;; or constructor reference). Note: `M.x.y` is left-assoc:
;; `(:field (:field M "x") "y")`.
(let ((head (parse-atom)))
(begin
(define loop
(fn ()
(when (at-op? ".")
(begin
(advance-tok!)
(let ((tok (peek-tok)))
(begin
(advance-tok!)
(set! head (list :field head (ocaml-tok-value tok)))
(loop)))))))
(loop)
head))))
(set!
parse-app
(fn
()
(let
((head (parse-atom)))
((head (parse-atom-postfix)))
(begin
(define
loop
@@ -391,7 +411,7 @@
(when
(at-app-start?)
(let
((arg (parse-atom)))
((arg (parse-atom-postfix)))
(begin (set! head (list :app head arg)) (loop))))))
(loop)
head))))
@@ -767,6 +787,7 @@
((= (ocaml-tok-type (peek-tok)) "eof") nil)
((at-op? ";;") nil)
((at-kw? "let") nil)
((at-kw? "module") nil)
(else (begin (advance-tok!) (skip-to-boundary!))))))
(define
parse-decl-let
@@ -818,6 +839,43 @@
(let
((expr-src (slice src expr-start (cur-pos))))
(let ((expr (ocaml-parse expr-src))) (list :expr expr)))))))
;; module M = struct DECLS end
;; Parsed by sub-tokenising the body source between `struct` and
;; the matching `end`. Nested modules / sigs increment depth.
(define
parse-decl-module
(fn ()
(advance-tok!)
(let ((name (ocaml-tok-value (consume! "ctor" nil))))
(begin
(consume! "op" "=")
(consume! "keyword" "struct")
(let ((body-start (cur-pos)) (depth 1))
(begin
(define skip
(fn ()
(cond
((>= idx tok-len) nil)
((= (ocaml-tok-type (peek-tok)) "eof") nil)
((at-kw? "struct")
(begin (set! depth (+ depth 1)) (advance-tok!) (skip)))
((at-kw? "begin")
(begin (set! depth (+ depth 1)) (advance-tok!) (skip)))
((at-kw? "sig")
(begin (set! depth (+ depth 1)) (advance-tok!) (skip)))
((at-kw? "end")
(cond
((= depth 1) nil)
(else
(begin (set! depth (- depth 1)) (advance-tok!) (skip)))))
(else (begin (advance-tok!) (skip))))))
(skip)
(let ((body-end (cur-pos)))
(begin
(consume! "keyword" "end")
(let ((body-src (slice src body-start body-end)))
(let ((body-prog (ocaml-parse-program body-src)))
(list :module-def name (rest body-prog))))))))))))
(define
loop
(fn
@@ -830,6 +888,8 @@
((= (ocaml-tok-type (peek-tok)) "eof") nil)
((at-kw? "let")
(begin (append! decls (parse-decl-let)) (loop)))
((at-kw? "module")
(begin (append! decls (parse-decl-module)) (loop)))
(else (begin (append! decls (parse-decl-expr)) (loop))))))))
(loop)
(cons :program decls)))))

View File

@@ -531,6 +531,30 @@ cat > "$TMPFILE" << 'EPOCHS'
(epoch 665)
(eval "(ocaml-run \"let f x = if x < 0 then raise (NegArg x) else x * 2 in try f (-5) with | NegArg n -> n\")")
;; ── Phase 4: Modules + field access ────────────────────────────
(epoch 700)
(eval "(ocaml-parse \"M.x\")")
(epoch 701)
(eval "(ocaml-parse \"r.field\")")
(epoch 702)
(eval "(ocaml-parse \"M.M2.x\")")
(epoch 703)
(eval "(ocaml-parse \"f r.x\")")
(epoch 710)
(eval "(ocaml-run-program \"module M = struct let x = 42 end ;; M.x\")")
(epoch 711)
(eval "(ocaml-run-program \"module M = struct let f x = x + 1 end ;; M.f 41\")")
(epoch 712)
(eval "(ocaml-run-program \"module M = struct let x = 1 let y = 2 end ;; M.x + M.y\")")
(epoch 713)
(eval "(ocaml-run-program \"module Math = struct let pi = 3.14 let square x = x * x end ;; Math.square 5\")")
(epoch 714)
(eval "(ocaml-run-program \"module Outer = struct module Inner = struct let v = 99 end end ;; Outer.Inner.v\")")
(epoch 715)
(eval "(ocaml-run-program \"module M = struct let rec fact n = if n = 0 then 1 else n * fact (n - 1) end ;; M.fact 5\")")
(epoch 716)
(eval "(ocaml-run-program \"module Pair = struct let make a b = (a, b) let swap p = match p with | (x, y) -> (y, x) end ;; Pair.swap (Pair.make 1 2)\")")
EPOCHS
OUTPUT=$(timeout 60 "$SX_SERVER" < "$TMPFILE" 2>/dev/null)
@@ -846,6 +870,19 @@ check 663 "try failwith" '"oops"'
check 664 "try sequence raises" '101'
check 665 "raise from function" '-5'
# ── Phase 4: Modules + field access ─────────────────────────────
check 700 "parse M.x" '("field" ("con" "M") "x")'
check 701 "parse r.field" '("field" ("var" "r") "field")'
check 702 "parse M.M2.x left-assoc" '("field" ("field" ("con" "M") "M2") "x")'
check 703 "parse f r.x bind tighter" '("app" ("var" "f") ("field" ("var" "r") "x"))'
check 710 "module M.x = 42" '42'
check 711 "module M.f 41 = 42" '42'
check 712 "module two values" '3'
check 713 "module fn: square 5" '25'
check 714 "nested module Outer.Inner" '99'
check 715 "module rec fact 5" '120'
check 716 "module Pair.swap" '("tuple" 2 1)'
TOTAL=$((PASS + FAIL))
if [ $FAIL -eq 0 ]; then
echo "ok $PASS/$TOTAL OCaml-on-SX tests passed"

View File

@@ -179,7 +179,8 @@ SX CEK evaluator (both JS and OCaml hosts)
### Phase 4 — Modules + functors
- [ ] `module M = struct let x = 1 let f y = x + y end` → SX dict `{:x 1 :f <fn>}`.
- [x] `module M = struct let x = 1 let f y = x + y end` → SX dict
`{"x" 1 "f" <fn>}`.
- [ ] `module type S = sig val x : int val f : int -> int end` → interface record
(runtime stub; typed checking in Phase 5).
- [ ] `module M : S = struct ... end` — coercive sealing (runtime: pass-through).
@@ -187,7 +188,7 @@ SX CEK evaluator (both JS and OCaml hosts)
- [ ] `module F = Functor(Base)` — functor application.
- [ ] `open M` — merge M's dict into current env (`env-merge`).
- [ ] `include M` — same as open at structure level.
- [ ] `M.name` — dict get via `:name` key.
- [x] `M.name` — dict get via field access.
- [ ] First-class modules (pack/unpack) — deferred to Phase 5.
- [ ] Standard module hierarchy: `List`, `Option`, `Result`, `String`, `Char`,
`Int`, `Float`, `Bool`, `Unit`, `Printf`, `Format` (stubs, filled in Phase 6).
@@ -321,6 +322,18 @@ the "mother tongue" closure: OCaml → SX → OCaml. This means:
_Newest first._
- 2026-05-08 Phase 4 — modules + field access (+11 tests, 215 total). Parser:
`module M = struct DECLS end` decl in `ocaml-parse-program`. Body parsed
by sub-tokenising the source between `struct` and the matching `end`,
tracking nesting via `struct`/`begin`/`sig`/`end`. Field access added
as a postfix layer above `parse-atom`, binding tighter than application:
`f r.x``(:app f (:field r "x"))`. Eval: `(:module-def NAME DECLS)`
builds a dict via new `ocaml-eval-module` that runs decls in a sub-env;
`(:field EXPR NAME)` looks up the field, with the special case that
`(:con NAME)` heads are interpreted as module-name lookups instead of
nullary ctors. Tested: simple module, multi-decl module, nested modules
(`Outer.Inner.v`), `let rec` inside a module, module containing tuple
pattern match. Phase 4 LOC: ~110 (well under 2000 budget).
- 2026-05-08 Phase 2 — `try`/`with` + `raise` builtin. Parser produces
`(:try EXPR CLAUSES)`; eval delegates to SX `guard` with `else`
matching the raised value against clause patterns and re-raising on