From 9907c1c58ca10a9e95f424488451b494b5c12009 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 9 May 2026 01:03:40 +0000 Subject: [PATCH] ocaml: phase 4 'lazy EXPR' + Lazy.force (+2 tests, 496 total) Tokenizer already had 'lazy' as a keyword. This commit wires it through: parser : parse-prefix emits (:lazy EXPR), like the existing 'assert' handler. eval : creates a one-element cell with state ('Thunk' expr env). host : _lazy_force flips the cell to ('Forced' v) on first call and returns the cached value thereafter. runtime : module Lazy = struct let force lz = _lazy_force lz end. Memoisation confirmed by tracking a side-effect counter through two forces of the same lazy: let counter = ref 0 in let lz = lazy (counter := !counter + 1; 42) in let a = Lazy.force lz in let b = Lazy.force lz in (a + b) * 100 + !counter = 8401 (= 84*100 + 1) --- lib/ocaml/eval.sx | 19 +++++++++++++++++++ lib/ocaml/parser.sx | 2 ++ lib/ocaml/runtime.sx | 4 ++++ lib/ocaml/test.sh | 10 ++++++++++ plans/ocaml-on-sx.md | 7 +++++++ 5 files changed, 42 insertions(+) diff --git a/lib/ocaml/eval.sx b/lib/ocaml/eval.sx index c88b73ff..2171a545 100644 --- a/lib/ocaml/eval.sx +++ b/lib/ocaml/eval.sx @@ -116,6 +116,20 @@ (fn (t) (fn (k) (has-key? (nth t 0) (str k))))) (list "_hashtbl_length" (fn (t) (len (keys (nth t 0))))) + ;; _lazy_force: evaluate the thunk on first force, cache result. + ;; cell: one-elt list whose value is ("Thunk" expr env) or + ;; ("Forced" v). + (list "_lazy_force" + (fn (cell) + (let ((state (nth cell 0))) + (cond + ((= (first state) "Forced") (nth state 1)) + (else + (let ((expr (nth state 1)) (env (nth state 2))) + (let ((v (ocaml-eval expr env))) + (begin + (set-nth! cell 0 (list "Forced" v)) + v)))))))) ;; _hashtbl_to_list: returns [(k, v); ...] as a list of pairs. ;; Keys are returned as the stringified form used internally. (list "_hashtbl_to_list" @@ -451,6 +465,11 @@ (cond ((= v false) (error "Assert_failure")) (else nil)))) + ((= tag "lazy") + ;; (:lazy EXPR) — create a one-element cell containing + ;; ("Thunk" EXPR env); _lazy_force evaluates and caches. + (let ((expr (nth ast 1))) + (list (list "Thunk" expr env)))) ((= tag "deref") (let ((cell (ocaml-eval (nth ast 1) env))) (nth cell 0))) diff --git a/lib/ocaml/parser.sx b/lib/ocaml/parser.sx index 806d1963..23007963 100644 --- a/lib/ocaml/parser.sx +++ b/lib/ocaml/parser.sx @@ -624,6 +624,8 @@ (begin (advance-tok!) (list :not (parse-prefix)))) ((at-kw? "assert") (begin (advance-tok!) (list :assert (parse-prefix)))) + ((at-kw? "lazy") + (begin (advance-tok!) (list :lazy (parse-prefix)))) (else (parse-app))))) (set! parse-binop-rhs diff --git a/lib/ocaml/runtime.sx b/lib/ocaml/runtime.sx index 3b269af4..02bc9597 100644 --- a/lib/ocaml/runtime.sx +++ b/lib/ocaml/runtime.sx @@ -455,6 +455,10 @@ let printf fmt = sprintf fmt end ;; + module Lazy = struct + let force lz = _lazy_force lz + end ;; + module Stack = struct let create () = ref [] let push x s = s := x :: !s diff --git a/lib/ocaml/test.sh b/lib/ocaml/test.sh index cea5865b..4092275f 100755 --- a/lib/ocaml/test.sh +++ b/lib/ocaml/test.sh @@ -1228,6 +1228,12 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 4951) (eval "(ocaml-run \"let t = Hashtbl.create 4 in Hashtbl.add t \\\"x\\\" 10; Hashtbl.add t \\\"y\\\" 20; let total = ref 0 in Hashtbl.iter (fun _ v -> total := !total + v) t; !total\")") +;; ── lazy / Lazy.force ───────────────────────────────────────── +(epoch 4960) +(eval "(ocaml-run \"let x = lazy (1 + 2) in Lazy.force x\")") +(epoch 4961) +(eval "(ocaml-run \"let counter = ref 0 in let lz = lazy (counter := !counter + 1; 42) in let a = Lazy.force lz in let b = Lazy.force lz in (a + b) * 100 + !counter\")") + EPOCHS OUTPUT=$(timeout 360 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -1948,6 +1954,10 @@ check 4944 "string_of_int + string_of_b" '"7-true"' check 4950 "Hashtbl.fold sum 1+2+3" '6' check 4951 "Hashtbl.iter ref accum 10+20" '30' +# ── lazy / Lazy.force ───────────────────────────────────────── +check 4960 "lazy 1+2 force" '3' +check 4961 "lazy memoization counter=1" '8401' + 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 ab95d4e4..66de51c2 100644 --- a/plans/ocaml-on-sx.md +++ b/plans/ocaml-on-sx.md @@ -407,6 +407,13 @@ _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 — `lazy EXPR` + `Lazy.force` (+2 tests, 496 + total). Tokenizer already had `lazy` as a keyword. parse-prefix now + emits `(:lazy EXPR)`; eval creates a one-element cell with state + `("Thunk" expr env)`. Host primitive `_lazy_force` flips the cell to + `("Forced" v)` on first call and returns the cached value on + subsequent calls. Memoization confirmed by tracking a side-effect + counter through two forces (counter increments only once). - 2026-05-09 Phase 6 — Hashtbl.iter / Hashtbl.fold (+2 tests, 494 total). New host primitive `_hashtbl_to_list` returns the entries as a list of OCaml tuples (`("tuple" k v)` form, matching the AST