@@ -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] *)
899954let 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
9281031let 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
9541057let 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+
9691118let 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