diff --git a/lib/ocaml/runtime.sx b/lib/ocaml/runtime.sx index 280a9fc6..56ba3f7e 100644 --- a/lib/ocaml/runtime.sx +++ b/lib/ocaml/runtime.sx @@ -512,36 +512,89 @@ end ;; module Printf = struct - (* 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. *) + (* sprintf walks fmt char-by-char. On '%' it parses optional + flags ('-' for left-justify, '0' for zero-pad), an optional + decimal width, and a final spec letter. Specs supported: + %d %i %u %s %f %c %b %x %X %o (and %% as a literal). + Width pads the formatted argument to at least N characters. *) let sprintf fmt = let n = _string_length fmt in + let is_spec c = + c = \"d\" || c = \"i\" || c = \"u\" || c = \"s\" || c = \"f\" + || c = \"c\" || c = \"b\" || c = \"x\" || c = \"X\" || c = \"o\" + in + let is_digit c = + let k = _char_code c in k >= 48 && k <= 57 + in + let pad s width left zero = + let pad_len = width - _string_length s in + if pad_len <= 0 then s + else + let ch = if zero && (not left) then \"0\" else \" \" in + let rec mk k acc = if k = 0 then acc else mk (k - 1) (acc ^ ch) in + let padding = mk pad_len \"\" in + if left then s ^ padding else padding ^ s + in + (* Skip flag chars from p, returning new pos. Records flags in + shared refs (set above each call). *) + let parse_flags_loop p left_flag zero_flag = + let i = ref p in + let cont = ref true in + while !cont do + if !i < n then + let c = _string_get fmt !i in + if c = \"-\" then (left_flag := true; i := !i + 1) + else if c = \"0\" then (zero_flag := true; i := !i + 1) + else cont := false + else cont := false + done; + !i + in + let parse_width_loop p = + let i = ref p in + let w = ref 0 in + let cont = ref true in + while !cont do + if !i < n then + let c = _string_get fmt !i in + if is_digit c then + (w := !w * 10 + (_char_code c - 48); i := !i + 1) + else cont := false + else cont := false + done; + (!w) * 1000000 + (!i) + 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 = \"i\" || spec = \"s\" - || spec = \"f\" || spec = \"c\" || spec = \"b\" - || spec = \"x\" || spec = \"X\" || spec = \"o\" - || spec = \"u\" then - (fun arg -> - let s = - if spec = \"d\" || spec = \"i\" || spec = \"u\" - then _string_of_int arg - else if spec = \"f\" then _string_of_float arg - else if spec = \"x\" then _int_to_hex_lower arg - else if spec = \"X\" then _int_to_hex_upper arg - else if spec = \"o\" then _int_to_octal 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) + if _string_get fmt (pos + 1) = \"%\" then + walk (pos + 2) (prefix ^ \"%\") + else + let left_flag = ref false in + let zero_flag = ref false in + let after_flags = parse_flags_loop (pos + 1) left_flag zero_flag in + let packed = parse_width_loop after_flags in + let width = packed / 1000000 in + let spec_pos = packed - width * 1000000 in + if spec_pos < n && is_spec (_string_get fmt spec_pos) then + let spec = _string_get fmt spec_pos in + let left = !left_flag in + let zero = !zero_flag in + (fun arg -> + let raw = + if spec = \"d\" || spec = \"i\" || spec = \"u\" + then _string_of_int arg + else if spec = \"f\" then _string_of_float arg + else if spec = \"x\" then _int_to_hex_lower arg + else if spec = \"X\" then _int_to_hex_upper arg + else if spec = \"o\" then _int_to_octal arg + else if spec = \"b\" then + (if arg then \"true\" else \"false\") + else arg + in + let s = pad raw width left zero in + walk (spec_pos + 1) (prefix ^ s)) + else walk (pos + 1) (prefix ^ _string_get fmt pos) else walk (pos + 1) (prefix ^ _string_get fmt pos) in walk 0 \"\" diff --git a/lib/ocaml/test.sh b/lib/ocaml/test.sh index 6501e0bd..6b12658f 100755 --- a/lib/ocaml/test.sh +++ b/lib/ocaml/test.sh @@ -1330,6 +1330,18 @@ cat > "$TMPFILE" << 'EPOCHS' (epoch 5074) (eval "(ocaml-run \"Printf.sprintf \\\"%x %X %o\\\" 255 4096 8\")") +;; ── Printf width specifiers ───────────────────────────────── +(epoch 5080) +(eval "(ocaml-run \"Printf.sprintf \\\"%5d\\\" 42\")") +(epoch 5081) +(eval "(ocaml-run \"Printf.sprintf \\\"%-5d|\\\" 42\")") +(epoch 5082) +(eval "(ocaml-run \"Printf.sprintf \\\"%05d\\\" 42\")") +(epoch 5083) +(eval "(ocaml-run \"Printf.sprintf \\\"%4s\\\" \\\"hi\\\"\")") +(epoch 5084) +(eval "(ocaml-run \"Printf.sprintf \\\"hi=%-3d, hex=%04x\\\" 9 15\")") + EPOCHS OUTPUT=$(timeout 360 "$SX_SERVER" < "$TMPFILE" 2>/dev/null) @@ -2113,6 +2125,13 @@ check 5072 "%X 4096" '"1000"' check 5073 "%o 8" '"10"' check 5074 "%x %X %o multi" '"ff 1000 10"' +# ── Printf width specifiers ───────────────────────────────────── +check 5080 "%5d 42 right-pad" '" 42"' +check 5081 "%-5d| 42 left-pad" '"42 |"' +check 5082 "%05d 42 zero-pad" '"00042"' +check 5083 "%4s hi" '" hi"' +check 5084 "%-3d %04x mixed" '"hi=9 , hex=000f"' + 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 07348aca..6f48af2c 100644 --- a/plans/ocaml-on-sx.md +++ b/plans/ocaml-on-sx.md @@ -407,6 +407,17 @@ _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 width specifiers `%5d` / `%-5d` / + `%05d` / `%4s` etc. (+5 tests, 538 total). Walker now parses + optional `-` (left-align) and `0` (zero-pad) flags after `%`, then + optional decimal width digits, then the spec letter. After + formatting the arg into a base string, pads to the width using + spaces (or zeros if `0` flag and not `-`). Encoded width+spec_pos + return as `width * 1000000 + spec_pos` because the parser does not + yet support tuple destructuring in `let` (TODO: lift that + limitation; for now this round-trips losslessly for any practical + width). Examples: `%5d` 42 = " 42", `%-5d|` 42 = "42 |", + `%05d` 42 = "00042". - 2026-05-09 Phase 6 — Printf.sprintf adds %i, %u (aliases of %d), %x (lowercase hex), %X (uppercase hex), %o (octal) (+5 tests, 533 total). New host primitives `_int_to_hex_lower`, `_int_to_hex_upper`,