Skip to content

Commit 3ad1bed

Browse files
committed
docs(#228): record state in STATE.a2ml; disclose qualified-effect fixture limitation
- .machine_readable/6a2/STATE.a2ml: bump last-updated; add session-note-2026-05-18 recording #228/ADR-014/PR#231 (machine state/progress convention was stale). - docs/specs/SETTLED-DECISIONS.adoc (ADR-014): add 'Known limitation' paragraph — qualified-effect positive fixture is harness-only (no public stdlib effect for a self-contained positive); bare-oracle audits must exclude it. - tests/conformance/qualified-paths/valid/qualified_effect.affine: header now states the harness-search-path dependency explicitly. Turns a silent landmine into a stated limitation. No code change. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent 050c74b commit 3ad1bed

3 files changed

Lines changed: 30 additions & 1 deletion

File tree

.machine_readable/6a2/STATE.a2ml

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@
44
[metadata]
55
project = "affinescript"
66
version = "0.1.0"
7-
last-updated = "2026-05-03"
7+
last-updated = "2026-05-18"
88
status = "active"
9+
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)."
910
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."
1011
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."
1112
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."

docs/specs/SETTLED-DECISIONS.adoc

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -276,5 +276,20 @@ pre-change parser). Typecheck/codegen are unchanged: a qualified
276276
ident is treated by its resolved symbol exactly like an unqualified
277277
one (the validation, not the representation, is the soundness gate).
278278

279+
*Known limitation (conformance harness).* The qualified-*type*
280+
positive fixture is self-contained (`prelude.Option`, a stdlib
281+
module). The qualified-*effect* positive fixture
282+
(`tests/conformance/qualified-paths/valid/qualified_effect.affine`)
283+
must reference a sibling support module (`effmod`) because **no stdlib
284+
module currently declares a public effect**, so it resolves only with
285+
the test harness's module search-path and *fails a bare `affinescript
286+
check`*. Bare-oracle audits (including the estate dialect-conformance
287+
`.affine-audit` harness) must treat that one fixture as harness-only.
288+
Qualified-effect resolution itself is sound and independently proven
289+
by the `invalid/private_member` fixture (it loads stdlib `effects` and
290+
rejects the private member). Resolving this fully — a public stdlib
291+
effect for a self-contained positive — is tracked as follow-up, not a
292+
blocker for the language decision.
293+
279294
Refs issue #228 (language decision human-gated; not auto-closed). Full
280295
ADR in `.machine_readable/6a2/META.a2ml` (ADR-014).

tests/conformance/qualified-paths/valid/qualified_effect.affine

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,19 @@
55
// `effmod.Logging` must load module `effmod`, resolve+typecheck it,
66
// and find `Logging` as a Public SKEffect *within that module*.
77
// Expected: type checking passes.
8+
//
9+
// ⚠️ KNOWN LIMITATION (issue #228 / ADR-014): the sibling support
10+
// module `effmod` is NOT stdlib, so this fixture resolves ONLY when
11+
// the test harness adds this directory to the module-loader
12+
// search-path. A bare `affinescript check` on this file alone WILL
13+
// fail with "Unknown module 'effmod'". There is no public stdlib
14+
// effect to build a self-contained positive (unlike `prelude.Option`
15+
// for the qualified-*type* fixture). Bare-oracle audits — including
16+
// the estate dialect-conformance `.affine-audit` harness — MUST treat
17+
// THIS fixture as harness-only and exclude it from drift counts.
18+
// Qualified-effect resolution itself is sound and independently
19+
// proven by invalid/private_member.affine (loads stdlib `effects`,
20+
// rejects the private member).
821
module ConformanceQualifiedEffect;
922

1023
pub fn act(x: Int) -> Int / effmod.Logging {

0 commit comments

Comments
 (0)