Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/workflows/hypatia-scan.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
77 changes: 0 additions & 77 deletions .github/workflows/rsr-antipattern.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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);
Expand Down
1 change: 1 addition & 0 deletions dune
Original file line number Diff line number Diff line change
@@ -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)
81 changes: 78 additions & 3 deletions lib/codegen.ml
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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 "<mod>" "<fn>" (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 *)
Expand Down
3 changes: 2 additions & 1 deletion lib/dune
Original file line number Diff line number Diff line change
Expand Up @@ -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)))

Expand Down
11 changes: 0 additions & 11 deletions lib/parser.mly
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
Loading