Repository navigation
LANG: type/effect grammar has no module-qualified path — Pkg.Type/Pkg.Effect unrepresentable (estate-wide port blocker; ADR-014) #228
Description
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.Res or Externs.Net is unrepresentable in any
type or effect position, so it fails with parse error at the ..
This was found while auditing the estate .affine corpus against
origin/main @417b97c with the compiler as oracle
(main.exe check). It is the single dominant fault estate-wide, not a
collection of dialect quirks: of ~1177 .affine across 28 repos, 525 fail to
parse, 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.Foo
qualified types/effects, so the corpus and the grammar disagree.
Grounded evidence (oracle probes on main @417b97c)
| Source | Result |
|---|---|
pub fn f() -{Net}-> () |
✅ parses (legacy -{E}-> is not the fault) |
pub fn f(x: mut Int) -> () / ref Int |
✅ parses (sigils fine) |
use Bar; pub fn f(x: Baz) -{Bar}-> () |
✅ parses (resolution-only error) |
pub fn f(x: Bar.Baz) -> () |
❌ parse error at the . |
pub type R = { a: Bar.Baz } |
❌ parse error at the . |
pub fn f() -{Bar.Baz}-> () |
❌ parse error at the . |
Root cause in lib/parser.mly:
type_expr_primary:(line 442) — the sole type producer — only has
upper_ident → TyCon/upper_ident LBRACKET … → TyApp. There is no
module_path DOT ident(or::) alternative anywhere in the type grammar.effect_term:— onlyname = ident(optionallyident LBRACKET …). An
effect 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 already
understands modules (use Bar; … Bar reaches a resolution error, not a parse
error). 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 in docs/specs/SETTLED-DECISIONS.adoc,
full ADR in .machine_readable/6a2/META.a2ml per the ADR-011/012 convention):
type_expr_primary: accept a qualified head —module_path <sep> upper_ident
(+ optionalLBRACKET 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) uses
DOT; use/value paths use COLONCOLON (ADR-011's Result::unwrap,
use option::{…}). The estate corpus uses . for qualified types. The ADR
should settle whether qualified type/effect references are Pkg.Type,
Pkg::Type, or both-accepted-one-canonical, consistent with the
existing module/use/:: machinery — this is the language owner's call and
should be fixed once, centrally, before any consumer adopts it.
Describe alternatives considered
- Estate consumer-port (rewrite every
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. - Do nothing / treat as "old dialect". Rejected: the prior diagnosis
("legacy-{E}->arrow") was empirically falsified above;-{E}->parses
fine. The corpus is not on an old dialect — it is using a construct the
grammar never supported.
Additional context
- STAGE-C peer. This is coordinated with the active STAGE-C / typed-wasm
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. - No estate porting will happen until this lands. After the grammar/ADR
decision, the estate.affinecorpus will be re-audited (most parse failures
are 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, tracked
separately. - Audit-first method also excluded ~500 false targets: affinescript's own
conformance/invalidnegative fixtures, and 3 repos
(bofj-kitt/proof-burrower/developer-ecosystem) that vendor a full copy of
the affinescript tree. - Oracle/build:
dune build bin/main.exe,AFFINESCRIPT_STDLIB=$PWD/stdlib,
main.exe check <file>; classify on stdout (Type checking passedvs
parse error). Estate repo CI does not compile.affine— local oracle
is the only validator.
Activity
docs(#228): record state in STATE.a2ml; disclose qualified-effect fix…
docs: truth CORE-03 / ADR-014 grammar landed (PR #241) (Refs #175 #176 …
docs: authoritative ledger — CAPABILITY-MATRIX/ECOSYSTEM/TECH-DEBT (R…
feat(parser)!: ADR-014 — module-qualified type/effect paths (Refs #228)
feat(parser)!: ADR-014 — module-qualified type/effect paths (Refs #228)…
test(xmod): INT-01 — prove + regression-lock cross-module wasm linkin…
docs(estate): #229 foundation — post-#228 re-audit, RS inventory, can…
docs(estate): #229 foundation — post-#228 re-audit, RS inventory, can…
hyperpolymath commentedon May 30, 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):
Tests (in `test/test_qualified_paths.ml`, wired into `test_main.ml`)
Out of scope (filed separately if owner agrees)
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).
feat(typecheck): module-qualified type/effect path resolution (ADR-014,…
feat(parser): accept lower_ident head in qualified_type_name (closes #…
hyperpolymath commentedon Jun 1, 2026
Status update — AS stdlib codegen pattern now established (2026-06-01) 📌
For ticket coordinators / anyone tracking this gap:
lib/codegen_deno.ml. Tests live intests/codegen-deno/*.What this means for the gap tracked in this ticket:
stdlib/*.affine→ wire indeno_builtinstable + prelude → smoke-test intests/codegen-deno/*.🤖 Cross-estate AS-status sweep on 2026-06-01.
feat(lang): `const mut <name>: T = init;` module-level mutable binding (
feat(parser): accept slash-effect-row in type-expression position (cl…
hyperpolymath commentedon Sep 19, 2026
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:
mainmodule_qualson the context, populated from importsstrip_module_qualifierhelperModule_resolution_errorUnknownModule+ boundary catchuseresolves · nouse→UnknownModule· effect position same path · bareTyConstays permissivetest/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.