Repository navigation
LANG: type/effect grammar has no module-qualified path — Pkg.Type/Pkg.Effect unrepresentable (estate-wide port blocker; ADR-014) #228
Description
Activity
- addedenhancementNew capability or improvement to existing behaviourNew capability or improvement to existing behaviourmajorMajor issue — significant scope, broader impact than a feature/bugMajor issue — significant scope, broader impact than a feature/bug
on May 18, 2026 - added 8 commits that reference this issue
on May 18, 2026 Current state + proposed next slice — pre-PR design comment
Parser layer (this issue's original gap): closed in #241 (sha).
Pkg.Type/Pkg::Type(mixed seps) now parse end-to-end at type and effect positions; ADR-014 inline comments are visible atlib/parser.mly:140–149 / 506–514 / 667–668. Folded form is canonical::-joined (formatter prints::, accepts.).Remaining gap (this PR's target). The folded
TyCon "Mod::Type"reachesTypecheck.lower_type_expr(lib/typecheck.ml:430) and silently falls through to abstractTCon "Mod::Type"— same lenient-on-unknown-TyCon behaviour as bare names. Sopub fn f(x: NoSuchMod.Thing) -> ()passes today (probed against HEAD). Effects are stricter — they reject — but with a misleading hint ("declare `effect NoSuchMod::IO;`").This is the symmetric piece to #178 /
lower_qualified_value_paths(lib/resolve.ml:969), which lowers `Mod.fn` value paths. Type/effect positions don't yet have the equivalent.Proposed change — smallest viable slice
Mirror #178 at the type/effect chokepoints in `typecheck.ml` (not via AST walk — avoids enumerating every type-position site):
- Add `module_quals : (string, unit) Hashtbl.t` to `Typecheck.context`; populate from `prog.prog_imports` (`ImportSimple` only; matches resolve.ml semantics) at `check_program` entry.
- New helper `strip_module_qualifier ctx name` — for any `name` containing `::`, split head; if `head` ∈ `module_quals` → return stripped tail; else raise `Module_resolution_error head`.
- Apply in `lower_type_expr` (`TyCon`, `TyApp` arms) and `lower_effect_expr` (`resolve` helper + `EffCon`).
- Add `UnknownModule of string` to `type_error`; catch the exception at `check_program` boundary (paralleling `Effect_validation_error → UnknownEffect`).
- Error text: `Unknown module 'X'. Add `use X;` (or `use X::{Type};`) to bring it into scope.`
Tests (in `test/test_qualified_paths.ml`, wired into `test_main.ml`)
- `type-qualified + use → strips and resolves locally` (`use Ajv; pub fn f(x: Ajv.Schema) -> () { () }`)
- `type-qualified, no use → UnknownModule error` (`pub fn f(x: NoSuchMod.Thing) -> () { () }`)
- `effect-qualified, no use → UnknownModule error` (`pub fn f() -{NoSuchMod.IO}-> () { () }`)
- regression: `bare TyCon still permissive` (no scope creep)
Out of scope (filed separately if owner agrees)
- Lowercase-module qualified refs don't parse — `qualified_type_name` head requires `upper_ident` (`lib/parser.mly:155`), so `json.Value` / `option.Option` are unrepresentable even though those modules exist (lowercase `module json;` in stdlib). Either rename stdlib modules to UpperCase (`Json`, `Option`, `Prelude`) or relax head to `ident` — both are parser-level decisions deferring to owner.
- `use Mod::Sub::{Item}` (multi-segment use with brace-list) fails parse against `{Item}` — adjacent grammar gap.
- `use A.B;` registers only `B` (last segment) as the qualifier — `A.B.T` ref then fails. Decision: should multi-segment use bind the whole path as one qualifier, or only the leaf? Tracks ADR-011.
- Bare unknown TyCons silently pass typecheck — pre-existing LANG: type/effect grammar has no module-qualified path —
Pkg.Type/Pkg.Effectunrepresentable (estate-wide port blocker; ADR-014) #228-unrelated leniency; widening would change behaviour for many downstream files and belongs to its own slice.
Refs not Closes
Refs #228 (this is one of several slices). Will file follow-up issues for the four out-of-scope items above.
— sketched against HEAD = `9f12643` (after #443).
Status update — AS stdlib codegen pattern now established (2026-06-01) 📌
For ticket coordinators / anyone tracking this gap:
- affinescript#522 + feat(stdlib): close 6 STEP-3 Deno-ESM gaps surfaced by TS→AS ports (Refs #239, #242) #504 merged 2026-06-01 establish the canonical stdlib extern → Deno-ESM lowering pattern via
lib/codegen_deno.ml. Tests live intests/codegen-deno/*. - affinescript#463 unblocked non-ASCII string emit in Deno-ESM (Unicode escapes, not octal).
- affinescript#421 put stdlib/json on the Zig-FFI rails (hpm-json-rsr).
What this means for the gap tracked in this ticket:
- The codegen pattern is now a known repeatable shape: declare in
stdlib/*.affine→ wire indeno_builtinstable + prelude → smoke-test intests/codegen-deno/*. - This ticket remains open as a tracked gap; no new mass-port is being kicked off from this post.
- Surfacing status so the broader picture stays accurate as more sub-tickets / dependent campaigns reference it.
🤖 Cross-estate AS-status sweep on 2026-06-01.
- affinescript#522 + feat(stdlib): close 6 STEP-3 Deno-ESM gaps surfaced by TS→AS ports (Refs #239, #242) #504 merged 2026-06-01 establish the canonical stdlib extern → Deno-ESM lowering pattern via
Verified implemented in full — closing.
The thread's own state comment split this into a parser layer and a remaining typecheck slice. Both have landed:
Parser layer — closed in #241 (
5cca379), as the thread records. Confirmed onmain:lib/parser.mlycarries thequalified_type_nameproduction with the inline note "ADR-014 (#228): module-qualified type/effect path", accepting.or::with::canonical.The remaining typecheck slice — landed since, and this is what I checked today. Every element the comment proposed is present:
Proposed On mainmodule_qualson the context, populated from importspresent strip_module_qualifierhelperpresent Module_resolution_errorpresent UnknownModule+ boundary catchpresent Tests: qualified + useresolves · nouse→UnknownModule· effect position same path · bareTyConstays permissiveall four in test/test_qualified_paths.mlPlus the follow-on for lowercase-qualifier heads (
lib/parser.mlyrecords it as #448 item 1), with its own test.The four out-of-scope items the comment listed were to be filed separately and are not part of this issue's acceptance criteria. Nothing in scope remains.
Is your feature request related to a problem? Please describe.
The type/effect grammar has no module-qualified path production. A qualified
reference such as
Externs.ResorExterns.Netis unrepresentable in anytype or effect position, so it fails with
parse errorat the..This was found while auditing the estate
.affinecorpus againstorigin/main@417b97c with the compiler as oracle(
main.exe check). It is the single dominant fault estate-wide, not acollection of dialect quirks: of ~1177
.affineacross 28 repos, 525 fail toparse, and the qualified-path gap is the leading cause in every hand-written
Frontier-playbook TS-port (accessibility-everywhere, idaptik, airborne-submarine-squadron,
and the per-file failures in aerie/ambientops/echidna/game-server-admin/git-scripts/
invariant-path/ubicity/panll). The Frontier playbook itself produces
Externs.Fooqualified types/effects, so the corpus and the grammar disagree.
Grounded evidence (oracle probes on main @417b97c)
pub fn f() -{Net}-> ()-{E}->is not the fault)pub fn f(x: mut Int) -> ()/ref Intuse Bar; pub fn f(x: Baz) -{Bar}-> ()pub fn f(x: Bar.Baz) -> ()parse errorat the.pub type R = { a: Bar.Baz }parse errorat the.pub fn f() -{Bar.Baz}-> ()parse errorat the.Root cause in
lib/parser.mly:type_expr_primary:(line 442) — the sole type producer — only hasupper_ident → TyCon/upper_ident LBRACKET … → TyApp. There is nomodule_path DOT ident(or::) alternative anywhere in the type grammar.effect_term:— onlyname = ident(optionallyident LBRACKET …). Aneffect cannot be module-qualified at all.
This contradicts an already-settled decision
ADR-011 (Stdlib Namespace Model: Real Modules, SETTLED-DECISIONS.adoc:136)
states the language uses real modules with qualified paths, and that this
is "consistent with the machinery the compiler (
module_loader.ml,module/use/::grammar) … already use." The resolution layer alreadyunderstands modules (
use Bar; … Barreaches a resolution error, not a parseerror). But the type/effect grammar cannot name a module-qualified entity,
so a core consequence of ADR-011 is unspeakable. Per ADR-012 (Grammar Changes
Are Correctness Assertions), closing this gap is asserting a truth the language
already settled, not adding surface sugar.
Describe the solution you'd like
Add a module-qualified path production to the type/effect grammar, and record
it as ADR-014 (new
==section indocs/specs/SETTLED-DECISIONS.adoc,full ADR in
.machine_readable/6a2/META.a2mlper the ADR-011/012 convention):type_expr_primary: accept a qualified head —module_path <sep> upper_ident(+ optional
LBRACKET type_arg,* RBRACKET) → a qualifiedTyCon/TyApp.effect_term: accept the same qualified head forEffVar/EffCon.Open language-side decision for the ADR (deliberately not pre-decided here —
escalating, not guessing): the separator.
module_path(parser.mly:138) usesDOT;use/value paths useCOLONCOLON(ADR-011'sResult::unwrap,use option::{…}). The estate corpus uses.for qualified types. The ADRshould settle whether qualified type/effect references are
Pkg.Type,Pkg::Type, or both-accepted-one-canonical, consistent with theexisting module/use/
::machinery — this is the language owner's call andshould be fixed once, centrally, before any consumer adopts it.
Describe alternatives considered
Pkg.Type→bareuse Pkg::{…}across~hundreds of files). Rejected: mechanical-but-unsound, churns hundreds of
files, entrenches a workaround against the Frontier playbook the ports
follow, and absorbs a language-design gap into N consumer repos instead of
resolving it at source.
("legacy
-{E}->arrow") was empirically falsified above;-{E}->parsesfine. The corpus is not on an old dialect — it is using a construct the
grammar never supported.
Additional context
line (Refs stdlib Http: typed-wasm target (convergence ABI; shared with Ephapax) #225, stdlib: portable Http.fetch (body/headers/methods/response) #160) as a peer workstream — the qualified type/effect form
must equal the STAGE-C canonical; real-world estate drift data should inform
STAGE-C prioritisation. Not "Closes" — language-side design decision, human-gated.
decision, the estate
.affinecorpus will be re-audited (most parse failuresare expected to clear with zero consumer churn); only the genuine
ReScript-surface residue (burble-class:
array<>,Dict.t<>,mutable,type rec,%%raw,(~x) =>,List(X)) is a true consumer port, trackedseparately.
conformance/invalidnegative fixtures, and 3 repos(bofj-kitt/proof-burrower/developer-ecosystem) that vendor a full copy of
the affinescript tree.
dune build bin/main.exe,AFFINESCRIPT_STDLIB=$PWD/stdlib,main.exe check <file>; classify on stdout (Type checking passedvsparse error). Estate repo CI does not compile.affine— local oracleis the only validator.