Skip to content
Closed
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
91 changes: 91 additions & 0 deletions .machine_readable/6a2/META.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -872,3 +872,94 @@ references = [
"stdlib/Vscode.affine + packages/affine-vscode/mod.js (#205 thenableThen)",
"typed-wasm ADR-004 (convergence / aggregate library; Ephapax)",
]

[[adr]]
id = "ADR-014"
status = "accepted"
date = "2026-05-18"
title = "Module-qualified type & effect paths: dot separator, sound module-scoped member lookup"
context = """
The type/effect grammar had NO module-qualified path production.
`Externs.Res` / `Pkg.Type` in type or effect position failed
`parse error` at the `.`. `type_expr_primary` only had
`upper_ident -> TyCon` / `upper_ident [..]/<..> -> TyApp`;
`effect_term` only `ident -> EffVar` / `ident [..] -> EffCon`.
`module_path` already parsed `ident (DOT ident)*` for module/use.
ADR-011 settled real modules with qualified paths; the estate's
Frontier-playbook ports pervasively write `Externs.Foo` in type/effect
position, contradicting a grammar that could not represent it. An
oracle audit (28 repos / 1176 .affine files) measured 498 DRIFT-SYNTAX
files, dominated by exactly this unrepresentable qualified-path shape.
"""
decision = """
A module-qualified path `A.B.C` is admissible in TYPE and EFFECT
position. The dot `.` is the canonical separator for these paths;
`::` remains value/import (ADR-011 unchanged). Module-prefix segments
may be lower- OR upper-case (stdlib modules are `prelude`/`option`/
`string`/`result` as well as `Network`/`Http`); the final segment is
the type/effect member and is `upper_ident`.

`A.B.C` resolves by SOUND MODULE-SCOPED MEMBER LOOKUP, never flat
current scope:
- load module `[A;B]` through the module loader;
- resolve + type-check it exactly as an import would
(`Resolve.resolve_and_typecheck_module`);
- look `C` up INSIDE that module's own resolved symbol table,
requiring `Public`/`PubCrate` and the right kind (`SKType` for
types, `SKEffect` for effects).
An unknown/wrong module, a missing member, a private member, or a
member of the wrong kind is a *resolution* error at the use-site span
— not a parse error, never silently accepted. A validated qualified
effect's name is admitted as a declared effect (issue #59) so a
sibling module's public effect is usable like a local one.

Design (ripple-minimised):
- `ast.ml`: `ident` gains `modpath : string list` ([] = unqualified).
- `parser.mly`: `mk_qualified_ident`; a `qualified_type_path` helper
(`module_prefix DOT upper_ident`); bare / [args] / <args> applied
forms for types, bare / [args] for effects.
- `typecheck.ml`: `lower_type_expr`/`lower_effect_expr` handle the
qualified case via a loader-backed validator threaded into the
context (`qualified_member_check`), raising `Qualified_path_error`
-> `QualifiedPathError` at the `check_program` boundary (same
pattern as `Effect_validation_error`). Resolved representation is
the canonical nominal type/effect — identical to the bare/imported
form; the validation, not the representation, is the soundness gate.
- `resolve.ml`: `make_qualified_member_check` built where the loader
lives; in the `resolve_and_typecheck_module` recursive group so
transitively-qualified stdlib paths resolve too.

The `upper_ident` vs `upper_ident DOT …` choice is the expected
benign DOT shift (Menhir shifts, ADR-012). Verified: parser-conflict
summary UNCHANGED — 21 shift/reduce states, 1 reduce/reduce state,
68 s/r + 7 r/r arbitrarily resolved, identical to the pre-change
parser. No new unexplained conflict.
"""
consequences = """
- Estate Frontier-playbook ports that write `Externs.Foo` /
`prelude.Option` in type/effect position now resolve soundly; the
oracle re-audit shows a large DRIFT-SYNTAX -> PASS/TYPE-ONLY shift.
- Typecheck/codegen unchanged: a qualified ident is treated by its
resolved symbol exactly like an unqualified one.
- Adding `modpath` to `ident` reflows every golden `.expected` AST
dump (regenerated; the span-normalised gate confirms no structural
change). 5 conformance cases added under
tests/conformance/qualified-paths/{valid,invalid}; the full gate is
green (258 -> 263, zero regressions).
- STAGE-C peer of #225 (typed-wasm Http) and #160 (C-spine). Language
decision is human-gated (ISSUE-CLOSURE): Refs #228, NOT Closes.
- Settled; do not reopen without amending this ADR.
"""
references = [
"https://github.com/hyperpolymath/affinescript/issues/228",
"https://github.com/hyperpolymath/affinescript/issues/225",
"https://github.com/hyperpolymath/affinescript/issues/160",
"docs/specs/SETTLED-DECISIONS.adoc (ADR-014 section)",
"lib/ast.ml (ident.modpath)",
"lib/parser.mly (qualified_type_path; mk_qualified_ident)",
"lib/typecheck.ml (lower_type_expr/lower_effect_expr; qualified_member_check)",
"lib/resolve.ml (make_qualified_member_check; module-scoped lookup)",
"tests/conformance/qualified-paths/{valid,invalid}",
"ADR-011 (real modules with qualified paths)",
"ADR-012 (parser-conflict disclosure; benign DOT shift)",
]
3 changes: 2 additions & 1 deletion .machine_readable/6a2/STATE.a2ml
Original file line number Diff line number Diff line change
Expand Up @@ -4,8 +4,9 @@
[metadata]
project = "affinescript"
version = "0.1.0"
last-updated = "2026-05-03"
last-updated = "2026-05-18"
status = "active"
session-note-2026-05-18 = "MODULE-QUALIFIED TYPE & EFFECT PATHS (issue #228, ADR-014). The type/effect grammar had NO module-qualified path production: Externs.Res / prelude.Option / any Pkg.Type in type or effect position failed parse-error at the dot, contradicting already-settled ADR-011. Implemented FULL SOUND qualified paths, dot canonical separator (:: stays value/import per ADR-011). (1) lib/ast.ml — ident gains modpath:string list ([] = unqualified); single mk_ident helper. (2) lib/parser.mly — mk_qualified_ident + path_seg/module_prefix/qualified_type_path; qualified type (bare/[]/<>) and effect (bare/[]) productions; the upper_ident-vs-DOT choice is the expected benign DOT shift, masked-conflict count UNCHANGED (21 s/r, 1 r/r — identical to pre-change parser, ADR-012). (3) lib/resolve.ml — make_qualified_member_check: loads module [A;B] via the loader, resolve+typecheck it as an import would, then looks the member up INSIDE that module's own resolved symbols requiring Public/PubCrate + right kind (SKType/SKEffect); unknown/wrong module, missing/private/wrong-kind member = resolution error at use-site span, never silent. Soundness = lookup within the named module, not flat scope. (4) lib/typecheck.ml — qualified-path validation injected via closure (resolve<->typecheck would be circular); validated qualified effect admitted as a declared effect (issue #59). (5) bin/main.ml threads the validator into 6 check_program calls. (6) Internal ident literals in trait/codegen_gc/borrow/verilog_codegen routed through Ast.mk_ident. (7) ADR-014 written to docs/specs/SETTLED-DECISIONS.adoc + .machine_readable/6a2/META.a2ml. (8) 5 conformance fixtures under tests/conformance/qualified-paths/{valid,invalid}; 12 golden .expected regenerated (pure structural no-op = the new modpath field only, verified). Gates: dune build clean; 258 -> 263 tests, 0 regressions; zero menhir conflict delta. Draft PR #231 (Refs #228, NOT Closes — language decision human-gated per ISSUE-CLOSURE; no auto-merge); STAGE-C peer of #225 (typed-wasm Http) and #160 (C-spine). KNOWN LIMITATION: tests/conformance/qualified-paths/valid/qualified_effect.affine references sibling module effmod, so it resolves ONLY with the test-harness module search-path; it FAILS a bare 'affinescript check' (no public stdlib effect exists to build a self-contained positive like prelude.Option does for types). Bare-oracle audits (incl. the estate dialect-conformance .affine-audit harness) MUST treat that one fixture as harness-only. Qualified-effect resolution itself is sound (proven indirectly: invalid/private_member loads stdlib module 'effects' and correctly rejects the private member). Estate-payoff numbers are understated by the bare-loader audit harness (repo-local modules report UndefinedModule); a re-audit with repo module graphs resolvable is pending and is the load-bearing review evidence. Part of the estate AffineScript dialect-conformance campaign (also issues #229 RS-surface elimination, #230 de-vendor)."
session-note-2026-05-03-c = "EXTERN/VSCODE/ARRAY/PATCON BATCH. (1) `extern fn name(...) -> Ret;` and `extern type Name;` now parse — added EXTERN keyword to lexer/token/parse_driver, FnExtern to fn_body and TyExtern to type_body in AST, extern_fn_decl + extern_type_decl rules in parser.mly. Resolve registers the symbol; Typecheck.check_fn_decl special-cases FnExtern to register the polymorphic scheme without body checking; Codegen.gen_decl emits a real `(import \"env\" \"<name>\" (func ...))` for each extern fn, mirroring gen_imports's cross-module shape. Borrow + Quantity skip extern fns. lib/dune now demotes warning 8/9 from error to warning so the new variants don't require lock-step updates across all 27 codegens (any non-Wasm codegen that doesn't handle FnExtern raises Match_failure with file:line at runtime — correct signal for 'this target has no story for host-supplied implementations'). 192 tests; 0 regressions. (2) Issue #35 Phase 2: stdlib/Vscode.affine and stdlib/VscodeLanguageClient.affine ship the ~12 + 3 binding declarations from the issue's API inventory (registerCommand, getConfiguration, showInformationMessage, createTerminal, ...). packages/affine-vscode/mod.js is the JS-side adapter that translates each `extern fn` invocation into the corresponding vscode/lc API call, with a JS-side handle table for opaque host objects and a string-marshal helper that reads `[u32 length][utf-8 bytes]` out of the wasm memory. Adapter returns a namespaced object `{ Vscode: {...}, VscodeLanguageClient: {...} }` matching the WASM cross-module imports' module names. examples/vscode_extension_minimal.affine demonstrates an end-to-end VS Code extension authored in AffineScript using `use Vscode::{registerCommand, showInformationMessage, pushSubscription}` — compiles to .cjs via the Node-CJS path. (3) issues-drafts/02 closed: `[T]` array type now parses via `LBRACKET type_expr RBRACKET → TyApp (Array, [TyArg elem])` in lib/parser.mly's type_expr_primary rule. Verified for fn params, return types, struct fields, and nested `[[T]]`. Other stdlib files (Option/math/io/string) still fail with distinct issues (`fn() -> T` type syntax, `Option<T>` angle brackets) — the array fix advanced math.affine's failure point from line 349 to 354 etc. (4) PatCon sub-pattern destructuring under WasmGC: `match Mk(7, 99) { Mk(a, b) => a }` now lowers to RefCast + StructGet for the tag check + per-field RefCast HtI31 + I31GetS unboxing for each PatVar sub-pattern. Validated end-to-end: emitted .wasm instantiates in Deno and `main()` returns 7. The mixed-arity case (zero-arg + with-args in same enum, e.g. Option) errors loudly with the workaround documented (split into two matches, or use Wasm 1.0). Unifying variant rep — uniform `struct {tag, payload}` so Some+None can share — is the next destructuring milestone. 195 → 198 → 200 tests; 0 regressions throughout."
session-note-2026-05-03-b = "FOLLOW-ON BATCH AFTER TYPED-WASM CLOSURE: variant-with-args under WasmGC, Node-target codegen Phase 1 (issue #35), stdlib Core.affine fix, cross-module for other codegens. (1) lib/codegen_gc.ml — added gen_variant_with_args helper that lowers ExprApp(ExprVariant(_), args) and bare-name `Some(42)` (via variant_tags lookup in ExprApp ExprVar) to a tagged anon-struct allocation [tag: i32, payload: anyref, ...] with i32 args boxed via ref.i31. ExprVar gained variant_tags fallback so `return Happy` works. gen_gc_function now post-processes body_code: when result_vt ≠ I32 the trailing `push_i32 0` fallback (emitted by gen_gc_block when blk_expr=None) is swapped for RefNull HtAny, which fixes 'end[0] expected type anyref, found i32' validator errors on functions that explicitly return a struct. Validated: emitted .wasm instantiates in Node 18 / V8 14, main() returns the GC struct. f64 args remain UnsupportedFeature (no i31 boxing path). (2) lib/codegen_node.ml — new module implementing issue #35 Phase 1 (Node-CJS emit). Wraps Codegen.generate_module output in a CJS shim with: inline base64-encoded wasm constant, lazy WebAssembly.instantiate on first activate/deactivate call, JS-side opaque-handle table (_registerHandle / _getHandle / _freeHandle exported for Phase 2 vscode bindings), minimal WASI fd_write so println-style codegen works, exports.activate/exports.deactivate re-exports of the wasm module's same-named exports. .cjs output extension dispatches in bin/main.ml (both JSON and non-JSON paths). Smoke-tested: real Node 18 require()s a generated .cjs and successfully calls activate(fakeContext) → 0, deactivate() → 0. Issue #35 Phases 2-4 (stdlib/Vscode.affine bindings, extension.ts → extension.affine migration, rattlescript-face sweep) remain as separate work. (3) stdlib/Core.affine — three parser-collision fixes: `pub fn const[A,B]` → `always` (const is a reserved keyword for compile-time bindings); `fn(x: A) -> C { ... }` lambdas → `|x: A|` form (the parser's actual lambda syntax); `flip` rewritten from `(A, B) -> C` to curried `A -> B -> C` to dodge tuple-arrow ambiguity. The originally-blocked tests/modules/test_simple_import.affine (use Core::{min}; min(10,20)) now compiles end-to-end. Other stdlib files (Option/math/io/string/...) still don't parse — each has distinct issues (fn() type syntax, `[T]` array type per issues-drafts/02, `Option<T>` angle brackets) that need their own passes or compiler-level fixes. (4) lib/module_loader.ml — new flatten_imports : t -> program -> program that prepends imported public TopFns into the importer's prog_decls (deduplicating against local fn names). bin/main.ml now binds [let flat_prog = Module_loader.flatten_imports loader prog in] in both compile_file paths and threads flat_prog through all 22 non-Wasm codegens (Julia, JS, C, WGSL, Faust, ONNX, OCaml, Lua, Bash, Nickel, ReScript, Rust, LLVM, Verilog, Gleam, CUDA, Metal, OpenCL, MLIR, Why3, Lean, SPIR-V) — Wasm and Wasm-GC keep the original prog because Codegen.gen_imports handles their cross-module needs natively. Smoke-tested: caller_ok.affine (use CrossCallee::{consume}) now compiles to JS / Julia / Rust / Lua with consume's body inlined. The non-JSON compile_file path also gained ~loader on its previously-loaderless Codegen.generate_module call (latent bug for cross-module wasm via this path). 188 → 190 tests; 0 regressions."
session-note-2026-05-03 = "TYPED-WASM CROSS-MODULE CLOSURE + MCP CARTRIDGE REWIRE + WASMGC LOUD-FAIL HARDENING. (1) lib/codegen.ml — generate_module gained ?loader and a new gen_imports pass that walks prog.prog_imports, loads each referenced module via Module_loader, and emits one (import \"<modpath>\" \"<fn>\" (func ...)) entry per imported function plus a (local_alias_name → import_func_idx) entry in func_indices. ImportSimple is namespace-only (no emit), ImportList emits per item, ImportGlob enumerates public TopFns. Closes the cross-module WASM import emission gap called out in session-note-2026-04-19-a — `verify-boundary CALLEE.affine CALLER.affine` now works on user-authored AffineScript pairs, not just hand-assembled bridges. (2) lib/resolve.ml — import_resolved_symbols / import_specific_items / ImportGlob inline path now also write to dest type_ctx.name_types (not just var_types), with a new lookup_source_scheme helper that falls back from sym_id-keyed source_types to name-keyed source_name_types because resolve_and_typecheck_module's per-decl Typecheck.check_decl populates name_types but never var_types. lib/typecheck.ml — Typecheck.check_program gained ?import_types : (string, scheme) Hashtbl.t that seeds name_types after register_builtins, supplied by the resolver. bin/main.ml compile_file (JSON + non-JSON paths), compile_to_wasm_module, verify_file all updated to thread import_type_ctx.name_types through and pass ~loader to Codegen.generate_module. (3) test/e2e/fixtures/ — CrossCallee.affine + cross_caller_{ok,dup,drop}.affine, plus 3 new alcotest cases under E2E Boundary Verify exercising the full pipeline (parse → resolve_with_loader → typecheck-with-import-types → codegen-with-loader → Tw_interface.verify_cross_module). All three boundary outcomes (clean / LinearImportCalledMultiple / LinearImportDroppedOnSomePath) confirmed end-to-end. (4) boj-server/cartridges/typed-wasm-mcp — mod.js rewritten to call `affinescript` (was: nonexistent `typed-wasm` binary), with cwd set to the source's directory so Module_loader resolves relative imports correctly. cartridge.json bumped to v0.2.0 with corrected input descriptions (.affine source paths) and a new typed_wasm_verify_boundary tool exposing the cross-module verifier. README.adoc updated to match. (5) lib/codegen_gc.ml — eliminated three silent-bad-codegen fallbacks (same class as BUG-005): wildcard ExprLambda/ExprUnsafe → RefNull replaced with explicit UnsupportedFeature errors; match-arm wildcard PatTuple/PatRecord → fall-to-default replaced with UnsupportedFeature; PatLit fallback for LitFloat/LitString replaced with explicit errors. test/test_e2e.ml gained E2E WasmGC Loud-Fail suite with 2 regression markers. Bumps wasm-gc-codegen from 70% to 85% (silent-fallback gap is gone; effects/try-catch/lambda/call_ref remain genuinely deferred to upstream EH proposal or whole-program CPS). 180 → 182 tests; 0 regressions."
Expand Down
6 changes: 6 additions & 0 deletions bin/main.ml
Original file line number Diff line number Diff line change
Expand Up @@ -187,6 +187,7 @@ let check_file face json path =
resolve_refs := List.rev resolve_ctx.references;
(match Affinescript.Typecheck.check_program
~import_types:type_ctx.Affinescript.Typecheck.name_types
?qualified_member_check:type_ctx.Affinescript.Typecheck.qualified_member_check
resolve_ctx.symbols prog with
| Error e ->
add (Affinescript.Json_output.of_type_error e)
Expand Down Expand Up @@ -234,6 +235,7 @@ let check_file face json path =
| Ok (resolve_ctx, type_ctx) ->
(match Affinescript.Typecheck.check_program
~import_types:type_ctx.Affinescript.Typecheck.name_types
?qualified_member_check:type_ctx.Affinescript.Typecheck.qualified_member_check
resolve_ctx.symbols prog with
| Error e ->
Format.eprintf "@[<v>%s@]@."
Expand Down Expand Up @@ -497,6 +499,7 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
| Ok (resolve_ctx, import_type_ctx) ->
(match Affinescript.Typecheck.check_program
~import_types:import_type_ctx.Affinescript.Typecheck.name_types
?qualified_member_check:import_type_ctx.Affinescript.Typecheck.qualified_member_check
resolve_ctx.symbols prog with
| Error e ->
add (Affinescript.Json_output.of_type_error e)
Expand Down Expand Up @@ -716,6 +719,7 @@ let compile_file face json wasm_gc vscode_ext vscode_adapter vscode_no_lc
| Ok (resolve_ctx, import_type_ctx) ->
(match Affinescript.Typecheck.check_program
~import_types:import_type_ctx.Affinescript.Typecheck.name_types
?qualified_member_check:import_type_ctx.Affinescript.Typecheck.qualified_member_check
resolve_ctx.symbols prog with
| Error e ->
Format.eprintf "@[<v>%s@]@."
Expand Down Expand Up @@ -1060,6 +1064,7 @@ let compile_to_wasm_module face path
| Ok (resolve_ctx, import_type_ctx) ->
match Affinescript.Typecheck.check_program
~import_types:import_type_ctx.Affinescript.Typecheck.name_types
?qualified_member_check:import_type_ctx.Affinescript.Typecheck.qualified_member_check
resolve_ctx.symbols prog with
| Error e ->
Format.eprintf "%s: %s@." path
Expand Down Expand Up @@ -1120,6 +1125,7 @@ let verify_file face path =
| Ok (resolve_ctx, import_type_ctx) ->
(match Affinescript.Typecheck.check_program
~import_types:import_type_ctx.Affinescript.Typecheck.name_types
?qualified_member_check:import_type_ctx.Affinescript.Typecheck.qualified_member_check
resolve_ctx.symbols prog with
| Error e ->
Format.eprintf "%s@."
Expand Down
Loading
Loading