Skip to content

Commit 23cd61f

Browse files
committed
feat(stdlib): STDLIB-04e — wire string_to_int alias + lock pure-extern semantics (Closes #332)
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 was unwired dead surface — no entry in interp, codegen_deno, resolve, or typecheck. 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 semantics (`String -> Option<Int>`), same OCaml impl, same `__as_parseInt` host shim. Adds 4 hermetic tests covering all three externs' round-trip semantics ("E2E STDLIB-04e Pure #332"): * int_to_string(42) == "42" * string_to_int("123") == Some(123) * string_to_int("abc") == None * string_length("hello") == 5 Updates `docs/TECH-DEBT.adoc` row 04e to DONE per the audit-split contract (notes the dead-surface discovery for posterity). Closes #332. Refs #175.
1 parent f45afa1 commit 23cd61f

6 files changed

Lines changed: 105 additions & 3 deletions

File tree

docs/TECH-DEBT.adoc

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

198202
== 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
@@ -1413,6 +1413,8 @@ let register_builtins (ctx : context) : unit =
14131413
bind_var ctx "to_uppercase" (TArrow (ty_string, QOmega, ty_string, EPure));
14141414
bind_var ctx "trim" (TArrow (ty_string, QOmega, ty_string, EPure));
14151415
bind_var ctx "parse_int" (TArrow (ty_string, QOmega, opt ty_int, EPure));
1416+
(* STDLIB-04e (Refs #332): `string_to_int` typed-alias of `parse_int`. *)
1417+
bind_var ctx "string_to_int" (TArrow (ty_string, QOmega, opt ty_int, EPure));
14161418
bind_var ctx "parse_float" (TArrow (ty_string, QOmega, opt ty_float, EPure));
14171419
bind_var ctx "char_to_int" (TArrow (ty_char, QOmega, ty_int, EPure));
14181420
bind_var ctx "int_to_char" (TArrow (ty_int, QOmega, ty_char, EPure));

test/test_e2e.ml

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3279,6 +3279,83 @@ 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;
3357+
]
3358+
32823359
(* ---- Issue #35 Phase 2 — Vscode bindings ----
32833360
32843361
Verifies stdlib/Vscode.affine and stdlib/VscodeLanguageClient.affine
@@ -3970,6 +4047,7 @@ let tests =
39704047
("E2E Xmod Other Codegens", cross_module_other_codegens_tests);
39714048
("E2E Externs", extern_tests);
39724049
("E2E STDLIB-04a Mut #328", stdlib_04a_mut_tests);
4050+
("E2E STDLIB-04e Pure #332", stdlib_04e_pure_tests);
39734051
("E2E Vscode Bindings", vscode_bindings_tests);
39744052
("E2E Array Type Sugar", array_type_tests);
39754053
("E2E Qualified Paths #228", qualified_path_tests);

0 commit comments

Comments
 (0)