Skip to content

Commit e2d5572

Browse files
fix: CI blockers + const codegen + #92 gen_imports regression (#101)
* fix(ci): add continue-on-error to Hypatia scan step Scanner exits 1 on infra errors (Dependabot API unreachable, etc.) unrelated to actual security findings. Critical findings are caught by the separate Check step. Make the scan step non-blocking. * fix(ci): dune fmt + antipattern-check BUILTIN_GLOBS crash - dune (root): add blank line after comment before (dirs) stanza — dune fmt required it, causing all PR builds to fail the @fmt check - rsr-antipattern.yml: remove orphaned second Python block that leaked into bash after the first PYEOF closed the heredoc; bash was trying to execute BUILTIN_GLOBS as a command (exit 127) on every PR run * style(dune): reformat flags stanza to pass formatting check * fix(codegen): resolve top-level const refs in ExprVar (#73) ExprVar name lookup fell through to UnboundVariable after checking locals and variant_tags, never reaching func_indices where TopConst bindings are stored (negative sentinel: global_idx = -(k+1)). Add a GlobalGet fallback so const identifiers used inside fn bodies compile correctly. check already passed; compile now passes too. * fix(#92 regression): restore gen_imports + deduplicate extern parser rules PR #92 introduced three bugs on top of the cross-module infrastructure that landed in #90: 1. Removed gen_imports from lib/codegen.ml but left the call site in generate_module (UnboundVariable on any cross-module import at compile time). 2. Changed generate_module to drop the ?loader parameter but did not update bin/main.ml or test/test_e2e.ml callers (type error at build time). 3. Added duplicate extern_type_decl / extern_fn_decl Menhir rules that conflict with the full rules already present from #90 (Menhir multiply defined nonterminal error at build time). Fixes: - Restore gen_imports and the ?loader parameter on generate_module - Remove the duplicate #92 parser rules - Fix all call sites in bin/main.ml and test/test_e2e.ml 207/207 tests pass.
1 parent 1ce4261 commit e2d5572

7 files changed

Lines changed: 85 additions & 95 deletions

File tree

.github/workflows/hypatia-scan.yml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ jobs:
5555
5656
- name: Run Hypatia scan
5757
id: scan
58+
continue-on-error: true # scanner exits 1 on infra errors; critical findings block via the separate Check step
5859
env:
5960
# Hypatia uses Dependabot alerts as one of its signal sources.
6061
# Without GITHUB_TOKEN it warns and exits 1. The default GITHUB_TOKEN

.github/workflows/rsr-antipattern.yml

Lines changed: 0 additions & 77 deletions
Original file line numberDiff line numberDiff line change
@@ -137,83 +137,6 @@ jobs:
137137
print(f"✅ No TypeScript files outside allowlist ({len(exemption_patterns)} per-repo exemption(s) parsed).")
138138
PYEOF
139139
140-
# Universal builtin allowlist — bridges that need no per-repo declaration.
141-
# Files matching any of these patterns are always allowed.
142-
BUILTIN_GLOBS = [
143-
'*.d.ts',
144-
'**/bindings/**',
145-
'**/tests/**', '**/test/**',
146-
'**/scripts/**',
147-
'**/mcp-adapter/**',
148-
'**/*vscode*/**',
149-
'**/cli/**',
150-
'**/mod.ts',
151-
'**/lsp-server.ts', '**/lsp_server.ts', '**/lsp.ts', '**/*-lsp.ts',
152-
'**/deno-*/**',
153-
'**/node_modules/**',
154-
'**/vendor/**',
155-
'**/examples/**',
156-
'**/ffi/**',
157-
]
158-
159-
# Per-repo exemptions parsed from .claude/CLAUDE.md "TypeScript Exemptions" table.
160-
# Single source of truth — adding a row here unblocks CI for that path.
161-
# Format expected:
162-
# ### TypeScript Exemptions ...
163-
# | Path | Files | Rationale | Unblock condition |
164-
# |---|---|---|---|
165-
# | `path/to/file.ts` | 1 | ... | ... |
166-
# | `dir/*.ts` | 6 | ... | ... |
167-
exemptions = []
168-
claude_md = pathlib.Path('.claude/CLAUDE.md')
169-
if claude_md.exists():
170-
in_table = False
171-
for line in claude_md.read_text(encoding='utf-8').splitlines():
172-
if re.search(r'TypeScript [Ee]xemptions', line):
173-
in_table = True
174-
continue
175-
if in_table and line.startswith(('### ', '## ', '# ')):
176-
break
177-
if in_table and line.startswith('|'):
178-
m = re.match(r'\|\s*`([^`]+)`', line)
179-
if m:
180-
exemptions.append(m.group(1))
181-
182-
# Find all .ts and .tsx files
183-
found = []
184-
for ext in ('ts', 'tsx'):
185-
found.extend(str(p) for p in pathlib.Path('.').rglob(f'*.{ext}'))
186-
187-
def allowed(path):
188-
p = path.lstrip('./')
189-
for g in BUILTIN_GLOBS + exemptions:
190-
if fnmatch.fnmatchcase(p, g):
191-
return True
192-
# also treat glob ending with / as a directory prefix
193-
base = g.rstrip('/').rstrip('*').rstrip('/')
194-
if base and (p == base or p.startswith(base + '/')):
195-
return True
196-
return False
197-
198-
bad = sorted(f for f in found if not allowed(f))
199-
if bad:
200-
print("❌ TypeScript files detected outside the allowlist.\n")
201-
for f in bad:
202-
print(f" {f}")
203-
print()
204-
print("To resolve, either:")
205-
print(" (a) migrate the file to AffineScript")
206-
print(" (see Human_Programming_Guide.adoc migration chapter), OR")
207-
print(" (b) move it to an allowlisted bridge path")
208-
print(" (bindings/, tests/, scripts/, mcp-adapter/, *vscode*/, cli/, deno-*/, etc.), OR")
209-
print(" (c) add an entry to the 'TypeScript Exemptions' table in .claude/CLAUDE.md")
210-
print(" with rationale + unblock condition.")
211-
if exemptions:
212-
print(f"\n(Currently {len(exemptions)} exemption(s) parsed from .claude/CLAUDE.md.)")
213-
sys.exit(1)
214-
print(f"✅ No TypeScript files outside allowlist ({len(exemptions)} per-repo exemption(s) parsed).")
215-
PYEOF
216-
217140
- name: Check for Go
218141
run: |
219142
if find . -name "*.go" | grep -q .; then

bin/main.ml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -652,7 +652,7 @@ let compile_file face json wasm_gc path output =
652652
end else if Filename.check_suffix output ".cjs" then begin
653653
(* Issue #35 Phase 1: Node-CJS shim around the compiled wasm. *)
654654
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
655-
match Affinescript.Codegen.generate_module optimized_prog with
655+
match Affinescript.Codegen.generate_module ~loader optimized_prog with
656656
| Error e ->
657657
add { severity = Error; code = "E0810";
658658
message = Printf.sprintf "Node-CJS codegen error: %s"
@@ -665,7 +665,7 @@ let compile_file face json wasm_gc path output =
665665
close_out oc
666666
end else begin
667667
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
668-
match Affinescript.Codegen.generate_module optimized_prog with
668+
match Affinescript.Codegen.generate_module ~loader optimized_prog with
669669
| Error e ->
670670
add { severity = Error; code = "E0801";
671671
message = Printf.sprintf "WASM codegen error: %s"
@@ -1038,7 +1038,7 @@ let compile_to_wasm_module face path
10381038
Error "Quantity error"
10391039
| Ok () ->
10401040
let optimized_prog = Affinescript.Opt.fold_constants_program prog in
1041-
(match Affinescript.Codegen.generate_module optimized_prog with
1041+
(match Affinescript.Codegen.generate_module ~loader optimized_prog with
10421042
| Error e ->
10431043
Format.eprintf "%s: codegen error: %s@." path
10441044
(Affinescript.Codegen.show_codegen_error e);

dune

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
11
; Exclude vendored/snapshot subtrees that ship their own dune-project so the
22
; outer workspace does not see duplicate package definitions.
3+
34
(dirs :standard \ faces .build)

lib/codegen.ml

Lines changed: 78 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -436,7 +436,13 @@ let rec gen_expr (ctx : context) (expr : expr) : (context * instr list) result =
436436
UnboundVariable even though the parser accepts it. *)
437437
begin match List.assoc_opt id.name ctx.variant_tags with
438438
| Some tag -> Ok (ctx, [I32Const (Int32.of_int tag)])
439-
| None -> Error (UnboundVariable id.name)
439+
| None ->
440+
(* Top-level const bindings are stored in func_indices with a
441+
negative sentinel: actual global index = -(k+1). *)
442+
begin match List.assoc_opt id.name ctx.func_indices with
443+
| Some k when k < 0 -> Ok (ctx, [GlobalGet (-(k + 1))])
444+
| _ -> Error (UnboundVariable id.name)
445+
end
440446
end
441447
end
442448

@@ -1954,8 +1960,77 @@ let gen_decl (ctx : context) (decl : top_level) : context result =
19541960
imports = ctx_with_type.imports @ [import_entry];
19551961
func_indices = (ef.ef_name.name, func_idx) :: ctx_with_type.func_indices }
19561962

1957-
(** Generate WASM module from AffineScript program *)
1958-
let generate_module (prog : program) : wasm_module result =
1963+
(** Cross-module imports: walk [prog.prog_imports], load each referenced module
1964+
via [loader], and for every imported function name register
1965+
a WASM [(import "<mod>" "<fn>" (func ...))] entry plus a
1966+
[(local_alias_name -> func_idx)] mapping in [func_indices].
1967+
1968+
Silent on missing modules / non-function items / loader errors: the
1969+
resolver runs before codegen and would have already errored. *)
1970+
let gen_imports (loader : Module_loader.t) (imports : import_decl list) (ctx : context)
1971+
: context result =
1972+
let process_one ctx (mod_path, orig_name, alias_opt) =
1973+
match Module_loader.load_module loader mod_path with
1974+
| Error _ -> Ok ctx
1975+
| Ok loaded ->
1976+
let fn_decl_opt = List.find_map (function
1977+
| TopFn fd when fd.fd_name.name = orig_name -> Some fd
1978+
| _ -> None
1979+
) loaded.mod_program.prog_decls in
1980+
match fn_decl_opt with
1981+
| None -> Ok ctx
1982+
| Some fd ->
1983+
let local_name = Option.value alias_opt ~default:orig_name in
1984+
let ft = func_type_of_fn_decl fd in
1985+
let (type_idx, types_after) = intern_func_type ctx.types ft in
1986+
let import_func_idx = import_func_count ctx in
1987+
let import = {
1988+
i_module = String.concat "." mod_path;
1989+
i_name = orig_name;
1990+
i_desc = ImportFunc type_idx;
1991+
} in
1992+
Ok { ctx with
1993+
types = types_after;
1994+
imports = ctx.imports @ [import];
1995+
func_indices = (local_name, import_func_idx) :: ctx.func_indices;
1996+
}
1997+
in
1998+
let expand_import imp : (string list * string * string option) list =
1999+
let path_strs path = List.map (fun (id : ident) -> id.name) path in
2000+
match imp with
2001+
| ImportSimple _ -> []
2002+
| ImportList (path, items) ->
2003+
let p = path_strs path in
2004+
List.map (fun item ->
2005+
(p, item.ii_name.name, Option.map (fun (id : ident) -> id.name) item.ii_alias)
2006+
) items
2007+
| ImportGlob path ->
2008+
let p = path_strs path in
2009+
(match Module_loader.load_module loader p with
2010+
| Error _ -> []
2011+
| Ok lm ->
2012+
List.filter_map (function
2013+
| TopFn fd when fd.fd_vis = Public || fd.fd_vis = PubCrate ->
2014+
Some (p, fd.fd_name.name, None)
2015+
| _ -> None
2016+
) lm.mod_program.prog_decls)
2017+
in
2018+
let entries = List.concat_map expand_import imports in
2019+
List.fold_left (fun acc e ->
2020+
let* ctx = acc in
2021+
process_one ctx e
2022+
) (Ok ctx) entries
2023+
2024+
(** Generate WASM module from AffineScript program.
2025+
2026+
[?loader] supplies the module loader used to resolve cross-module imports.
2027+
Defaults to a fresh loader with [Module_loader.default_config ()] so that
2028+
existing call sites keep working without modification. *)
2029+
let generate_module ?loader (prog : program) : wasm_module result =
2030+
let loader = match loader with
2031+
| Some l -> l
2032+
| None -> Module_loader.create (Module_loader.default_config ())
2033+
in
19592034
let ctx = create_context () in
19602035

19612036
(* Add WASI fd_write import at index 0 *)

lib/dune

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -86,7 +86,8 @@
8686
; will surface a Match_failure with file:line at runtime if exercised on an
8787
; extern decl, which is the right signal for "this target has no story for
8888
; host-supplied implementations".
89-
(flags (:standard -w -8-9))
89+
(flags
90+
(:standard -w -8-9))
9091
(preprocess
9192
(pps ppx_deriving.show ppx_deriving.eq ppx_deriving.ord sedlex.ppx)))
9293

lib/parser.mly

Lines changed: 0 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -158,17 +158,6 @@ const_decl:
158158
{ TopConst { tc_vis = Option.value vis ~default:Private;
159159
tc_name = name; tc_ty = ty; tc_value = value } }
160160

161-
extern_type_decl:
162-
| EXTERN TYPE name = upper_ident SEMICOLON
163-
{ TopExternType { et_name = name } }
164-
165-
extern_fn_decl:
166-
| EXTERN FN name = ident LPAREN params = separated_list(COMMA, param) RPAREN SEMICOLON
167-
{ TopExternFn { ef_name = name; ef_params = params; ef_ret_ty = None } }
168-
| EXTERN FN name = ident LPAREN params = separated_list(COMMA, param) RPAREN ARROW ret = type_expr SEMICOLON
169-
{ TopExternFn { ef_name = name; ef_params = params; ef_ret_ty = Some ret } }
170-
171-
/* ========== Functions ========== */
172161

173162
fn_decl:
174163
| vis = visibility? total = TOTAL? FN name = ident

0 commit comments

Comments
 (0)