Skip to content

Commit a22a74c

Browse files
test: inline-extern shape coverage (Refs #346) (#353)
## Summary Adds **class-level coverage** for the "inline extern fed to every downstream consumer" surface that produced the PR #346 `FnExtern` interp bug (`Interp.eval_decl`'s `TopFn` arm missing the `FnExtern` match arm — silent pattern-match failure since the interpreter was written; fired the moment STDLIB-04a's tests became the first to hand an inline `extern fn` to `Interp.eval_program`). Queued as a [comment on PR #346](#346 (comment)) in the previous session; this is the follow-up. ## What this PR adds Four fixtures under `test/e2e/fixtures/`: | Fixture | Shape | |---------|-------| | `inline_extern_pure.affine` | `extern fn host_pure_identity(x: Int) -> Int;` (no effects) | | `inline_extern_effectful.affine` | `extern fn host_log(msg: String) -> Unit / IO;` (effect row) | | `inline_extern_polymorphic.affine` | `extern fn host_identity[T](x: T) -> T;` (type params) | | `inline_extern_type_consumed.affine` | `extern type Handle; extern fn host_use(h: Handle) -> Int;` | Each fed through **parse → resolve → typecheck → interp** via the new `inline_extern_pipeline_ok` helper in `test/test_e2e.ml`. Assertion: all four phases return `Ok`. A regression that re-introduces the silent pattern-match-failure path that broke main between #334 and #346 would fail loudly here instead. Suite registered as `"E2E Inline Extern Shapes (Refs #346)"`. ## Why this matters Per the `.claude/CLAUDE.md` §"Test-fixture hygiene for latent bug surfaces" rule landed this session: when adding a stdlib `extern fn` (or any other new declaration shape), test it against **every downstream consumer** (parse / resolve / typecheck / interp / codegen). PR #346 fixed *one* instance of this class; this PR pins the *class* against the gate so the next latent gap of the same shape (an `extern type` consumed by an `extern fn` in a module that other modules import, etc.) surfaces against CI rather than against the next agent. ## Test plan - [ ] CI `build` job clean - [ ] CI `dune runtest` — four new alcotest cases pass; existing suite unchanged - [ ] No new lints Refs #346, Refs `.claude/CLAUDE.md` §"Test-fixture hygiene for latent bug surfaces" --- _Generated by [Claude Code](https://claude.ai/code/session_01WHUYQEPKgQU6jBgUj4snYU)_ Co-authored-by: Claude <noreply@anthropic.com>
1 parent a00f285 commit a22a74c

5 files changed

Lines changed: 152 additions & 0 deletions

File tree

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 hyperpolymath
3+
//
4+
// Inline-extern fixture (effectful): an `extern fn` declaration
5+
// whose return type carries an effect row. Exercises the same path
6+
// as inline_extern_pure plus the effect-row resolution branch in
7+
// typecheck. Pinned against the gate so a regression to the
8+
// pre-#346 silent-failure shape would fail loudly.
9+
//
10+
// See .claude/CLAUDE.md §"Test-fixture hygiene for latent bug
11+
// surfaces".
12+
13+
effect IO;
14+
15+
extern fn host_log(msg: String) -> Unit / IO;
16+
17+
pub fn main() -> Int {
18+
return 0;
19+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 hyperpolymath
3+
//
4+
// Inline-extern fixture (polymorphic): an `extern fn` declaration
5+
// with type parameters. Exercises the polymorphic-scheme registration
6+
// path that the FnExtern special case in typecheck handles
7+
// (see lib/typecheck.ml `check_fn_decl` `FnExtern` arm).
8+
//
9+
// See .claude/CLAUDE.md §"Test-fixture hygiene for latent bug
10+
// surfaces".
11+
12+
extern fn host_identity[T](x: T) -> T;
13+
14+
pub fn main() -> Int {
15+
return 0;
16+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 hyperpolymath
3+
//
4+
// Inline-extern fixture (pure): an `extern fn` declaration with no
5+
// effect row, fed to the full pipeline (parse → resolve → typecheck
6+
// → interp). Counterpart to the missing `FnExtern` arm in
7+
// Interp.eval_decl that PR #346 fixed; this fixture pins the
8+
// "no-effect inline extern" shape against the gate so a regression
9+
// to the silent-pattern-match-failure path of the kind that broke
10+
// main between #334 and #346 would fail loudly here instead.
11+
//
12+
// See .claude/CLAUDE.md §"Test-fixture hygiene for latent bug
13+
// surfaces" and the comment on PR #346 for the rationale.
14+
15+
extern fn host_pure_identity(x: Int) -> Int;
16+
17+
pub fn main() -> Int {
18+
// The call itself doesn't have to succeed at runtime — the
19+
// interpreter's VBuiltin table doesn't know about
20+
// host_pure_identity — but parse + resolve + typecheck + the
21+
// eval_decl arm for FnExtern MUST all return Ok. The check_program
22+
// test driver asserts that, not the runtime value.
23+
return 0;
24+
}
Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
// SPDX-License-Identifier: MPL-2.0
2+
// SPDX-FileCopyrightText: 2026 hyperpolymath
3+
//
4+
// Inline-extern fixture (extern type + extern fn together):
5+
// an opaque host type declared inline and consumed by an extern
6+
// function in the same unit. Exercises both TyExtern registration
7+
// and the FnExtern path that consumes the resulting type.
8+
//
9+
// See .claude/CLAUDE.md §"Test-fixture hygiene for latent bug
10+
// surfaces".
11+
12+
extern type Handle;
13+
14+
extern fn host_use(h: Handle) -> Int;
15+
16+
pub fn main() -> Int {
17+
return 0;
18+
}

test/test_e2e.ml

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3834,6 +3834,80 @@ let qualified_value_tests = [
38343834
Alcotest.test_case "use Mod as M; M::fn(x) resolves (#178 follow-up)" `Quick test_qualval_coloncolon_alias_call;
38353835
]
38363836

3837+
(* ---- Inline `extern fn` / `extern type` shape-coverage fixtures ----
3838+
Class-level coverage for the "first user of an inline extern shape
3839+
feeds it to every downstream consumer" surface that produced the
3840+
PR #346 FnExtern interp bug (eval_decl missing match arm — survived
3841+
since the interpreter was written, fired the moment STDLIB-04a's
3842+
tests became the first to hand an inline extern fn to
3843+
Interp.eval_program).
3844+
3845+
See .claude/CLAUDE.md §"Test-fixture hygiene for latent bug
3846+
surfaces" for the rationale. Each fixture is fed through
3847+
parse → resolve → typecheck → interp; the assertion is that ALL
3848+
four return Ok. A regression that re-introduces the silent
3849+
pattern-match-failure path of the kind that broke main between
3850+
#334 and #346 would fail loudly here. *)
3851+
3852+
let inline_extern_pipeline_ok path : bool =
3853+
let loader = Module_loader.create {
3854+
Module_loader.stdlib_path = "stdlib";
3855+
search_paths = [];
3856+
current_dir = fixture_dir;
3857+
} in
3858+
match parse_fixture path with
3859+
| Error _ -> false
3860+
| Ok raw ->
3861+
let prog = Resolve.lower_qualified_value_paths raw in
3862+
(match Resolve.resolve_program_with_loader prog loader with
3863+
| Error _ -> false
3864+
| Ok (resolve_ctx, import_type_ctx) ->
3865+
(match Typecheck.check_program
3866+
~import_types:import_type_ctx.Typecheck.name_types
3867+
resolve_ctx.symbols prog with
3868+
| Error _ -> false
3869+
| Ok _ ->
3870+
(* The PR #346 root cause was here: Interp.eval_decl's TopFn
3871+
arm didn't match FnExtern, raising Match_failure. The
3872+
fixtures don't *call* the extern at runtime (the host
3873+
impl isn't registered), but eval_program walks every
3874+
TopFn through eval_decl as part of building the initial
3875+
env — that's the path that fired the missing arm. *)
3876+
(match Interp.eval_program prog with
3877+
| Ok _ -> true
3878+
| Error _ -> false)))
3879+
3880+
let test_inline_extern_pure () =
3881+
Alcotest.(check bool)
3882+
"inline `extern fn host_pure_identity(x: Int) -> Int;` passes the pipeline"
3883+
true
3884+
(inline_extern_pipeline_ok (fixture "inline_extern_pure.affine"))
3885+
3886+
let test_inline_extern_effectful () =
3887+
Alcotest.(check bool)
3888+
"inline `extern fn host_log(msg) -> Unit / IO;` passes the pipeline"
3889+
true
3890+
(inline_extern_pipeline_ok (fixture "inline_extern_effectful.affine"))
3891+
3892+
let test_inline_extern_polymorphic () =
3893+
Alcotest.(check bool)
3894+
"inline `extern fn host_identity[T](x: T) -> T;` passes the pipeline"
3895+
true
3896+
(inline_extern_pipeline_ok (fixture "inline_extern_polymorphic.affine"))
3897+
3898+
let test_inline_extern_type_consumed () =
3899+
Alcotest.(check bool)
3900+
"inline `extern type Handle; extern fn host_use(h: Handle) -> Int;` passes the pipeline"
3901+
true
3902+
(inline_extern_pipeline_ok (fixture "inline_extern_type_consumed.affine"))
3903+
3904+
let inline_extern_shape_tests = [
3905+
Alcotest.test_case "pure (no effects)" `Quick test_inline_extern_pure;
3906+
Alcotest.test_case "effectful (effect row)" `Quick test_inline_extern_effectful;
3907+
Alcotest.test_case "polymorphic (type params)" `Quick test_inline_extern_polymorphic;
3908+
Alcotest.test_case "extern type + consuming fn" `Quick test_inline_extern_type_consumed;
3909+
]
3910+
38373911
(* ---- Type-syntax sugars: fn(...) -> T, Option<T>, (A, B) -> C ---- *)
38383912

38393913
let parse_check_passes src : bool =
@@ -4304,6 +4378,7 @@ let tests =
43044378
("E2E Array Type Sugar", array_type_tests);
43054379
("E2E Qualified Paths #228", qualified_path_tests);
43064380
("E2E Qualified Value #178", qualified_value_tests);
4381+
("E2E Inline Extern Shapes (Refs #346)", inline_extern_shape_tests);
43074382
("E2E WasmGC PatCon Destructure", wasm_gc_patcon_tests);
43084383
("E2E Type Syntax Sugar", type_syntax_sugar_tests);
43094384
]

0 commit comments

Comments
 (0)