feat(parser)!: ADR-014 — module-qualified type/effect paths (Refs #228) by hyperpolymath · Pull Request #241 · hyperpolymath/affinescript · GitHub
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
66 changes: 66 additions & 0 deletions .machine_readable/6a2/META.a2ml
27 changes: 27 additions & 0 deletions docs/specs/SETTLED-DECISIONS.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -243,3 +243,30 @@ agreed async ABI for the typed-wasm convergence layer; Ephapax is a
co-stakeholder (typed-wasm ADR-004). Delivered as 4 incremental,
gated PRs. Full design in `docs/specs/async-on-wasm-cps.adoc`; full ADR
in `.machine_readable/6a2/META.a2ml` (ADR-013).

== Module-Qualified Type/Effect Path Separator (ADR-014)

The type/effect grammar had no module-qualified path production, so a
qualified reference like `Externs.Res` was *unrepresentable* in any type
or effect position (`parse error` at the `.`). An estate audit
(compiler-as-oracle) found this the single dominant fault: 525 of ~1177
`.affine` files fail to parse, qualified-path the leading cause. ADR-011
already settled real modules with qualified paths; this consequence was
simply unspeakable. The separator was the escalated, owner-decided
question (`module_path` uses `.`; `use`/value paths use `::`; the corpus
uses `.` for types).

Decision: accept *both* `.` and `::` (mixed permitted); *`Pkg::Type` is
canonical* (consistent with ADR-011 value paths); `.` is tolerated and
normalised to `::`. The parser folds a qualified name into one canonical
`::`-joined ident, so resolve/typecheck/codegen need no change and the
formatter prints the canonical form for free. Conflict-neutral by
construction (it only adds `.`/`::` lookahead where no reduce action
existed — exactly the parse-error void): measured Menhir conflict states
unchanged at 21 S/R + 1 R/R, item counts unchanged at 68 S/R / 7 R/R.
The grammar PR unblocks *parsing*; most estate parse failures clear with
zero consumer churn (genuine ReScript-surface residue is #229).

This decision is settled; do not reopen without amending the ADR. Full
ADR in `.machine_readable/6a2/META.a2ml` (ADR-014); ledger CORE-03 in
`docs/TECH-DEBT.adoc`.
43 changes: 43 additions & 0 deletions lib/parser.mly
Original file line number Diff line number Diff line change
Expand Up @@ -137,6 +137,29 @@ module_path:
| id = ident { [id] }
| path = module_path DOT id = ident { path @ [id] }

/* ADR-014 (#228): module-qualified type/effect path. Separator is `.` or
`::` (mixed permitted); `::` is canonical (consistent with ADR-011 value
paths). A qualified name is >=2 `upper_ident` segments; it is folded to a
single canonical `::`-joined string so downstream (resolve/typecheck/all
codegens, formatter) sees one ident and needs no change — the formatter
prints the stored `::` form, normalising any `.` input for free.
Right-recursive + restricted to `upper_ident` so it only adds
`DOT`/`COLONCOLON` lookahead after a type/effect-position `upper_ident`
(no prior reduce action there — that void is the `parse error at .`
#228 reports), introducing zero new LR conflicts. */
%inline qsep:
| DOT { () }
| COLONCOLON { () }

qualified_type_name:
| head = upper_ident qsep rest = qualified_type_name_rest
{ head ^ "::" ^ rest }

qualified_type_name_rest:
| name = upper_ident { name }
| name = upper_ident qsep rest = qualified_type_name_rest
{ name ^ "::" ^ rest }

/* ========== Imports ========== */

import_decl:
Expand Down Expand Up @@ -450,6 +473,20 @@ type_expr_primary:
| MUT ty = type_expr_primary { TyMut ty }
| name = lower_ident { TyVar (mk_ident name $startpos $endpos) }
| name = upper_ident { TyCon (mk_ident name $startpos $endpos) }
/* ADR-014 (#228): module-qualified type name. `Pkg.Type` and
`Pkg::Type` (and deeper, `A.B.C` / `A::B::C`, mixed separators) are
accepted; the segments are folded into a single canonical name joined
by `::`, so `::` is the canonical form on print with no formatter
change and `.` is silently normalised. Conflict-safe: this only adds
`DOT`/`COLONCOLON` as lookahead after a type-position `upper_ident`,
where no reduce action previously existed (that absence is exactly the
`parse error at .` #228 reports), so it introduces no new LR conflict.
Resolution treats the `::`-joined name as a qualified reference. */
| name = qualified_type_name { TyCon (mk_ident name $startpos $endpos) }
| name = qualified_type_name LBRACKET args = separated_nonempty_list(COMMA, type_arg) RBRACKET
{ TyApp (mk_ident name $startpos $endpos, args) }
| name = qualified_type_name LT args = separated_nonempty_list(COMMA, type_arg) GT
{ TyApp (mk_ident name $startpos $endpos, args) }
| name = upper_ident LBRACKET args = separated_nonempty_list(COMMA, type_arg) RBRACKET
{ TyApp (mk_ident name $startpos(name) $endpos(name), args) }
/* Angle-bracket alias for type application: `Option<T>` ≡ `Option[T]`,
Expand Down Expand Up @@ -569,6 +606,12 @@ effect_term:
| name = ident { EffVar name }
| name = ident LBRACKET args = separated_list(COMMA, type_arg) RBRACKET
{ EffCon (name, args) }
/* ADR-014 (#228): module-qualified effect, e.g. `-{Externs.Net}->` /
`-{Externs::Net}->`. Same canonical `::`-fold as qualified types. */
| name = qualified_type_name
{ EffVar (mk_ident name $startpos $endpos) }
| name = qualified_type_name LBRACKET args = separated_list(COMMA, type_arg) RBRACKET
{ EffCon (mk_ident name $startpos $endpos, args) }

/* ========== Traits ========== */

Expand Down
58 changes: 58 additions & 0 deletions test/test_e2e.ml
Loading