Skip to content

Commit b58178a

Browse files
hyperpolymathclaude
andcommitted
feat(effects+codegen): implement interpreter effect dispatch + BUG-005 WasmGC silent-fallback fix
BUG-005 (codegen_gc.ml): - Add `UnboundFunction of string` error variant; replace the silent drop+null fallback in `ExprApp (ExprVar id, args)` when the function is absent from `func_indices` — was producing valid but semantically wrong WASM with no diagnostic. - Replace the silent drop+null fallback in `ExprApp (callee, args)` (indirect / higher-order calls) with `UnsupportedFeature`; call_ref is not yet wired. - Add `UnboundFunction` case to `format_codegen_error`. Effects runtime (interp.ml): - `eval_decl TopEffect`: register each effect op as a `PerformEffect`-raising `VBuiltin` so that calling an op actually propagates the effect. Previously the entire `TopEffect` branch was a no-op, making all effect ops unbound. - Fix multi-arg bug in `ExprHandle` dispatch: `arg_vals` was wrapping multiple args into a `VTuple` before binding against handler patterns, making any effect op with ≥ 2 params silently fail the `fold_left2` length check. - Bind the resume continuation under `"__resume__"` in the handler env so that the `resume expr` keyword form (`ExprResume`) can find it regardless of what name the programmer chose for the continuation parameter. - `ExprResume`: look up `"__resume__"` in env and call it; fall back to returning the arg value when used outside a handler (safe no-op semantics). Effects runtime (codegen_gc.ml — WasmGC): - `TopEffect`: register each op as an `unreachable` stub function with correct func_indices entry so subsequent function indices are not shifted and direct calls trap rather than producing a link error. - `ExprHandle`: replace silent body-only compilation (which dropped all handler arms) with an explicit `UnsupportedFeature` error; full dispatch requires the WASM EH proposal or a CPS transform, neither of which is present yet. - `ExprResume`: same — reject explicitly rather than emitting the bare arg value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7987f79 commit b58178a

2 files changed

Lines changed: 139 additions & 42 deletions

File tree

lib/codegen_gc.ml

Lines changed: 80 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,10 @@ type codegen_error =
4545
| UnsupportedFeature of string
4646
| UnboundVariable of string
4747
| UnboundType of string
48+
(** Raised when [ExprApp] names a function that has no entry in [func_indices].
49+
This is always a compiler bug — every defined function must be registered
50+
before codegen reaches a call-site. *)
51+
| UnboundFunction of string
4852
[@@deriving show]
4953

5054
type 'a cg_result = ('a, codegen_error) Result.t
@@ -355,24 +359,21 @@ let rec gen_gc_expr (ctx : gc_ctx) (expr : expr) : (gc_ctx * gc_instr list) cg_r
355359
| Some func_idx ->
356360
Ok (ctx_after_args, arg_codes @ [Std (Wasm.Call func_idx)])
357361
| None ->
358-
(* Function index not found: emit drop for all args + null as placeholder *)
359-
let drop_code = List.init (List.length args) (fun _ -> std Wasm.Drop) in
360-
Ok (ctx_after_args, arg_codes @ drop_code @ [RefNull HtAny])
362+
(* BUG-005: every reachable function must be registered in func_indices
363+
before codegen visits any call-site. Silently emitting drop+null here
364+
would produce well-typed but semantically wrong WASM that is impossible
365+
to debug at runtime. Fail loudly instead. *)
366+
Error (UnboundFunction id.name)
361367
end
362368

