Skip to content

Commit 1938289

Browse files
feat(stdlib): STDLIB-04e — wire string_to_int alias + lock pure-extern semantics (Closes #332) (#338)
## Summary Originally scoped as bookkeeping ("real + tested, close-as-done"), but re-auditing caught the same trap STDLIB-04c removed: `string_to_int` declared in `stdlib/effects.affine:33` was **unwired dead surface** — no entry in `interp.ml`, `codegen_deno.ml`, `resolve.ml` seed, or `typecheck.ml` binding. Any caller of `use effects::{string_to_int}` would compile and fail at run. ## Fix Wire `string_to_int` as the typed-alias of `parse_int` (canonical name) — same `String -> Option<Int>` signature, same OCaml impl, same `__as_parseInt` host shim. Touches all four registration points so the alias is first-class. ## Tests 4 hermetic tests in `E2E STDLIB-04e Pure #332` locking the round-trip semantics for all three pure externs: | Test | Assertion | |---|---| | `int_to_string(42)` | == `"42"` | | `string_to_int("123")` | == `Some(123)` | | `string_to_int("abc")` | == `None` | | `string_length("hello")` | == `5` | ## Test plan - [x] 4 new hermetic tests added - [ ] CI: `dune runtest` green (stdlib AOT gate untouched; e2e gate +4) - [ ] Hypatia DOC-FORMAT: no `.md` introduced Updates `docs/TECH-DEBT.adoc` row 04e → DONE (notes the dead-surface discovery for posterity). Closes #332. Refs #175. --- _Generated by [Claude Code](https://claude.ai/code/session_01NUHL3MH3yKKQAEhSZn4Thu)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent a29f511 commit 1938289

6 files changed

Lines changed: 103 additions & 3 deletions

File tree

docs/TECH-DEBT.adoc

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -213,9 +213,13 @@ to mirror `++` |S3 |open — issue #330
213213
`write_file`) — wired on all backends but no dedicated hermetic e2e
214214
tests; test-debt, not impl-debt |S3 |open — issue #331
215215
|STDLIB-04e |Pure externs (`int_to_string`/`string_to_int`/
216-
`string_length`) — real + tested (`lib/interp.ml:615-633`,
217-
`lib/codegen_deno.ml:263-272`); bookkeeping to mark DONE |S3 |open —
218-
issue #332
216+
`string_length`) — *DONE* (Refs #332): `int_to_string` +
217+
`string_length` were already real-wired; `string_to_int` was unwired
218+
dead surface (re-audit caught the gap the initial sweep missed — same
219+
trap STDLIB-04c removed). Now the typed-alias of `parse_int` in interp
220+
+ Deno codegen + resolve/typecheck seeds. 4 hermetic tests in "E2E
221+
STDLIB-04e Pure #332" lock the round-trip semantics. |S3 |DONE
222+
2026-05-24 (Refs #332)
219223
|===
220224

221225
== Section D — INT (ecosystem integration)

lib/codegen_deno.ml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -269,6 +269,10 @@ let () =
269269
b "trim" (fun a -> Printf.sprintf "String(%s).trim()" (arg 0 a));
270270
b "int_to_string" (fun a -> Printf.sprintf "String(%s)" (arg 0 a));
271271
b "float_to_string" (fun a -> Printf.sprintf "String(%s)" (arg 0 a));
272+
(* STDLIB-04e (Refs #332): `string_to_int` is the typed-alias of
273+
`parse_int` declared in stdlib/effects.affine. Same `__as_parseInt`
274+
host shim, returns Option<Int>. *)
275+
b "string_to_int" (fun a -> Printf.sprintf "__as_parseInt(%s)" (arg 0 a));
272276
b "parse_int" (fun a -> Printf.sprintf "__as_parseInt(%s)" (arg 0 a));
273277
b "parse_float" (fun a -> Printf.sprintf "__as_parseFloat(%s)" (arg 0 a));
274278
b "char_to_int" (fun a -> Printf.sprintf "__as_charToInt(%s)" (arg 0 a));

lib/interp.ml

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -623,6 +623,18 @@ let create_initial_env () : env =
623623
| [VFloat f] -> Ok (VString (string_of_float f))
624624
| _ -> Error (TypeMismatch "float_to_string expects Float")
625625
));
626+
(* STDLIB-04e (Refs #332): `string_to_int` is the typed-alias surface
627+
declared in stdlib/effects.affine. Same semantics as `parse_int`
628+
(the canonical name) — the alias was unwired before this PR, so
629+
any caller of the documented extern would compile and fail at run. *)
630+
("string_to_int", VBuiltin ("string_to_int", fun args ->
631+
match args with
632+
| [VString s] ->
633+
(match int_of_string_opt s with
634+
| Some n -> Ok (VVariant ("Some", Some (VInt n)))
635+
| None -> Ok (VVariant ("None", None)))
636+
| _ -> Error (TypeMismatch "string_to_int expects String")
637+
));
626638
("parse_int", VBuiltin ("parse_int", fun args ->
627639
match args with
628640
| [VString s] ->

lib/resolve.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,8 @@ let seed_builtins (symbols : Symbol.t) : unit =
6262
def "char_to_int"; def "int_to_char"; def "show";
6363
def "to_lowercase"; def "to_uppercase"; def "trim";
6464
def "int_to_string"; def "float_to_string"; def "string_length";
65+
(* STDLIB-04e (Refs #332): `string_to_int` typed-alias for `parse_int` *)
66+
def "string_to_int";
6567
def "parse_int"; def "parse_float";
6668
(* Numeric coercions and math *)
6769
def "int"; def "float";

lib/typecheck.ml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1421,6 +1421,8 @@ let register_builtins (ctx : context) : unit =
14211421
bind_var ctx "to_uppercase" (TArrow (ty_string, QOmega, ty_string, EPure));
14221422
bind_var ctx "trim" (TArrow (ty_string, QOmega, ty_string, EPure));
14231423
bind_var ctx "parse_int" (TArrow (ty_string, QOmega, opt ty_int, EPure));
1424+
(* STDLIB-04e (Refs #332): `string_to_int` typed-alias of `parse_int`. *)
1425+
bind_var ctx "string_to_int" (TArrow (ty_string, QOmega, opt ty_int, EPure));
14241426
bind_var ctx "parse_float" (TArrow (ty_string, QOmega, opt ty_float, EPure));
14251427
bind_var ctx "char_to_int" (TArrow (ty_char, QOmega, ty_int, EPure));
14261428
bind_var ctx "int_to_char" (TArrow (ty_int, QOmega, ty_char, EPure));

test/test_e2e.ml

Lines changed: 76 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3279,6 +3279,81 @@ let stdlib_04a_mut_tests = [
32793279
Alcotest.test_case "#328 Deno codegen emits __cell shape" `Quick test_stdlib_04a_mut_deno_codegen;
32803280
]
32813281

3282+
(* ---- STDLIB-04e: Pure externs (Refs #332) ----
3283+
3284+
Three externs declared in stdlib/effects.affine as pure:
3285+
int_to_string : Int -> String
3286+
string_to_int : String -> Option<Int>
3287+
string_length : String -> Int
3288+
3289+
`int_to_string` + `string_length` were already wired; `string_to_int`
3290+
was unwired (dead surface — any caller would compile and fail at run).
3291+
This row wires `string_to_int` as the typed-alias of `parse_int` and
3292+
asserts hermetic round-trip semantics for all three. *)
3293+
3294+
let test_stdlib_04e_int_to_string () =
3295+
let prog = Parse_driver.parse_string ~file:"<test>"
3296+
"fn f() -> String { int_to_string(42) }" in
3297+
match Interp.eval_program prog with
3298+
| Error e -> Alcotest.failf "interp failed: %s" (Value.show_eval_error e)
3299+
| Ok env ->
3300+
(match Value.lookup_env "f" env with
3301+
| Ok fn ->
3302+
(match Interp.apply_function fn [] with
3303+
| Ok (Value.VString "42") -> ()
3304+
| Ok v -> Alcotest.failf "expected VString \"42\", got %s"
3305+
(Value.show_value v)
3306+
| Error e -> Alcotest.failf "apply failed: %s" (Value.show_eval_error e))
3307+
| Error e -> Alcotest.failf "lookup f failed: %s" (Value.show_eval_error e))
3308+
3309+
let test_stdlib_04e_string_to_int_some () =
3310+
let prog = Parse_driver.parse_string ~file:"<test>"
3311+
"fn f() -> Option<Int> { string_to_int(\"123\") }" in
3312+
match Interp.eval_program prog with
3313+
| Error e -> Alcotest.failf "interp failed: %s" (Value.show_eval_error e)
3314+
| Ok env ->
3315+
(match Value.lookup_env "f" env with
3316+
| Ok fn ->
3317+
(match Interp.apply_function fn [] with
3318+
| Ok (Value.VVariant ("Some", Some (Value.VInt 123))) -> ()
3319+
| Ok v -> Alcotest.failf "expected Some(123), got %s"
3320+
(Value.show_value v)
3321+
| Error e -> Alcotest.failf "apply failed: %s" (Value.show_eval_error e))
3322+
| Error e -> Alcotest.failf "lookup f failed: %s" (Value.show_eval_error e))
3323+
3324+
let test_stdlib_04e_string_to_int_none () =
3325+
let prog = Parse_driver.parse_string ~file:"<test>"
3326+
"fn f() -> Option<Int> { string_to_int(\"abc\") }" in
3327+
match Interp.eval_program prog with
3328+
| Error e -> Alcotest.failf "interp failed: %s" (Value.show_eval_error e)
3329+
| Ok env ->
3330+
(match Value.lookup_env "f" env with
3331+
| Ok fn ->
3332+
(match Interp.apply_function fn [] with
3333+
| Ok (Value.VVariant ("None", None)) -> ()
3334+
| Ok v -> Alcotest.failf "expected None, got %s" (Value.show_value v)
3335+
| Error e -> Alcotest.failf "apply failed: %s" (Value.show_eval_error e))
3336+
| Error e -> Alcotest.failf "lookup f failed: %s" (Value.show_eval_error e))
3337+
3338+
let test_stdlib_04e_string_length () =
3339+
let prog = Parse_driver.parse_string ~file:"<test>"
3340+
"fn f() -> Int { string_length(\"hello\") }" in
3341+
match Interp.eval_program prog with
3342+
| Error e -> Alcotest.failf "interp failed: %s" (Value.show_eval_error e)
3343+
| Ok env ->
3344+
(match Value.lookup_env "f" env with
3345+
| Ok fn ->
3346+
(match Interp.apply_function fn [] with
3347+
| Ok (Value.VInt 5) -> ()
3348+
| Ok v -> Alcotest.failf "expected VInt 5, got %s" (Value.show_value v)
3349+
| Error e -> Alcotest.failf "apply failed: %s" (Value.show_eval_error e))
3350+
| Error e -> Alcotest.failf "lookup f failed: %s" (Value.show_eval_error e))
3351+
3352+
let stdlib_04e_pure_tests = [
3353+
Alcotest.test_case "#332 int_to_string(42) == \"42\"" `Quick test_stdlib_04e_int_to_string;
3354+
Alcotest.test_case "#332 string_to_int(\"123\") == Some(123)" `Quick test_stdlib_04e_string_to_int_some;
3355+
Alcotest.test_case "#332 string_to_int(\"abc\") == None" `Quick test_stdlib_04e_string_to_int_none;
3356+
Alcotest.test_case "#332 string_length(\"hello\") == 5" `Quick test_stdlib_04e_string_length;
32823357
(* ---- STDLIB-04b: Throws extern `error<T>` (Refs #329) ----
32833358
32843359
`error<T>(msg: String) -> T / Throws` was declared in
@@ -4054,6 +4129,7 @@ let tests =
40544129
("E2E Xmod Other Codegens", cross_module_other_codegens_tests);
40554130
("E2E Externs", extern_tests);
40564131
("E2E STDLIB-04a Mut #328", stdlib_04a_mut_tests);
4132+
("E2E STDLIB-04e Pure #332", stdlib_04e_pure_tests);
40574133
("E2E STDLIB-04b error #329", stdlib_04b_error_tests);
40584134
("E2E Vscode Bindings", vscode_bindings_tests);
40594135
("E2E Array Type Sugar", array_type_tests);

0 commit comments

Comments
 (0)