Skip to content

Commit f61034b

Browse files
test(borrow): #225 PR3b — affine-capture obligation discharged by composition (#237)
ADR-013 obligation 1 (a linear/owned local captured into the continuation env is consumed exactly once; double-resumption impossible) requires NO transform-aware static machinery. It is discharged by composition: (a) the borrow checker runs on the straight-line AST BEFORE the codegen CPS transform (verified pipeline order, bin/main.ml), so a double consume of an owned local across the async let is rejected there exactly as in any synchronous body; and (b) the PR2 once-resumption trap guarantees the continuation runs at most once, so the transformed code performs each consume the same number of times the checker already verified. This commit pins (a) with two regressions in test_effects.ml (frontend_borrow = parse->resolve->typecheck->Borrow.check_program): - owned local captured across the async split + consumed TWICE ⇒ use-after-move, rejected by the existing checker (no new rule); - the same capture shape consumed ONCE ⇒ accepted (the rule is not over-rejecting the captured-once case the transform relies on). The rationale is documented in-code (test_effects.ml block comment; the codegen recogniser docstring already states it from PR3a). No compiler/codegen change. dune test --force 260 (was 258, +2); tools/run_codegen_wasm_tests.sh green (unaffected, confirmed). Refs #225 #160
1 parent bf28bcd commit f61034b

1 file changed

Lines changed: 100 additions & 0 deletions

File tree

test/test_effects.ml

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,40 @@ let frontend (src : string) : (unit, string) result =
3232
| Ok _ -> Ok ()
3333
| Error e -> Error ("Type error: " ^ Typecheck.format_type_error e)
3434

35+
(** parse -> resolve -> typecheck -> BORROW-check an inline source.
36+
[Ok ()] = borrow-checker accepted; [Error msg] = first failing stage
37+
([Error] from [Borrow] is prefixed "Borrow error:"). Used by the
38+
#225 PR3b affine-capture regression: the borrow checker runs on the
39+
straight-line AST *before* the WasmGC CPS transform (verified
40+
pipeline order in bin/main.ml), so the exactly-once obligation on a
41+
local that the transform later captures into a continuation env is
42+
enforced here, by the existing checker, with no transform-aware
43+
machinery. *)
44+
let frontend_borrow (src : string) : (unit, string) result =
45+
let open Result in
46+
let ( let* ) = bind in
47+
let* prog =
48+
try Ok (Parse_driver.parse_string ~file:"<test_effects>" src)
49+
with
50+
| Parse_driver.Parse_error (m, sp) ->
51+
Error (Printf.sprintf "Parse error at %s: %s" (Span.show sp) m)
52+
| e -> Error (Printf.sprintf "Unexpected: %s" (Printexc.to_string e))
53+
in
54+
let loader = Module_loader.create (Module_loader.default_config ()) in
55+
let* resolve_ctx =
56+
match Resolve.resolve_program_with_loader prog loader with
57+
| Ok (rc, _) -> Ok rc
58+
| Error (e, _) -> Error ("Resolution error: " ^ Resolve.show_resolve_error e)
59+
in
60+
let* () =
61+
match Typecheck.check_program resolve_ctx.symbols prog with
62+
| Ok _ -> Ok ()
63+
| Error e -> Error ("Type error: " ^ Typecheck.format_type_error e)
64+
in
65+
match Borrow.check_program resolve_ctx.symbols prog with
66+
| Ok () -> Ok ()
67+
| Error e -> Error ("Borrow error: " ^ Borrow.format_borrow_error e)
68+
3569
let contains ~needle s =
3670
let nl = String.length needle and sl = String.length s in
3771
let rec go i = i + nl <= sl && (String.sub s i nl = needle || go (i + 1)) in
@@ -70,10 +104,76 @@ let handled_try_no_partial () =
70104
| Ok () -> ()
71105
| Error m -> Alcotest.failf "handled try should add no effect, got: %s" m
72106

107+
(* #225 PR3b — affine-capture obligation (ADR-013 obligation 1).
108+
109+
The WasmGC CPS transform captures a local that is live across the
110+
async split into the continuation environment. Soundness ("a linear/
111+
owned capture is consumed exactly once; double-resumption is
112+
impossible") is discharged BY COMPOSITION, not by transform-aware
113+
static analysis:
114+
115+
(a) the borrow checker runs on the straight-line AST *before* the
116+
codegen transform (pipeline order in bin/main.ml), so a double
117+
consume of an owned local across the async let is rejected
118+
there, exactly as in any synchronous body; and
119+
(b) the PR2 once-resumption trap guarantees the continuation runs
120+
at most once, so the transformed code performs each consume the
121+
same number of times the checker already verified.
122+
123+
These two tests pin (a): the capture-shaped body is borrow-checked
124+
with the existing machinery, double-use rejected, single-use OK. *)
125+
126+
let async_capture_shape body =
127+
Printf.sprintf
128+
"extern type Resource;\n\
129+
extern fn mkRes() -> Resource;\n\
130+
extern fn consume(own x: Resource) -> Int;\n\
131+
extern fn http_request_thenable(url: String, method: String, \
132+
body: String) -> Int /{Net, Async};\n\
133+
fn launch() -> Int /{Net, Async} {\n%s\n}" body
134+
135+
(* An owned local captured across the async split and consumed TWICE is
136+
a use-after-move — rejected by the existing borrow checker on the
137+
pre-transform AST (no transform-aware rule needed). *)
138+
let async_capture_double_use_rejected () =
139+
let src =
140+
async_capture_shape
141+
" let x = mkRes();\n\
142+
\ let r = http_request_thenable(\"u\", \"GET\", \"\");\n\
143+
\ let a = consume(x);\n\
144+
\ consume(x)"
145+
in
146+
match frontend_borrow src with
147+
| Ok () ->
148+
Alcotest.fail "expected a use-after-move borrow error, got Ok"
149+
| Error m ->
150+
Alcotest.(check bool) "is a borrow error" true
151+
(contains ~needle:"Borrow error:" m);
152+
Alcotest.(check bool) "names a moved value" true
153+
(contains ~needle:"moved" m)
154+
155+
(* The same capture shape with the owned local consumed exactly ONCE in
156+
the continuation is accepted — confirms the rule is not over-
157+
rejecting the legitimate captured-once case the transform relies on. *)
158+
let async_capture_single_use_ok () =
159+
let src =
160+
async_capture_shape
161+
" let x = mkRes();\n\
162+
\ let r = http_request_thenable(\"u\", \"GET\", \"\");\n\
163+
\ consume(x)"
164+
in
165+
match frontend_borrow src with
166+
| Ok () -> ()
167+
| Error m -> Alcotest.failf "expected Ok (captured-once), got: %s" m
168+
73169
let tests =
74170
[
75171
Alcotest.test_case "declared /{Partial} + ? accepted" `Quick
76172
declared_partial_ok;
173+
Alcotest.test_case "PR3b: owned capture double-use rejected" `Quick
174+
async_capture_double_use_rejected;
175+
Alcotest.test_case "PR3b: owned capture single-use accepted" `Quick
176+
async_capture_single_use_ok;
77177
Alcotest.test_case "declared /{IO} + ? rejected (#59)" `Quick
78178
declared_missing_partial_rejected;
79179
Alcotest.test_case "undeclared row stays permissive" `Quick

0 commit comments

Comments
 (0)