Skip to content

Commit f6089a2

Browse files
hyperpolymathclaude
andcommitted
feat(cli): verify-boundary subcommand + verify-bridge exit-code fix
Closes the public CLI surface for typed-wasm Level 10 cross-module boundary verification. Prior to this commit, the Tw_interface analysis (per-path LinearImportCalledMultiple / LinearImportDroppedOnSomePath detection) was complete and unit-tested, but only wired to the verify-bridge subcommand which hardcodes the two internal IDApTIK bridges as the callee. Two changes: 1. New `verify-boundary CALLEE CALLER` subcommand accepting arbitrary AffineScript source files. Compiles both through the full frontend pipeline (via a new shared `compile_to_wasm_module` helper factored out of `verify_file`), extracts the callee's ownership-annotated export interface from its `affinescript.ownership` custom section, and runs `verify_cross_module` against the caller's imports + bodies. Exits 0 on clean, 1 on violations. Manpage documents both Linear- violation classes and a worked example. 2. Fix the exit-code bug in `verify_boundary_fn` (the `verify-bridge` handler). Previously it pretty-printed violations but always returned `Ok ()` to Cmdliner, so the `Exit 1 = violations found` contract in the docs was unenforceable. Now tracks a violation count across bridges and returns `Error` when non-zero — CI pipelines calling `verify-bridge` now fail correctly on boundary drift. No change to `Tw_interface.verify_cross_module` itself. The 177-test suite (including 8 pre-existing E2E Boundary Verify cases covering single-call, duplicate-call, and partial-path violations via direct API invocation) passes unchanged. Scope note: AffineScript's `use A.B` imports currently inline at the AST layer rather than emitting WASM import directives, so a caller compiled from AffineScript source today exposes no cross-module imports for the verifier to check. The new CLI is therefore immediately useful for hand-assembled or bridge-pattern caller modules; broader applicability follows when the compiler gains cross-module WASM import emission (separate feature, not a typed-wasm gap). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 35c476d commit f6089a2

1 file changed

Lines changed: 158 additions & 9 deletions

File tree

bin/main.ml

Lines changed: 158 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -668,6 +668,61 @@ let lint_file face json path =
668668

669669
(** {1 Stage 7: typed-wasm ownership verifier subcommand} *)
670670

