Skip to content

Commit dbddeeb

Browse files
committed
test(stdlib): STDLIB-04d — hermetic e2e coverage for IO externs (Closes #331)
`print`/`println`/`read_line`/`read_file`/`write_file` were already wired in interp + Deno codegen, but had no dedicated hermetic tests asserting the round-trip semantics. Test-debt, not impl-debt: a silent regression to any of these would slip through the existing gate (the TEA-bridge tests exercise the redirect path but don't assert extern behaviour as a first-class surface). 5 new tests in "E2E STDLIB-04d IO #331": * write_file -> read_file round-trip on a real tmpfile, asserting the string round-trips byte-for-byte (with tmpfile cleanup via Fun.protect) * read_file on a missing path returns Err(_) (not raise) * print exec without error * println exec without error * Deno codegen wires both print + println into the emitted prelude `read_line` is interactive and intentionally out of scope here — the TEA-bridge tests already exercise that surface with full Unix.dup2 stdin redirection. No implementation change. Updates `docs/TECH-DEBT.adoc` row 04d → DONE per the audit-split contract. Closes #331. Refs #175.
1 parent f45afa1 commit dbddeeb

2 files changed

Lines changed: 139 additions & 2 deletions

File tree

docs/TECH-DEBT.adoc

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -187,8 +187,13 @@ round-trips + Deno codegen __cell-shape assertion) |S3 |DONE
187187
operator independently lowered. Decide: remove (operator-only) or wire
188188
to mirror `++` |S3 |open — issue #330
189189
|STDLIB-04d |IO externs (`print`/`println`/`read_line`/`read_file`/
190-
`write_file`) — wired on all backends but no dedicated hermetic e2e
191-
tests; test-debt, not impl-debt |S3 |open — issue #331
190+
`write_file`) — *DONE* (Refs #331): 5 hermetic tests in "E2E
191+
STDLIB-04d IO #331" — `write_file` → `read_file` round-trip on a real
192+
tmpfile, `read_file` on a missing path returns `Err` (not raise),
193+
`print`/`println` exec without error, Deno codegen wires both into the
194+
prelude. `read_line` is interactive and intentionally out of scope (the
195+
TEA-bridge tests already exercise the redirected-stdin path). No impl
196+
change; was test-debt, not impl-debt. |S3 |DONE 2026-05-24 (Refs #331)
192197
|STDLIB-04e |Pure externs (`int_to_string`/`string_to_int`/
193198
`string_length`) — real + tested (`lib/interp.ml:615-633`,
194199
`lib/codegen_deno.ml:263-272`); bookkeeping to mark DONE |S3 |open —

test/test_e2e.ml

Lines changed: 132 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3279,6 +3279,137 @@ 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-04d: IO externs hermetic test coverage (Refs #331) ----
3283+
3284+
`print`/`println`/`read_line`/`read_file`/`write_file` were already
3285+
wired in interp + Deno codegen, but had no dedicated hermetic tests
3286+
asserting the round-trip semantics (test-debt, not impl-debt). This
3287+
row adds them. `read_line` is interactive and skipped here — that
3288+
surface is exercised by the TEA-bridge tests with redirected stdin. *)
3289+
3290+
(* write_file -> read_file round-trip on a real tmpfile *)
3291+
let test_stdlib_04d_write_then_read_file () =
3292+
let tmp = Filename.temp_file "as_04d_io" ".txt" in
3293+
Fun.protect ~finally:(fun () -> if Sys.file_exists tmp then Sys.remove tmp)
3294+
(fun () ->
3295+
let src = Printf.sprintf {|
3296+
fn writer() -> Result<Unit, String> { write_file("%s", "hello-04d\n") }
3297+
fn reader() -> Result<String, String> { read_file("%s") }
3298+
|} (String.escaped tmp) (String.escaped tmp) in
3299+
let prog = Parse_driver.parse_string ~file:"<test>" src in
3300+
match Interp.eval_program prog with
3301+
| Error e -> Alcotest.failf "interp load failed: %s"
3302+
(Value.show_eval_error e)
3303+
| Ok env ->
3304+
let call name =
3305+
match Value.lookup_env name env with
3306+
| Error e -> Error e
3307+
| Ok fn -> Interp.apply_function fn []
3308+
in
3309+
(match call "writer" with
3310+
| Ok (Value.VVariant ("Ok", _)) -> ()
3311+
| Ok v -> Alcotest.failf "writer expected Ok(Unit), got %s"
3312+
(Value.show_value v)
3313+
| Error e -> Alcotest.failf "writer failed: %s"
3314+
(Value.show_eval_error e));
3315+
(match call "reader" with
3316+
| Ok (Value.VVariant ("Ok", Some (Value.VString s))) ->
3317+
Alcotest.(check string) "reader returns written content"
3318+
"hello-04d\n" s
3319+
| Ok v -> Alcotest.failf "reader expected Ok(String), got %s"
3320+
(Value.show_value v)
3321+
| Error e -> Alcotest.failf "reader failed: %s"
3322+
(Value.show_eval_error e)))
3323+
3324+
(* read_file on a path that does not exist returns Err, not raises. *)
3325+
let test_stdlib_04d_read_file_missing () =
3326+
let tmp = Filename.temp_file "as_04d_missing" ".txt" in
3327+
Sys.remove tmp; (* removed -- guaranteed missing *)
3328+
let src = Printf.sprintf
3329+
"fn f() -> Result<String, String> { read_file(\"%s\") }"
3330+
(String.escaped tmp) in
3331+
let prog = Parse_driver.parse_string ~file:"<test>" src in
3332+
match Interp.eval_program prog with
3333+
| Error e -> Alcotest.failf "interp failed: %s" (Value.show_eval_error e)
3334+
| Ok env ->
3335+
(match Value.lookup_env "f" env with
3336+
| Ok fn ->
3337+
(match Interp.apply_function fn [] with
3338+
| Ok (Value.VVariant ("Err", _)) -> ()
3339+
| Ok v -> Alcotest.failf "expected Err(_), got %s" (Value.show_value v)
3340+
| Error e -> Alcotest.failf "apply failed: %s"
3341+
(Value.show_eval_error e))
3342+
| Error e -> Alcotest.failf "lookup failed: %s"
3343+
(Value.show_eval_error e))
3344+
3345+
(* `print` and `println` exec without error. Stdout-content capture is
3346+
intentionally out of scope here (the TEA-bridge tests already
3347+
exercise the redirect path with full Unix.dup2 plumbing); we just
3348+
prove the lowering doesn't blow up at runtime. *)
3349+
let test_stdlib_04d_print_no_error () =
3350+
let src = "fn f() -> Unit { print(\"\") }" in
3351+
let prog = Parse_driver.parse_string ~file:"<test>" src in
3352+
match Interp.eval_program prog with
3353+
| Error e -> Alcotest.failf "interp failed: %s" (Value.show_eval_error e)
3354+
| Ok env ->
3355+
(match Value.lookup_env "f" env with
3356+
| Ok fn ->
3357+
(match Interp.apply_function fn [] with
3358+
| Ok _ -> ()
3359+
| Error e -> Alcotest.failf "print failed: %s"
3360+
(Value.show_eval_error e))
3361+
| Error e -> Alcotest.failf "lookup failed: %s"
3362+
(Value.show_eval_error e))
3363+
3364+
let test_stdlib_04d_println_no_error () =
3365+
let src = "fn f() -> Unit { println(\"\") }" in
3366+
let prog = Parse_driver.parse_string ~file:"<test>" src in
3367+
match Interp.eval_program prog with
3368+
| Error e -> Alcotest.failf "interp failed: %s" (Value.show_eval_error e)
3369+
| Ok env ->
3370+
(match Value.lookup_env "f" env with
3371+
| Ok fn ->
3372+
(match Interp.apply_function fn [] with
3373+
| Ok _ -> ()
3374+
| Error e -> Alcotest.failf "println failed: %s"
3375+
(Value.show_eval_error e))
3376+
| Error e -> Alcotest.failf "lookup failed: %s"
3377+
(Value.show_eval_error e))
3378+
3379+
(* Deno codegen lowers IO externs to the right host shape. *)
3380+
let test_stdlib_04d_io_deno_codegen () =
3381+
let src = {|
3382+
fn run() -> Unit {
3383+
print("a");
3384+
println("b")
3385+
}
3386+
|} in
3387+
let prog = Parse_driver.parse_string ~file:"<test>" src in
3388+
let loader = Module_loader.create (Module_loader.default_config ()) in
3389+
match Resolve.resolve_program_with_loader prog loader with
3390+
| Error (e, _) ->
3391+
Alcotest.failf "resolve failed: %s" (Resolve.show_resolve_error e)
3392+
| Ok (rctx, _) ->
3393+
(match Codegen_deno.codegen_deno prog rctx.symbols with
3394+
| Error e -> Alcotest.failf "deno-codegen failed: %s" e
3395+
| Ok js ->
3396+
let has needle =
3397+
let nl = String.length needle and sl = String.length js in
3398+
let rec go i = i + nl <= sl &&
3399+
(String.sub js i nl = needle || go (i + 1))
3400+
in nl = 0 || go 0
3401+
in
3402+
Alcotest.(check bool) "prelude defines print/println"
3403+
true (has "const print" && has "const println"))
3404+
3405+
let stdlib_04d_io_tests = [
3406+
Alcotest.test_case "#331 write_file -> read_file round-trip" `Quick test_stdlib_04d_write_then_read_file;
3407+
Alcotest.test_case "#331 read_file on missing path returns Err" `Quick test_stdlib_04d_read_file_missing;
3408+
Alcotest.test_case "#331 print exec without error" `Quick test_stdlib_04d_print_no_error;
3409+
Alcotest.test_case "#331 println exec without error" `Quick test_stdlib_04d_println_no_error;
3410+
Alcotest.test_case "#331 Deno codegen wires print/println" `Quick test_stdlib_04d_io_deno_codegen;
3411+
]
3412+
32823413
(* ---- Issue #35 Phase 2 — Vscode bindings ----
32833414
32843415
Verifies stdlib/Vscode.affine and stdlib/VscodeLanguageClient.affine
@@ -3970,6 +4101,7 @@ let tests =
39704101
("E2E Xmod Other Codegens", cross_module_other_codegens_tests);
39714102
("E2E Externs", extern_tests);
39724103
("E2E STDLIB-04a Mut #328", stdlib_04a_mut_tests);
4104+
("E2E STDLIB-04d IO #331", stdlib_04d_io_tests);
39734105
("E2E Vscode Bindings", vscode_bindings_tests);
39744106
("E2E Array Type Sugar", array_type_tests);
39754107
("E2E Qualified Paths #228", qualified_path_tests);

0 commit comments

Comments
 (0)