From 1b38f89055a3382c61b2706eee45d1fb8e063c79 Mon Sep 17 00:00:00 2001 From: giles Date: Sat, 9 May 2026 00:42:35 +0000 Subject: [PATCH] ocaml: phase 6 Printf.sprintf %d/%s/%f/%c/%b/%% + global string_of_* (+5 tests, 492 total) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replaces the stub sprintf in runtime.sx with a real implementation: walk fmt char-by-char accumulating a prefix; on recognised %X return a one-arg fn that formats the arg and recurses on the rest of fmt. The function self-curries to the spec count — there's no separate arity machinery, just a closure chain. Specs: %d (int), %s (string), %f (float), %c (char/string in our model), %b (bool), %% (literal). Unknown specs pass through. Same expression returns a string (no specs) or a function (>=1 spec) — OCaml proper would reject this; works fine in OCaml-on-SX's dynamic runtime. Also adds top-level aliases: string_of_int = _string_of_int string_of_float = _string_of_float string_of_bool = if b then "true" else "false" int_of_string = _int_of_string Printf.sprintf "x=%d" 42 = "x=42" Printf.sprintf "%s = %d" "answer" 42 = "answer = 42" Printf.sprintf "%d%%" 50 = "50%" --- lib/ocaml/runtime.sx | 39 ++++++++++++++++++++++++++++++++++++--- lib/ocaml/test.sh | 19 +++++++++++++++++++ plans/ocaml-on-sx.md | 12 ++++++++++++ 3 files changed, 67 insertions(+), 3 deletions(-) diff --git a/lib/ocaml/runtime.sx b/lib/ocaml/runtime.sx index 9958cf38..b9fa6373 100644 --- a/lib/ocaml/runtime.sx +++ b/lib/ocaml/runtime.sx @@ -424,8 +424,35 @@ end ;; module Printf = struct - let sprintf fmt = fmt - let printf fmt = print_string fmt + (* sprintf walks fmt, accumulating prefix. When it sees a %X + spec, it returns a function of one arg that substitutes the + arg and recurses on the rest of fmt. With no specs, returns + the bare format string. Specs supported: %d %s %f %c %b + (and %% as a literal). Unknown specs are passed through. *) + let sprintf fmt = + let n = _string_length fmt in + let rec walk pos prefix = + if pos >= n then prefix + else if pos + 1 < n && _string_get fmt pos = \"%\" then + let spec = _string_get fmt (pos + 1) in + if spec = \"%\" then walk (pos + 2) (prefix ^ \"%\") + else if spec = \"d\" || spec = \"s\" || spec = \"f\" + || spec = \"c\" || spec = \"b\" then + (fun arg -> + let s = + if spec = \"d\" then _string_of_int arg + else if spec = \"f\" then _string_of_float arg + else if spec = \"b\" then + (if arg then \"true\" else \"false\") + else arg + in + walk (pos + 2) (prefix ^ s)) + else walk (pos + 1) (prefix ^ _string_get fmt pos) + else walk (pos + 1) (prefix ^ _string_get fmt pos) + in + walk 0 \"\" + + let printf fmt = sprintf fmt end ;; module Stack = struct @@ -643,7 +670,13 @@ | [] -> [] | h :: t -> if mem h b then h :: inter t b else inter t b end - end") + end ;; + + let string_of_int n = _string_of_int n + let string_of_float f = _string_of_float f + let string_of_bool b = if b then \"true\" else \"false\" + let int_of_string s = _int_of_string s + ") (define ocaml-stdlib-loaded false) (define ocaml-stdlib-env nil) diff --git a/lib/ocaml/test.sh b/lib/ocaml/test.sh index 8382b86d..68a5fd2c 100755 --- a/lib/ocaml/test.sh +++ b/lib/ocaml/test.sh @@ -1210,6 +1210,18 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 4932) (eval "(ocaml-run \"try (assert false; 0) with _ -> 99\")") +;; ── Printf.sprintf + global string_of_* ────────────────────── +(epoch 4940) +(eval "(ocaml-run \"Printf.sprintf \\\"hello\\\"\")") +(epoch 4941) +(eval "(ocaml-run \"Printf.sprintf \\\"x=%d\\\" 42\")") +(epoch 4942) +(eval "(ocaml-run \"Printf.sprintf \\\"%s = %d\\\" \\\"answer\\\" 42\")") +(epoch 4943) +(eval "(ocaml-run \"Printf.sprintf \\\"%d%%\\\" 50\")") +(epoch 4944) +(eval "(ocaml-run \"string_of_int 7 ^ \\\"-\\\" ^ string_of_bool true\")") + EPOCHS OUTPUT=$(timeout 360 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -1919,6 +1931,13 @@ check 4930 "assert true; 42" '42' check 4931 "assert (x = 5); x + 1" '6' check 4932 "try (assert false; ...) with" '99' +# ── Printf.sprintf ─────────────────────────────────────────────── +check 4940 "sprintf no args" '"hello"' +check 4941 "sprintf one %d" '"x=42"' +check 4942 "sprintf %s = %d" '"answer = 42"' +check 4943 "sprintf %d%% literal percent" '"50%"' +check 4944 "string_of_int + string_of_b" '"7-true"' + 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 5d124532..bb65f08a 100644 --- a/plans/ocaml-on-sx.md +++ b/plans/ocaml-on-sx.md @@ -407,6 +407,18 @@ _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 6 — Printf.sprintf with %d/%s/%f/%c/%b/%% (+4 + tests) and global `string_of_int`/`string_of_float`/`string_of_bool` + (+1 test). 492 total. sprintf walks fmt char-by-char accumulating + a prefix; on a recognised spec it returns a one-arg fn that formats + the arg and recurses on the rest of fmt — naturally curries to the + right arity since the spec count drives the chain. Dynamic typing + lets us return either a string (no specs) or a function (≥1 spec) + from the same expression, which OCaml proper would reject. + Examples: + Printf.sprintf "x=%d" 42 = "x=42" + Printf.sprintf "%s = %d" "answer" 42 = "answer = 42" + Printf.sprintf "%d%%" 50 = "50%" - 2026-05-09 Phase 4 — `assert EXPR` (+3 tests, 487 total). Tokenizer already classified `assert` as a keyword; parse-prefix now handles it like `not` (advance, recur, wrap). Eval evaluates the operand and