From dbddeeb63fa46431a0d7f41330d0f54359d3885b Mon Sep 17 00:00:00 2001 From: Claude Date: Sun, 24 May 2026 04:44:57 +0000 Subject: [PATCH] =?UTF-8?q?test(stdlib):=20STDLIB-04d=20=E2=80=94=20hermet?= =?UTF-8?q?ic=20e2e=20coverage=20for=20IO=20externs=20(Closes=20#331)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- docs/TECH-DEBT.adoc | 9 ++- test/test_e2e.ml | 132 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 139 insertions(+), 2 deletions(-) diff --git a/docs/TECH-DEBT.adoc b/docs/TECH-DEBT.adoc index eac7b010..3a3e95d9 100644 --- a/docs/TECH-DEBT.adoc +++ b/docs/TECH-DEBT.adoc @@ -187,8 +187,13 @@ round-trips + Deno codegen __cell-shape assertion) |S3 |DONE operator independently lowered. Decide: remove (operator-only) or wire to mirror `++` |S3 |open — issue #330 |STDLIB-04d |IO externs (`print`/`println`/`read_line`/`read_file`/ -`write_file`) — wired on all backends but no dedicated hermetic e2e -tests; test-debt, not impl-debt |S3 |open — issue #331 +`write_file`) — *DONE* (Refs #331): 5 hermetic tests in "E2E +STDLIB-04d IO #331" — `write_file` → `read_file` round-trip on a real +tmpfile, `read_file` on a missing path returns `Err` (not raise), +`print`/`println` exec without error, Deno codegen wires both into the +prelude. `read_line` is interactive and intentionally out of scope (the +TEA-bridge tests already exercise the redirected-stdin path). No impl +change; was test-debt, not impl-debt. |S3 |DONE 2026-05-24 (Refs #331) |STDLIB-04e |Pure externs (`int_to_string`/`string_to_int`/ `string_length`) — real + tested (`lib/interp.ml:615-633`, `lib/codegen_deno.ml:263-272`); bookkeeping to mark DONE |S3 |open — diff --git a/test/test_e2e.ml b/test/test_e2e.ml index 59af9de1..c7b7a406 100644 --- a/test/test_e2e.ml +++ b/test/test_e2e.ml @@ -3279,6 +3279,137 @@ let stdlib_04a_mut_tests = [ Alcotest.test_case "#328 Deno codegen emits __cell shape" `Quick test_stdlib_04a_mut_deno_codegen; ] +(* ---- STDLIB-04d: IO externs hermetic test coverage (Refs #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). This + row adds them. `read_line` is interactive and skipped here — that + surface is exercised by the TEA-bridge tests with redirected stdin. *) + +(* write_file -> read_file round-trip on a real tmpfile *) +let test_stdlib_04d_write_then_read_file () = + let tmp = Filename.temp_file "as_04d_io" ".txt" in + Fun.protect ~finally:(fun () -> if Sys.file_exists tmp then Sys.remove tmp) + (fun () -> + let src = Printf.sprintf {| +fn writer() -> Result { write_file("%s", "hello-04d\n") } +fn reader() -> Result { read_file("%s") } +|} (String.escaped tmp) (String.escaped tmp) in + let prog = Parse_driver.parse_string ~file:"" src in + match Interp.eval_program prog with + | Error e -> Alcotest.failf "interp load failed: %s" + (Value.show_eval_error e) + | Ok env -> + let call name = + match Value.lookup_env name env with + | Error e -> Error e + | Ok fn -> Interp.apply_function fn [] + in + (match call "writer" with + | Ok (Value.VVariant ("Ok", _)) -> () + | Ok v -> Alcotest.failf "writer expected Ok(Unit), got %s" + (Value.show_value v) + | Error e -> Alcotest.failf "writer failed: %s" + (Value.show_eval_error e)); + (match call "reader" with + | Ok (Value.VVariant ("Ok", Some (Value.VString s))) -> + Alcotest.(check string) "reader returns written content" + "hello-04d\n" s + | Ok v -> Alcotest.failf "reader expected Ok(String), got %s" + (Value.show_value v) + | Error e -> Alcotest.failf "reader failed: %s" + (Value.show_eval_error e))) + +(* read_file on a path that does not exist returns Err, not raises. *) +let test_stdlib_04d_read_file_missing () = + let tmp = Filename.temp_file "as_04d_missing" ".txt" in + Sys.remove tmp; (* removed -- guaranteed missing *) + let src = Printf.sprintf + "fn f() -> Result { read_file(\"%s\") }" + (String.escaped tmp) in + let prog = Parse_driver.parse_string ~file:"" src in + match Interp.eval_program prog with + | Error e -> Alcotest.failf "interp failed: %s" (Value.show_eval_error e) + | Ok env -> + (match Value.lookup_env "f" env with + | Ok fn -> + (match Interp.apply_function fn [] with + | Ok (Value.VVariant ("Err", _)) -> () + | Ok v -> Alcotest.failf "expected Err(_), got %s" (Value.show_value v) + | Error e -> Alcotest.failf "apply failed: %s" + (Value.show_eval_error e)) + | Error e -> Alcotest.failf "lookup failed: %s" + (Value.show_eval_error e)) + +(* `print` and `println` exec without error. Stdout-content capture is + intentionally out of scope here (the TEA-bridge tests already + exercise the redirect path with full Unix.dup2 plumbing); we just + prove the lowering doesn't blow up at runtime. *) +let test_stdlib_04d_print_no_error () = + let src = "fn f() -> Unit { print(\"\") }" in + let prog = Parse_driver.parse_string ~file:"" src in + match Interp.eval_program prog with + | Error e -> Alcotest.failf "interp failed: %s" (Value.show_eval_error e) + | Ok env -> + (match Value.lookup_env "f" env with + | Ok fn -> + (match Interp.apply_function fn [] with + | Ok _ -> () + | Error e -> Alcotest.failf "print failed: %s" + (Value.show_eval_error e)) + | Error e -> Alcotest.failf "lookup failed: %s" + (Value.show_eval_error e)) + +let test_stdlib_04d_println_no_error () = + let src = "fn f() -> Unit { println(\"\") }" in + let prog = Parse_driver.parse_string ~file:"" src in + match Interp.eval_program prog with + | Error e -> Alcotest.failf "interp failed: %s" (Value.show_eval_error e) + | Ok env -> + (match Value.lookup_env "f" env with + | Ok fn -> + (match Interp.apply_function fn [] with + | Ok _ -> () + | Error e -> Alcotest.failf "println failed: %s" + (Value.show_eval_error e)) + | Error e -> Alcotest.failf "lookup failed: %s" + (Value.show_eval_error e)) + +(* Deno codegen lowers IO externs to the right host shape. *) +let test_stdlib_04d_io_deno_codegen () = + let src = {| +fn run() -> Unit { + print("a"); + println("b") +} +|} in + let prog = Parse_driver.parse_string ~file:"" src in + let loader = Module_loader.create (Module_loader.default_config ()) in + match Resolve.resolve_program_with_loader prog loader with + | Error (e, _) -> + Alcotest.failf "resolve failed: %s" (Resolve.show_resolve_error e) + | Ok (rctx, _) -> + (match Codegen_deno.codegen_deno prog rctx.symbols with + | Error e -> Alcotest.failf "deno-codegen failed: %s" e + | Ok js -> + let has needle = + let nl = String.length needle and sl = String.length js in + let rec go i = i + nl <= sl && + (String.sub js i nl = needle || go (i + 1)) + in nl = 0 || go 0 + in + Alcotest.(check bool) "prelude defines print/println" + true (has "const print" && has "const println")) + +let stdlib_04d_io_tests = [ + Alcotest.test_case "#331 write_file -> read_file round-trip" `Quick test_stdlib_04d_write_then_read_file; + Alcotest.test_case "#331 read_file on missing path returns Err" `Quick test_stdlib_04d_read_file_missing; + Alcotest.test_case "#331 print exec without error" `Quick test_stdlib_04d_print_no_error; + Alcotest.test_case "#331 println exec without error" `Quick test_stdlib_04d_println_no_error; + Alcotest.test_case "#331 Deno codegen wires print/println" `Quick test_stdlib_04d_io_deno_codegen; +] + (* ---- Issue #35 Phase 2 — Vscode bindings ---- Verifies stdlib/Vscode.affine and stdlib/VscodeLanguageClient.affine @@ -3970,6 +4101,7 @@ let tests = ("E2E Xmod Other Codegens", cross_module_other_codegens_tests); ("E2E Externs", extern_tests); ("E2E STDLIB-04a Mut #328", stdlib_04a_mut_tests); + ("E2E STDLIB-04d IO #331", stdlib_04d_io_tests); ("E2E Vscode Bindings", vscode_bindings_tests); ("E2E Array Type Sugar", array_type_tests); ("E2E Qualified Paths #228", qualified_path_tests);