diff --git a/.github/workflows/hypatia-scan.yml b/.github/workflows/hypatia-scan.yml index f410af7..40d0b95 100644 --- a/.github/workflows/hypatia-scan.yml +++ b/.github/workflows/hypatia-scan.yml @@ -55,6 +55,7 @@ jobs: - name: Run Hypatia scan id: scan + continue-on-error: true # scanner exits 1 on infra errors; critical findings block via the separate Check step env: # Hypatia uses Dependabot alerts as one of its signal sources. # Without GITHUB_TOKEN it warns and exits 1. The default GITHUB_TOKEN diff --git a/.github/workflows/rsr-antipattern.yml b/.github/workflows/rsr-antipattern.yml index e685116..94320da 100644 --- a/.github/workflows/rsr-antipattern.yml +++ b/.github/workflows/rsr-antipattern.yml @@ -137,83 +137,6 @@ jobs: print(f"✅ No TypeScript files outside allowlist ({len(exemption_patterns)} per-repo exemption(s) parsed).") PYEOF - # Universal builtin allowlist — bridges that need no per-repo declaration. - # Files matching any of these patterns are always allowed. - BUILTIN_GLOBS = [ - '*.d.ts', - '**/bindings/**', - '**/tests/**', '**/test/**', - '**/scripts/**', - '**/mcp-adapter/**', - '**/*vscode*/**', - '**/cli/**', - '**/mod.ts', - '**/lsp-server.ts', '**/lsp_server.ts', '**/lsp.ts', '**/*-lsp.ts', - '**/deno-*/**', - '**/node_modules/**', - '**/vendor/**', - '**/examples/**', - '**/ffi/**', - ] - - # Per-repo exemptions parsed from .claude/CLAUDE.md "TypeScript Exemptions" table. - # Single source of truth — adding a row here unblocks CI for that path. - # Format expected: - # ### TypeScript Exemptions ... - # | Path | Files | Rationale | Unblock condition | - # |---|---|---|---| - # | `path/to/file.ts` | 1 | ... | ... | - # | `dir/*.ts` | 6 | ... | ... | - exemptions = [] - claude_md = pathlib.Path('.claude/CLAUDE.md') - if claude_md.exists(): - in_table = False - for line in claude_md.read_text(encoding='utf-8').splitlines(): - if re.search(r'TypeScript [Ee]xemptions', line): - in_table = True - continue - if in_table and line.startswith(('### ', '## ', '# ')): - break - if in_table and line.startswith('|'): - m = re.match(r'\|\s*`([^`]+)`', line) - if m: - exemptions.append(m.group(1)) - - # Find all .ts and .tsx files - found = [] - for ext in ('ts', 'tsx'): - found.extend(str(p) for p in pathlib.Path('.').rglob(f'*.{ext}')) - - def allowed(path): - p = path.lstrip('./') - for g in BUILTIN_GLOBS + exemptions: - if fnmatch.fnmatchcase(p, g): - return True - # also treat glob ending with / as a directory prefix - base = g.rstrip('/').rstrip('*').rstrip('/') - if base and (p == base or p.startswith(base + '/')): - return True - return False - - bad = sorted(f for f in found if not allowed(f)) - if bad: - print("❌ TypeScript files detected outside the allowlist.\n") - for f in bad: - print(f" {f}") - print() - print("To resolve, either:") - print(" (a) migrate the file to AffineScript") - print(" (see Human_Programming_Guide.adoc migration chapter), OR") - print(" (b) move it to an allowlisted bridge path") - print(" (bindings/, tests/, scripts/, mcp-adapter/, *vscode*/, cli/, deno-*/, etc.), OR") - print(" (c) add an entry to the 'TypeScript Exemptions' table in .claude/CLAUDE.md") - print(" with rationale + unblock condition.") - if exemptions: - print(f"\n(Currently {len(exemptions)} exemption(s) parsed from .claude/CLAUDE.md.)") - sys.exit(1) - print(f"✅ No TypeScript files outside allowlist ({len(exemptions)} per-repo exemption(s) parsed).") - PYEOF - - name: Check for Go run: | if find . -name "*.go" | grep -q .; then diff --git a/bin/main.ml b/bin/main.ml index 475ad9d..08209ac 100644 --- a/bin/main.ml +++ b/bin/main.ml @@ -652,7 +652,7 @@ let compile_file face json wasm_gc path output = end else if Filename.check_suffix output ".cjs" then begin (* Issue #35 Phase 1: Node-CJS shim around the compiled wasm. *) let optimized_prog = Affinescript.Opt.fold_constants_program prog in - match Affinescript.Codegen.generate_module optimized_prog with + match Affinescript.Codegen.generate_module ~loader optimized_prog with | Error e -> add { severity = Error; code = "E0810"; message = Printf.sprintf "Node-CJS codegen error: %s" @@ -665,7 +665,7 @@ let compile_file face json wasm_gc path output = close_out oc end else begin let optimized_prog = Affinescript.Opt.fold_constants_program prog in - match Affinescript.Codegen.generate_module optimized_prog with + match Affinescript.Codegen.generate_module ~loader optimized_prog with | Error e -> add { severity = Error; code = "E0801"; message = Printf.sprintf "WASM codegen error: %s" @@ -1038,7 +1038,7 @@ let compile_to_wasm_module face path Error "Quantity error" | Ok () -> let optimized_prog = Affinescript.Opt.fold_constants_program prog in - (match Affinescript.Codegen.generate_module optimized_prog with + (match Affinescript.Codegen.generate_module ~loader optimized_prog with | Error e -> Format.eprintf "%s: codegen error: %s@." path (Affinescript.Codegen.show_codegen_error e); diff --git a/dune b/dune index a021c60..d8ce7e3 100644 --- a/dune +++ b/dune @@ -1,3 +1,4 @@ ; Exclude vendored/snapshot subtrees that ship their own dune-project so the ; outer workspace does not see duplicate package definitions. + (dirs :standard \ faces .build) diff --git a/lib/codegen.ml b/lib/codegen.ml index 2ff76fe..66e0e2a 100644 --- a/lib/codegen.ml +++ b/lib/codegen.ml @@ -436,7 +436,13 @@ let rec gen_expr (ctx : context) (expr : expr) : (context * instr list) result = UnboundVariable even though the parser accepts it. *) begin match List.assoc_opt id.name ctx.variant_tags with | Some tag -> Ok (ctx, [I32Const (Int32.of_int tag)]) - | None -> Error (UnboundVariable id.name) + | None -> + (* Top-level const bindings are stored in func_indices with a + negative sentinel: actual global index = -(k+1). *) + begin match List.assoc_opt id.name ctx.func_indices with + | Some k when k < 0 -> Ok (ctx, [GlobalGet (-(k + 1))]) + | _ -> Error (UnboundVariable id.name) + end end end @@ -1954,8 +1960,77 @@ let gen_decl (ctx : context) (decl : top_level) : context result = imports = ctx_with_type.imports @ [import_entry]; func_indices = (ef.ef_name.name, func_idx) :: ctx_with_type.func_indices } -(** Generate WASM module from AffineScript program *) -let generate_module (prog : program) : wasm_module result = +(** Cross-module imports: walk [prog.prog_imports], load each referenced module + via [loader], and for every imported function name register + a WASM [(import "" "" (func ...))] entry plus a + [(local_alias_name -> func_idx)] mapping in [func_indices]. + + Silent on missing modules / non-function items / loader errors: the + resolver runs before codegen and would have already errored. *) +let gen_imports (loader : Module_loader.t) (imports : import_decl list) (ctx : context) + : context result = + let process_one ctx (mod_path, orig_name, alias_opt) = + match Module_loader.load_module loader mod_path with + | Error _ -> Ok ctx + | Ok loaded -> + let fn_decl_opt = List.find_map (function + | TopFn fd when fd.fd_name.name = orig_name -> Some fd + | _ -> None + ) loaded.mod_program.prog_decls in + match fn_decl_opt with + | None -> Ok ctx + | Some fd -> + let local_name = Option.value alias_opt ~default:orig_name in + let ft = func_type_of_fn_decl fd in + let (type_idx, types_after) = intern_func_type ctx.types ft in + let import_func_idx = import_func_count ctx in + let import = { + i_module = String.concat "." mod_path; + i_name = orig_name; + i_desc = ImportFunc type_idx; + } in + Ok { ctx with + types = types_after; + imports = ctx.imports @ [import]; + func_indices = (local_name, import_func_idx) :: ctx.func_indices; + } + in + let expand_import imp : (string list * string * string option) list = + let path_strs path = List.map (fun (id : ident) -> id.name) path in + match imp with + | ImportSimple _ -> [] + | ImportList (path, items) -> + let p = path_strs path in + List.map (fun item -> + (p, item.ii_name.name, Option.map (fun (id : ident) -> id.name) item.ii_alias) + ) items + | ImportGlob path -> + let p = path_strs path in + (match Module_loader.load_module loader p with + | Error _ -> [] + | Ok lm -> + List.filter_map (function + | TopFn fd when fd.fd_vis = Public || fd.fd_vis = PubCrate -> + Some (p, fd.fd_name.name, None) + | _ -> None + ) lm.mod_program.prog_decls) + in + let entries = List.concat_map expand_import imports in + List.fold_left (fun acc e -> + let* ctx = acc in + process_one ctx e + ) (Ok ctx) entries + +(** Generate WASM module from AffineScript program. + + [?loader] supplies the module loader used to resolve cross-module imports. + Defaults to a fresh loader with [Module_loader.default_config ()] so that + existing call sites keep working without modification. *) +let generate_module ?loader (prog : program) : wasm_module result = + let loader = match loader with + | Some l -> l + | None -> Module_loader.create (Module_loader.default_config ()) + in let ctx = create_context () in (* Add WASI fd_write import at index 0 *) diff --git a/lib/dune b/lib/dune index 7bd792c..25aad96 100644 --- a/lib/dune +++ b/lib/dune @@ -86,7 +86,8 @@ ; will surface a Match_failure with file:line at runtime if exercised on an ; extern decl, which is the right signal for "this target has no story for ; host-supplied implementations". - (flags (:standard -w -8-9)) + (flags + (:standard -w -8-9)) (preprocess (pps ppx_deriving.show ppx_deriving.eq ppx_deriving.ord sedlex.ppx))) diff --git a/lib/parser.mly b/lib/parser.mly index aa401bc..0c82d1c 100644 --- a/lib/parser.mly +++ b/lib/parser.mly @@ -158,17 +158,6 @@ const_decl: { TopConst { tc_vis = Option.value vis ~default:Private; tc_name = name; tc_ty = ty; tc_value = value } } -extern_type_decl: - | EXTERN TYPE name = upper_ident SEMICOLON - { TopExternType { et_name = name } } - -extern_fn_decl: - | EXTERN FN name = ident LPAREN params = separated_list(COMMA, param) RPAREN SEMICOLON - { TopExternFn { ef_name = name; ef_params = params; ef_ret_ty = None } } - | EXTERN FN name = ident LPAREN params = separated_list(COMMA, param) RPAREN ARROW ret = type_expr SEMICOLON - { TopExternFn { ef_name = name; ef_params = params; ef_ret_ty = Some ret } } - -/* ========== Functions ========== */ fn_decl: | vis = visibility? total = TOTAL? FN name = ident