671+
(** Compile a source file through the full frontend pipeline
672+
(parse → resolve → typecheck → borrow → quantity → codegen) and return
673+
the in-memory [Wasm.wasm_module] on success. On any stage failure,
674+
formats the diagnostic to stderr in the requested face's vocabulary
675+
and returns [Error stage_label].
676+
677+
Shared by the intra-module [verify] subcommand and the cross-module
678+
[verify-boundary] subcommand so the ownership custom section emitted
679+
during codegen is the same one consumed by both verifiers. *)
680+
let compile_to_wasm_module face path
681+
: (Affinescript.Wasm.wasm_module, string) Result.t =
682+
try
683+
let prog = parse_with_face face path in
684+
let loader_config = Affinescript.Module_loader.default_config () in
685+
let loader = Affinescript.Module_loader.create loader_config in
686+
match Affinescript.Resolve.resolve_program_with_loader prog loader with
687+
| Error (e, _span) ->
688+
Format.eprintf "%s: resolution error: %s@." path
689+
(Affinescript.Face.format_resolve_error face e);
690+
Error "Resolution error"
691+
| Ok (resolve_ctx, _type_ctx) ->
692+
match Affinescript.Typecheck.check_program resolve_ctx.symbols prog with
693+
| Error e ->
694+
Format.eprintf "%s: %s@." path
695+
(Affinescript.Face.format_type_error face e);
696+
Error "Type error"
697+
| Ok _type_ctx ->
698+
match Affinescript.Borrow.check_program resolve_ctx.symbols prog with
699+
| Error e ->
700+
Format.eprintf "%s: borrow error: %s@." path
701+
(Affinescript.Face.format_borrow_error face e);
702+
Error "Borrow error"
703+
| Ok () ->
704+
match Affinescript.Quantity.check_program_quantities prog with
705+
| Error (err, _span) ->
706+
Format.eprintf "%s: quantity error: %s@." path
707+
(Affinescript.Face.format_quantity_error face err);
708+
Error "Quantity error"
709+
| Ok () ->
710+
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
711+
(match Affinescript.Codegen.generate_module optimized_prog with
712+
| Error e ->
713+
Format.eprintf "%s: codegen error: %s@." path
714+
(Affinescript.Codegen.show_codegen_error e);
715+
Error "Codegen error"
716+
| Ok wasm_mod -> Ok wasm_mod)
717+
with
718+
| Affinescript.Lexer.Lexer_error (msg, pos) ->
719+
Format.eprintf "%s:%d:%d: lexer error: %s@." path pos.line pos.col msg;
720+
Error "Lexer error"
721+
| Affinescript.Parse_driver.Parse_error (msg, span) ->
722+
Format.eprintf "%a: parse error: %s@."
723+
Affinescript.Span.pp_short span msg;
724+
Error "Parse error"
725+
671726
(** Verify typed-wasm Level 7/10 ownership constraints on a compiled AffineScript
672727
source file.
673728
@@ -897,6 +952,12 @@ let make_linear_caller mod_name fn_name : Affinescript.Wasm.wasm_module =
897952
898953
Usage: affinescript verify-bridge [--which tea-bridge|router|all] *)
899954
let verify_boundary_fn which =
955+
(* Accumulates total violations so the exit code reflects them. Previously
956+
this function always returned `Ok (), so `verify-bridge` silently
957+
claimed success even when `verify_cross_module` found violations — the
958+
pretty-printed report was visible but the CI contract (exit 1) never
959+
triggered. *)
960+
let total_violations = ref 0 in
900961
let verify_one name callee_mod fn_name =
901962
let iface = Affinescript.Tw_interface.extract_exports callee_mod in
902963
let caller = make_linear_caller name fn_name in
@@ -905,6 +966,7 @@ let verify_boundary_fn which =
905966
| Ok () ->
906967
Affinescript.Tw_interface.pp_cross_report Format.std_formatter [];
907968
| Error errs ->
969+
total_violations := !total_violations + List.length errs;
908970
Affinescript.Tw_interface.pp_cross_report Format.std_formatter errs)
909971
in
910972
(match which with
@@ -923,7 +985,48 @@ let verify_boundary_fn which =
923985
verify_one "router"
924986
(Affinescript.Tea_router.generate ())
925987
"affinescript_router_push");
926-
`Ok ()
988+
if !total_violations = 0 then `Ok ()
989+
else `Error (false, Printf.sprintf "%d boundary violation(s)" !total_violations)
990+
991+
(** Verify typed-wasm Level 7/10 cross-module boundary constraints between
992+
two user-compiled AffineScript modules.
993+
994+
Compiles [callee_path] and [caller_path] through the full frontend
995+
pipeline, extracts [callee]'s ownership-annotated export interface from
996+
its [affinescript.ownership] custom section, then checks that every
997+
function in [caller] invokes each Linear-param import with consistent
998+
per-path call counts:
999+
- max_calls > 1 on any path → LinearImportCalledMultiple
1000+
- min_calls = 0 ∧ max_calls ≥ 1 → LinearImportDroppedOnSomePath
1001+
1002+
Unlike [verify-bridge] (which exercises the two internal IDApTIK
1003+
bridges against a synthetic one-call caller), this subcommand takes
1004+
user modules so any AffineScript consumer can validate its own
1005+
multi-module composition. The callee exposes [Linear] params via
1006+
[pub fn f(x: own T)]; the caller imports those exports and its body
1007+
becomes the subject of the path-sensitive call-count analysis.
1008+
1009+
Exit code 0 = clean. Exit code 1 = violations found. *)
1010+
let verify_boundary_files_fn face callee_path caller_path =
1011+
let callee_face = resolve_face face callee_path in
1012+
let caller_face = resolve_face face caller_path in
1013+
match compile_to_wasm_module callee_face callee_path with
1014+
| Error label -> `Error (false, "callee: " ^ label)
1015+
| Ok callee_mod ->
1016+
match compile_to_wasm_module caller_face caller_path with
1017+
| Error label -> `Error (false, "caller: " ^ label)
1018+
| Ok caller_mod ->
1019+
let iface = Affinescript.Tw_interface.extract_exports callee_mod in
1020+
Format.printf "=== boundary check: callee=%s caller=%s ===@."
1021+
callee_path caller_path;
1022+
match Affinescript.Tw_interface.verify_cross_module iface caller_mod with
1023+
| Ok () ->
1024+
Affinescript.Tw_interface.pp_cross_report Format.std_formatter [];
1025+
`Ok ()
1026+
| Error errs ->
1027+
Affinescript.Tw_interface.pp_cross_report Format.std_formatter errs;
1028+
`Error (false, Printf.sprintf "%d boundary violation(s)"
1029+
(List.length errs))
9271030

9281031
let which_arg =
9291032
let which = Arg.enum [
@@ -952,20 +1055,66 @@ let interface_cmd =
9521055
Cmd.v info Term.(ret (const interface_cmd_fn $ which_arg))
9531056

9541057
let verify_bridge_cmd =
955-
let doc = "Verify cross-module typed-wasm boundary constraints" in
1058+
let doc = "Verify cross-module typed-wasm boundary constraints (internal bridges)" in
9561059
let man = [
9571060
`S "DESCRIPTION";
958-
`P "Generates the selected bridge module, extracts its ownership-annotated \
959-
export interface, and verifies that a well-formed synthetic caller module \
960-
(one that imports a Linear-param export and calls it exactly once per \
961-
execution path) passes Level 7/10 cross-module boundary checking.";
962-
`P "This complements the intra-module $(b,verify) subcommand by checking the \
963-
boundary between the AffineScript-generated module and its callers.";
1061+
`P "Generates the selected internal bridge module (tea-bridge, router, or \
1062+
both), extracts its ownership-annotated export interface, and verifies \
1063+
that a well-formed synthetic caller module (one that imports a \
1064+
Linear-param export and calls it exactly once per execution path) \
1065+
passes Level 7/10 cross-module boundary checking.";
1066+
`P "Use $(b,verify-boundary) instead when the caller and callee are \
1067+
user-authored AffineScript modules rather than the internal IDApTIK \
1068+
bridges.";
9641069
`P "Exit 0 = boundary clean. Exit 1 = violations found.";
9651070
] in
9661071
let info = Cmd.info "verify-bridge" ~doc ~man in
9671072
Cmd.v info Term.(ret (const verify_boundary_fn $ which_arg))
9681073

1074+
(** Positional args for [verify-boundary]: callee first, caller second. *)
1075+
let verify_boundary_callee_arg =
1076+
Arg.(required & pos 0 (some file) None
1077+
& info [] ~docv:"CALLEE"
1078+
~doc:"AffineScript source for the exporter: its ownership-annotated \
1079+
$(b,pub fn) declarations define the contract the caller must \
1080+
honour.")
1081+
1082+
let verify_boundary_caller_arg =
1083+
Arg.(required & pos 1 (some file) None
1084+
& info [] ~docv:"CALLER"
1085+
~doc:"AffineScript source for the importer: every local function body \
1086+
is checked against the callee interface using per-path call-count \
1087+
analysis.")
1088+
1089+
let verify_boundary_cmd =
1090+
let doc = "Verify cross-module typed-wasm boundary constraints between two source files" in
1091+
let man = [
1092+
`S "DESCRIPTION";
1093+
`P "Compiles $(b,CALLEE) and $(b,CALLER) through the full frontend pipeline, \
1094+
extracts $(b,CALLEE)'s ownership-annotated export interface from its \
1095+
$(b,affinescript.ownership) custom section, and then checks every \
1096+
function body in $(b,CALLER) against that interface.";
1097+
`P "For each import that corresponds to a callee export with at least one \
1098+
$(b,own) (Linear) parameter, the verifier counts per-execution-path \
1099+
invocations and reports:";
1100+
`P "$(b,LinearImportCalledMultiple): the import is called more than once \
1101+
on some path. The Linear argument would be duplicated, violating \
1102+
exclusive ownership.";
1103+
`P "$(b,LinearImportDroppedOnSomePath): the import is called on some paths \
1104+
but not others (min=0, max≥1). The Linear argument is silently dropped \
1105+
on the zero-call paths.";
1106+
`P "Imports that never appear in a function body are not flagged — there \
1107+
is no obligation for every function to invoke every import.";
1108+
`P "Exit 0 = boundary clean. Exit 1 = violations found.";
1109+
`S "EXAMPLES";
1110+
`P " affinescript verify-boundary lib.affine app.affine";
1111+
] in
1112+
let info = Cmd.info "verify-boundary" ~doc ~man in
1113+
Cmd.v info Term.(ret (const verify_boundary_files_fn
1114+
$ face_arg
1115+
$ verify_boundary_callee_arg
1116+
$ verify_boundary_caller_arg))
1117+
9691118
let repl_cmd =
9701119
let doc = "Start the interactive REPL" in
9711120
let info = Cmd.info "repl" ~doc in
@@ -1177,7 +1326,7 @@ let default_cmd =
11771326
lex_cmd; parse_cmd; check_cmd; eval_cmd; repl_cmd; compile_cmd;
11781327
fmt_cmd; lint_cmd;
11791328
tea_bridge_cmd; router_bridge_cmd; cs_bridge_cmd; verify_cmd;
1180-
interface_cmd; verify_bridge_cmd;
1329+
interface_cmd; verify_bridge_cmd; verify_boundary_cmd;
11811330
hover_cmd; goto_def_cmd; complete_cmd; server_cmd;
11821331
preview_python_cmd; preview_js_cmd; preview_pseudocode_cmd
11831332
]

0 commit comments

Comments
 (0)