363369
| ExprApp (callee, args) ->
364-
(* Indirect calls or non-variable callees: evaluate and discard for now *)
365-
let* (ctx1, callee_code) = gen_gc_expr ctx callee in
366-
let* (ctx2, arg_codes_rev) =
367-
List.fold_left (fun acc arg ->
368-
let* (c, rev_codes) = acc in
369-
let* (c', code) = gen_gc_expr c arg in
370-
Ok (c', code :: rev_codes)
371-
) (Ok (ctx1, [])) args
372-
in
373-
let arg_codes = List.concat (List.rev arg_codes_rev) in
374-
let drop_code = List.init (List.length args + 1) (fun _ -> std Wasm.Drop) in
375-
Ok (ctx2, callee_code @ arg_codes @ drop_code @ [RefNull HtAny])
370+
(* BUG-005: indirect / higher-order calls (callee is not a plain ExprVar) are
371+
not yet lowered to call_ref in the WasmGC backend. The old behaviour —
372+
evaluate callee+args, drop everything, push null — was silently wrong and
373+
would crash at the call-site with an opaque type error. Reject explicitly
374+
until call_ref support is added. *)
375+
let _ = (callee, args) in
376+
Error (UnsupportedFeature "indirect / higher-order call in WasmGC backend (call_ref not yet implemented)")
376377

377378
(* ── Record allocation (core GC operation) ─────────────────────── *)
378379

@@ -739,18 +740,35 @@ let rec gen_gc_expr (ctx : gc_ctx) (expr : expr) : (gc_ctx * gc_instr list) cg_r
739740

740741
(* ── Effect / error-handling passthrough ────────────────────────── *)
741742

742-
| ExprHandle eh ->
743-
(* Effect handlers: compile body only; continuation support deferred *)
744-
gen_gc_expr ctx eh.eh_body
743+
| ExprHandle _eh ->
744+
(* Effect handler dispatch is not implementable in this WasmGC backend
745+
without either:
746+
- The WASM exception-handling proposal (EH) to propagate PerformEffect
747+
across stack frames and capture the perform-site continuation, OR
748+
- A whole-program CPS transform before codegen.
749+
750+
Silently compiling just the body (the previous behaviour) was wrong:
751+
any handler arms are dropped, so effects are never caught, and the
752+
first `perform` would trap at the op stub rather than dispatch to the
753+
correct handler arm. Fail loudly instead.
754+
755+
To use algebraic effects, compile with the interpreter backend (-i). *)
756+
Error (UnsupportedFeature
757+
"effect handler (handle { ... }) in WasmGC backend — \
758+
requires WASM EH proposal or CPS transform; use `--interp` / `-i`")
745759

746760
| ExprTry et ->
747761
gen_gc_block ctx et.et_body
748762

749-
| ExprResume arg_opt ->
750-
begin match arg_opt with
751-
| Some e -> gen_gc_expr ctx e
752-
| None -> Ok (ctx, [push_i32 0])
753-
end
763+
| ExprResume _arg_opt ->
764+
(* `resume` is only meaningful inside an effect handler arm. The WasmGC
765+
backend has no handler dispatch (see ExprHandle above), so emitting
766+
the argument value here would be silently wrong — the enclosing handle
767+
expression already fails with UnsupportedFeature before we ever reach
768+
a resume. Fail consistently. *)
769+
Error (UnsupportedFeature
770+
"`resume` expression in WasmGC backend — \
771+
only valid inside a `handle` block; use `--interp` / `-i`")
754772

755773
| ExprRowRestrict (base, _) ->
756774
(* Row restriction is type-level; GC pointer is unchanged *)
@@ -1104,9 +1122,46 @@ let gen_gc_decl (ctx : gc_ctx) (decl : top_level) : gc_ctx cg_result =
11041122
Ok ctx
11051123
end
11061124

1107-
| TopConst _ | TopEffect _ | TopTrait _ | TopImpl _ ->
1125+
| TopConst _ | TopTrait _ | TopImpl _ ->
11081126
Ok ctx
11091127

1128+
| TopEffect ed ->
1129+
(* Register each effect operation as an unreachable stub function.
1130+
This gives each op a valid func_indices entry (so direct calls at
1131+
least produce a trap rather than a link error), and correctly
1132+
offsets function indices for all subsequent definitions.
1133+
1134+
Full effect handler dispatch requires either:
1135+
- The WASM exception-handling proposal (EH, standardised 2023) to
1136+
propagate perform-site continuations across stack frames, OR
1137+
- A whole-program CPS transform before codegen.
1138+
Neither is implemented in this backend yet. Use the interpreter
1139+
(`-i`) for programs that perform effects.
1140+
1141+
All parameters are typed as GcAnyref (conservative): concrete types
1142+
are not yet propagated from the type-inference pass into codegen. *)
1143+
let ctx' = List.fold_left (fun ctx (op : effect_op_decl) ->
1144+
let n_params = List.length op.eod_params in
1145+
let func_type = GcFuncType {
1146+
gft_params = List.init n_params (fun _ -> GcAnyref);
1147+
gft_results = [GcAnyref];
1148+
} in
1149+
let (ctx1, type_idx) = register_gc_type ctx func_type in
1150+
let func_idx = ctx1.import_count + List.length ctx1.gc_funcs_acc in
1151+
let stub : gc_func = {
1152+
gf_type = type_idx;
1153+
gf_locals = [];
1154+
(* Trap immediately: calling an effect op in WasmGC without a
1155+
handler dispatch mechanism is always a programming error. *)
1156+
gf_body = [Std Wasm.Unreachable];
1157+
} in
1158+
{ ctx1 with
1159+
func_indices = (op.eod_name.name, func_idx) :: ctx1.func_indices;
1160+
gc_funcs_acc = ctx1.gc_funcs_acc @ [stub];
1161+
}
1162+
) ctx ed.ed_ops in
1163+
Ok ctx'
1164+
11101165
(** {1 Module-level entry point} *)
11111166

11121167
(** Generate a {!Wasm_gc.gc_module} from an AffineScript program.
@@ -1144,3 +1199,4 @@ let format_codegen_error (e : codegen_error) : string =
11441199
| UnsupportedFeature msg -> Printf.sprintf "GC codegen: unsupported feature: %s" msg
11451200
| UnboundVariable name -> Printf.sprintf "GC codegen: unbound variable: %s" name
11461201
| UnboundType name -> Printf.sprintf "GC codegen: unbound type: %s" name
1202+
| UnboundFunction name -> Printf.sprintf "GC codegen: function '%s' has no func_indices entry (compiler bug — register before codegen)" name

lib/interp.ml

Lines changed: 59 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -251,19 +251,32 @@ let rec eval (env : env) (expr : expr) : value result =
251251
) eh.eh_handlers in
252252
begin match op_arm with
253253
| Some (HandlerOp (_, pats, body)) ->
254-
(* Bind effect arguments to handler parameters.
255-
The last parameter is conventionally the resume continuation,
256-
but for now we bind a simple identity closure. *)
257-
let arg_vals = match args with
258-
| [single] -> [single]
259-
| multiple -> [VTuple multiple]
260-
in
261-
let resume_fn = VBuiltin ("resume", fun resume_args ->
254+
(* Build the resume continuation. In this tree-walking interpreter
255+
the continuation is shallow: calling resume(v) returns v as the
256+
result of the entire `handle` expression. This is correct for
257+
the common single-shot, tail-resume pattern. Full multi-shot
258+
continuations require either OCaml 5 effects or a CPS transform. *)
259+
let resume_fn = VBuiltin ("__resume__", fun resume_args ->
262260
match resume_args with
263261
| [v] -> Ok v
264-
| _ -> Ok VUnit
262+
| [] -> Ok VUnit
263+
| vs -> Ok (VTuple vs)
265264
) in
266-
let all_vals = arg_vals @ [resume_fn] in
265+
(* Bind effect argument values to handler patterns.
266+
Convention: all declared params first, then the continuation as
267+
the last pattern. Pass args flat (not wrapped in a tuple) so
268+
that multi-arg effects bind correctly to separate patterns. *)
269+
let all_vals = args @ [resume_fn] in
270+
let n_pats = List.length pats in
271+
let n_vals = List.length all_vals in
272+
(* If the handler omits the continuation param, provide it anyway
273+
so that ExprResume still works via the __resume__ env slot. *)
274+
let trimmed_vals = List.filteri (fun i _ -> i < n_pats) all_vals in
275+
let pad_vals =
276+
if n_vals < n_pats then
277+
trimmed_vals @ List.init (n_pats - n_vals) (fun _ -> VUnit)
278+
else trimmed_vals
279+
in
267280
let bindings = List.fold_left2 (fun acc pat v ->
268281
match acc with
269282
| Ok bs ->
@@ -272,21 +285,35 @@ let rec eval (env : env) (expr : expr) : value result =
272285
| Error e -> Error e
273286
end
274287
| Error e -> Error e
275-
) (Ok []) pats (List.filteri (fun i _ -> i < List.length pats) all_vals) in
288+
) (Ok []) pats pad_vals in
276289
let* bindings = bindings in
277-
let env' = extend_env_list bindings env in
290+
(* Also bind the resume fn under "__resume__" so that the
291+
`ExprResume` keyword form can find it regardless of what
292+
name the programmer chose for the continuation parameter. *)
293+
let env' =
294+
extend_env "__resume__" resume_fn
295+
(extend_env_list bindings env)
296+
in
278297
eval env' body
279298
| _ -> Error (RuntimeError ("Unhandled effect: " ^ op_name))
280299
end
281300
| Error e -> Error e
282301
end
283302

284303
| ExprResume arg_opt ->
285-
(* Resume is only meaningful inside an effect handler. At the top
286-
level it's a no-op that returns the argument or unit. *)
287-
begin match arg_opt with
304+
(* Evaluate the argument, then call the resume continuation bound in the
305+
environment by the enclosing ExprHandle dispatcher. The continuation
306+
is stored under "__resume__" so that the `resume expr` keyword form
307+
works without the programmer having to name the continuation parameter.
308+
If called outside a handler (no "__resume__" in env), return the value
309+
as-is — this matches the surface-syntax intuition "resume x ≈ x". *)
310+
let* arg_val = match arg_opt with
288311
| Some e -> eval env e
289-
| None -> Ok VUnit
312+
| None -> Ok VUnit
313+
in
314+
begin match lookup_env "__resume__" env with
315+
| Ok resume_fn -> apply_function resume_fn [arg_val]
316+
| Error _ -> Ok arg_val
290317
end
291318

292319
| ExprTry et ->
@@ -865,10 +892,24 @@ let eval_decl (env : env) (decl : top_level) : env result =
865892
let* v = eval env tc.tc_value in
866893
Ok (extend_env tc.tc_name.name v env)
867894

868-
| TopType _ | TopEffect _ | TopTrait _ | TopImpl _ ->
869-
(* Type declarations don't affect runtime *)
895+
| TopType _ | TopTrait _ | TopImpl _ ->
896+
(* Type/trait/impl declarations don't affect the runtime value environment *)
870897
Ok env
871898

899+
| TopEffect ed ->
900+
(* Register each effect operation as a PerformEffect-raising builtin.
901+
When an effect op is called from within a `handle` expression, the
902+
Error(PerformEffect ...) propagates up the call stack until caught by
903+
the ExprHandle dispatcher. Unhandled effects surface as RuntimeError. *)
904+
let env' = List.fold_left (fun env (op : effect_op_decl) ->
905+
let op_name = op.eod_name.name in
906+
let builtin = VBuiltin (op_name, fun args ->
907+
Error (PerformEffect (op_name, args))
908+
) in
909+
extend_env op_name builtin env
910+
) env ed.ed_ops in
911+
Ok env'
912+
872913
(** Evaluate a program *)
873914
let eval_program (prog : program) : env result =
874915
let initial_env = create_initial_env () in

0 commit comments

Comments
 (0)