spec: revise canonical type names — module-scoped, bare=local
This commit is contained in:
@@ -1,81 +1,83 @@
|
||||
# Canonical Type Names — AST is Context-Free — Design Spec
|
||||
# Canonical Type Names — Module-Scoped Qualification — Design Spec
|
||||
|
||||
**Date:** 2026-05-10 (revised 2026-05-11 after user clarification)
|
||||
**Date:** 2026-05-10 (revised twice: once on 2026-05-10, again on 2026-05-11 after user clarification on module-scoped semantics)
|
||||
**Status:** Draft — awaiting user spec review
|
||||
**Authors:** Brummel (orchestrator) + Claude
|
||||
|
||||
## Goal
|
||||
|
||||
Establish the invariant that every AST object stands alone, hashable
|
||||
and reasonable about without an external module context. Concretely:
|
||||
**every non-primitive `Type::Con` in `.ail.json` is fully qualified
|
||||
(`module.Type`).** Bare names are a schema violation in JSON. The
|
||||
LLM-author surface (`.ailx` prose) continues to allow bare-local
|
||||
names, because the surrounding module context is right there in the
|
||||
source — the prose parser qualifies on read, the prose printer trims
|
||||
on write. The bare-vs-qualified divergence is resolved at the
|
||||
surface/JSON boundary, not inside the compiler.
|
||||
Establish a single, unambiguous rule for `Type::Con` names in
|
||||
`.ail.json`:
|
||||
|
||||
The user's directive on 2026-05-11: *"You can omit full qualification
|
||||
of local names in `.ailx` files because it follows directly from the
|
||||
- The file's top-level `"name"` field is THE module context for
|
||||
everything inside.
|
||||
- A bare `Type::Con.name` always means *local to this module*.
|
||||
- A cross-module reference MUST be qualified `"<owning_module>.<TypeName>"`
|
||||
— bare cross-module refs are a schema violation.
|
||||
- Primitives (`Int`, `Bool`, `Str`, `Unit`, `Float`) stay bare and
|
||||
have no module qualifier.
|
||||
|
||||
Under this rule, every `Type::Con` is unambiguous: bare = local,
|
||||
qualified = explicitly cross-module. The bug-class fixed by this
|
||||
milestone (cross-module references silently mismatching when one
|
||||
side stays bare and another resolves through an imports-fallback)
|
||||
becomes structurally impossible — there is no bare cross-module
|
||||
form in canonical JSON.
|
||||
|
||||
The user's directive on 2026-05-11: *"You can omit local-name
|
||||
qualification in `.ailx` files because it follows directly from the
|
||||
module name — that's source code. But in the AST, i.e. in the data
|
||||
structure, i.e. in the `.ail.json` files, everything must be fully
|
||||
qualified. Every AST object must stand alone, without a module
|
||||
context."*
|
||||
context."* And after seeing `bench_mono_dispatch.ail.json`'s
|
||||
existing shape: *"the module qualifier is at the top of the file
|
||||
already (`"name": "..."`); references within the file can stay bare
|
||||
when intra-module; cross-module refs need explicit qualifier."*
|
||||
|
||||
This closes the long-running design ambiguity that produced five
|
||||
separate in-tree imports-fallback / qualifier-helper mechanisms
|
||||
(iters 15a / 23.1.3 / 23.1.4 / 23.2 / 23.2.4). Each existed because
|
||||
some sites carried bare type names while others produced qualified
|
||||
ones; values crossing module boundaries misaligned. With the new
|
||||
invariant, the divergence cannot occur — there is one canonical form
|
||||
for type names in the AST, and it is qualified.
|
||||
The module IS the unit of context. A single `Type::Con` in
|
||||
isolation is interpretable iff you also have its owning module's
|
||||
`"name"`. The .ail.json file already pairs these (one Module per
|
||||
file, with `"name"` at the top and Defs nested inside). This spec
|
||||
formalises that pairing as the canonical-form rule.
|
||||
|
||||
## Architecture
|
||||
|
||||
The `.ail.json` schema becomes stricter:
|
||||
The `.ail.json` schema gains a load-time validation rule:
|
||||
|
||||
- Every `Type::Con { name }` is either a primitive (`Int`, `Bool`,
|
||||
`Str`, `Unit`, `Float`) or carries a single `.` separator
|
||||
(`module.Type`). Bare non-primitive names are **a schema
|
||||
violation**, rejected by `load_workspace` with a clear diagnostic.
|
||||
- `Type::Var` is unchanged — a different AST variant via the `"k"`
|
||||
tag in JSON, never a qualifier candidate.
|
||||
- `Term::Ctor.type_name` follows the same rule — it's a Type::Con name.
|
||||
For every `Type::Con { name, args }` encountered while loading a
|
||||
module `M`:
|
||||
|
||||
The Form-B prose surface (`.ailx`) is the bridge between LLM-author
|
||||
convenience and AST canonicality:
|
||||
1. If `name.contains('.')`: must be `<owner>.<type>` where `<owner>`
|
||||
is a known module (the workspace's module map) AND `<type>` is a
|
||||
type def in `<owner>`. Else `WorkspaceLoadError::BadCrossModuleTypeRef`.
|
||||
2. Else if `name` is a primitive (`Int`/`Bool`/`Str`/`Unit`/`Float`):
|
||||
accepted, no further check.
|
||||
3. Else (bare non-primitive): `name` must be a type def in `M`'s own
|
||||
`defs`. Else `WorkspaceLoadError::BareCrossModuleTypeRef { module: M, name, candidates }` —
|
||||
candidates lists the qualified forms found in `M`'s imports so
|
||||
the error message can suggest the right rewrite.
|
||||
|
||||
```
|
||||
.ailx (Form-B, bare-tolerant surface for the LLM)
|
||||
|
|
||||
| prose parser: bare local → qualified (qualification at parse)
|
||||
v
|
||||
.ail.json (Form-A, canonical — all Type::Con qualified)
|
||||
|
|
||||
v
|
||||
load_workspace
|
||||
|
|
||||
+-- parse + import DFS (existing)
|
||||
+-- prelude inject (existing, iter 23.1)
|
||||
+-- schema validation: reject bare non-primitive Type::Con <-- NEW
|
||||
+-- validate_classdefs (existing)
|
||||
+-- build_registry (existing — sees canonical types)
|
||||
|
|
||||
v
|
||||
check_workspace (sees canonical AST — no fallbacks needed)
|
||||
check / mono / codegen / hash
|
||||
```
|
||||
The same rule applies to `Term::Ctor.type_name` (which is conceptually
|
||||
a Type::Con name embedded in a Term).
|
||||
|
||||
The prose printer (when writing `.ailx` from a `.ail.json`) trims a
|
||||
qualifier when it equals the current module name — so the surface
|
||||
stays compact for the LLM author. The prose parser (when reading
|
||||
`.ailx` into a `.ail.json`) qualifies bare names. Round-trip is
|
||||
semantics-preserving.
|
||||
`Type::Var` is unchanged — different AST variant via the `"k"` tag,
|
||||
never a qualifier candidate.
|
||||
|
||||
The Form-B prose surface (`.ailx`) continues to allow bare-local
|
||||
names everywhere. The prose printer/parser are the bridge: when
|
||||
writing prose from a canonical `.ail.json`, the printer trims a
|
||||
qualifier if its owner equals the current file's module (`prelude.Ordering`
|
||||
in prelude's own file → printed as `Ordering`). When parsing prose,
|
||||
the parser keeps bare names bare (they're local by construction
|
||||
since the prose was written in the context of a single module).
|
||||
Cross-module refs in prose carry their qualifier verbatim through
|
||||
both directions.
|
||||
|
||||
There is **no internal canonicalisation pass** in `load_workspace`.
|
||||
The AST is canonical because the JSON is canonical, enforced by
|
||||
schema validation at the workspace boundary.
|
||||
The AST is already canonical post-parse because bare = local is an
|
||||
unambiguous rule. The validator catches stale fixtures that have
|
||||
bare cross-module refs (legacy state from when imports-fallback
|
||||
papered over the issue).
|
||||
|
||||
## Components
|
||||
|
||||
@@ -83,154 +85,150 @@ The milestone decomposes into four iterations.
|
||||
|
||||
| Iter | Scope |
|
||||
|------|-------|
|
||||
| **ct.1** | `.ail.json` schema validation: reject bare non-primitive `Type::Con` names at `load_workspace` time with `WorkspaceLoadError::BareTypeNameInJson { module, name }`. Plus the one-time migration: a workspace-walker (e.g. `ail migrate-types` or a small Rust binary in `crates/ail/src/bin/`) that reads every `.ail.json` under `examples/`, looks up each bare non-primitive Type::Con's defining module via the import graph + the existing imports-fallback logic, rewrites to qualified form, and writes back. Run once, commit the rewritten fixtures. After this iter, the entire `examples/` corpus is canonical. |
|
||||
| **ct.2** | Remove the obsolete typechecker mechanisms: `Pattern::Ctor`'s imports-fallback (iter 15a, lib.rs:2486-2521), `Term::Ctor`'s synth imports-fallback (iter 23.1.3, lib.rs:1979-2025), and `qualify_local_types` (iter 15a, lib.rs:1848). Each removal is RED-tested against an existing fixture that exercised the fallback path (now passes through the qualified-AST path). The per-module `env.types` overlay in `check_in_workspace` (~lib.rs:1247-1287) may also collapse — `env.types` becomes workspace-flat with qualified keys. |
|
||||
| **ct.3** | Remove obsolete codegen + mono mechanisms: `lookup_ctor_by_type`'s imports-fallback (iter 23.1.4) and `apply_per_module_ctor_index_overlay` in mono (iter 23.2 fix `84dcc46`). The codegen `lower_workspace_inner` implicit-prelude import_map entry (iter 23.2.4 fix `be882c4`) STAYS — that's fn-name resolution for mono symbols, not Type::Con names. |
|
||||
| **ct.4** | DESIGN.md amendments + JOURNAL migration entry + re-baseline of hash-pinned regression tests + IR snapshot refresh. Decision 2 amended to name the canonical-form rule explicitly. Form-B prose printer/parser verified to round-trip cleanly across the bare/qualified boundary (write a round-trip test if not already covered). |
|
||||
| **ct.1** | `.ail.json` schema validator at `load_workspace` (the rule above) + one-time migration. The migration is targeted: a workspace-walker tool (small Rust binary or test-driven script under `crates/ail/src/bin/`) reads every `.ail.json` under `examples/`, finds bare cross-module Type::Con names (i.e. bare names that don't resolve to a local type def in the same file), looks up the qualified form via the import graph, rewrites in place. Files with only intra-module refs are unchanged. Single bulk commit for the rewritten fixtures. |
|
||||
| **ct.2** | Remove obsolete typechecker mechanisms that papered over bare cross-module refs: `Pattern::Ctor`'s imports-fallback (iter 15a, ~`crates/ailang-check/src/lib.rs:2486-2521`) and `Term::Ctor`'s synth imports-fallback (iter 23.1.3, ~lib.rs:1979-2025). Both become unreachable because bare-non-primitive type-name reaches them only when local (direct lookup succeeds) — never as a stale cross-module ref. **`qualify_local_types` (iter 15a, ~lib.rs:1848) STAYS** — it correctly handles the cross-boundary translation when a cross-module fn signature is pulled into the current module's typecheck context. Under the new rule it becomes more prominent (called uniformly at every cross-module fn-lookup boundary, where today it runs only at the Term::Var prefix-qualified path); this iter audits its call sites for consistency. |
|
||||
| **ct.3** | Remove obsolete codegen + mono mechanisms: `lookup_ctor_by_type`'s imports-fallback (iter 23.1.4) and `apply_per_module_ctor_index_overlay` in mono (iter 23.2 fix `84dcc46`). The codegen `lower_workspace_inner` implicit-prelude import_map entry (iter 23.2.4 fix `be882c4`) STAYS — fn-name resolution for mono symbols, not Type::Con names. |
|
||||
| **ct.4** | DESIGN.md amendments + JOURNAL migration entry + re-baseline of hash-pinned regression tests that touched cross-module-ref fixtures + IR snapshot refresh (where affected). Form-B prose round-trip test extended to cover the qualifier-trim-on-print behaviour for intra-module refs. |
|
||||
|
||||
After ct.4, iter 23.3 Task 4+5 resume (recreate
|
||||
`examples/compare_primitives_smoke.ail.json` per the iter 23.3 plan;
|
||||
`examples/compare_primitives_smoke.ail.json` per the iter-23.3 plan;
|
||||
expected to pass without further compiler changes).
|
||||
|
||||
## Data flow
|
||||
|
||||
### Example 1: surface → JSON → AST
|
||||
### Example 1: intra-module file (no changes from today)
|
||||
|
||||
LLM-author writes in `.ailx` (module `foo`):
|
||||
```
|
||||
fn main : () -> Unit ![IO] {
|
||||
let result = compare(1, 2);
|
||||
match result { LT -> 1; EQ -> 2; GT -> 3 }
|
||||
}
|
||||
```
|
||||
`examples/bench_mono_dispatch.ail.json` has `"name": "bench_mono_dispatch"`
|
||||
at the top and references to local `class Foo` / `class Looper` /
|
||||
`instance Foo Int` / etc. All Type::Con names within the file are
|
||||
bare and refer to local types or primitives.
|
||||
|
||||
Prose parser produces `.ail.json` for module `foo`. The `compare` call
|
||||
returns `Ordering` (defined in prelude). The match arm patterns
|
||||
`LT/EQ/GT` are ctor names — the parser knows they belong to
|
||||
`prelude.Ordering` because `compare` resolves to a prelude method, but
|
||||
**the JSON doesn't need to encode where they came from**. Pattern
|
||||
ctor names continue to be bare (their parent type is reconstructible
|
||||
from the scrutinee's type at typecheck).
|
||||
Under the new rule:
|
||||
- Validator runs: `Foo` resolves locally; `Int` is primitive; no
|
||||
cross-module refs anywhere. Accepted unchanged.
|
||||
- AST is canonical post-parse. No rewriting, no hash shift.
|
||||
|
||||
What the parser DOES qualify: any standalone `Type::Con` reference.
|
||||
If `foo` declares `fn helper : (Ordering) -> Int { ... }`, the prose
|
||||
parser writes:
|
||||
```json
|
||||
{
|
||||
"name": "helper",
|
||||
"type": {
|
||||
"k": "fn",
|
||||
"params": [{ "k": "con", "name": "prelude.Ordering" }],
|
||||
"ret": { "k": "con", "name": "Int" },
|
||||
"effects": []
|
||||
},
|
||||
...
|
||||
}
|
||||
```
|
||||
This file's hash is bit-identical pre- and post-milestone.
|
||||
|
||||
`prelude.Ordering` qualified, `Int` (primitive) bare.
|
||||
### Example 2: cross-module file (migration target)
|
||||
|
||||
### Example 2: AST already canonical — no resolution needed downstream
|
||||
A user-written fixture (e.g. iter 23.3 Task 4's
|
||||
`examples/compare_primitives_smoke.ail.json` once recreated) has
|
||||
`"imports": [{ "module": "prelude" }]` and a `Term::Ctor { type_name: "Ordering", ctor: "LT" }` —
|
||||
bare cross-module reference. Today this passes through the iter
|
||||
23.1.3 imports-fallback.
|
||||
|
||||
`load_workspace` reads the qualified JSON. Schema validation passes
|
||||
(every non-primitive Type::Con has the qualifier). `build_registry`
|
||||
indexes instances by qualified type name. `check_workspace` runs;
|
||||
when it encounters `Type::Con { name: "prelude.Ordering" }` it looks
|
||||
up the type in workspace-flat `env.types["prelude.Ordering"]` directly.
|
||||
Pattern::Ctor against `LT`: the scrutinee's type (already qualified)
|
||||
gives the TypeDef directly; check ctor name match against its
|
||||
ctors list. No fallback. No imports walking.
|
||||
Under the new rule:
|
||||
- Validator rejects: `Ordering` is bare, not in this module's local
|
||||
types, not a primitive → `BareCrossModuleTypeRef { module: "compare_primitives_smoke", name: "Ordering", candidates: ["prelude.Ordering"] }`.
|
||||
- Migration tool rewrites: `Term::Ctor { type_name: "prelude.Ordering", ctor: "LT" }`. Diff is minimal — just the qualifier prepended.
|
||||
- Post-migration: validator passes; typechecker sees the qualified
|
||||
type, looks up `prelude.Ordering` directly in `env.types`, no
|
||||
fallback needed.
|
||||
|
||||
### Example 3: the iter 23.3 bug, post-migration
|
||||
### Example 3: the iter 23.3 bug, post-milestone
|
||||
|
||||
Prelude's `compare` method's return type in `.ail.json`:
|
||||
```json
|
||||
{ "name": "compare", "type": { "k": "fn", ..., "ret": { "k": "con", "name": "prelude.Ordering" }, ... } }
|
||||
{ "k": "fn", ..., "ret": { "k": "con", "name": "Ordering" }, ... }
|
||||
```
|
||||
|
||||
User-module call `compare 1 2` returns `prelude.Ordering`. Pattern
|
||||
`LT` resolves against the qualified scrutinee. Trivial match.
|
||||
**The bug is structurally impossible** because there's no bare
|
||||
form internally that could mismatch.
|
||||
This is *prelude's own file* — `Ordering` is local. Bare. Accepted
|
||||
unchanged. Hash bit-identical.
|
||||
|
||||
When user-side typechecker pulls this signature across the module
|
||||
boundary (`compare 1 2` in module `compare_primitives_smoke`), the
|
||||
`qualify_local_types` helper rewrites the bare `Ordering` to
|
||||
`prelude.Ordering` *in the consumer's typecheck context*. The
|
||||
user-side scrutinee's type becomes `prelude.Ordering`. Pattern
|
||||
`LT` resolves against that qualified type's TypeDef directly. No
|
||||
mismatch. **Bug structurally closed.**
|
||||
|
||||
The fix: ensure `qualify_local_types` runs uniformly at every
|
||||
cross-module fn-lookup site (today's coverage is partial; ct.2
|
||||
audits and extends).
|
||||
|
||||
## Error handling
|
||||
|
||||
One new diagnostic, fired at workspace-load time:
|
||||
One new diagnostic at workspace load:
|
||||
|
||||
- `WorkspaceLoadError::BareTypeNameInJson { module, name }`:
|
||||
"Module `M` contains a bare type name `T`; AST type names must be
|
||||
fully qualified (`<module>.T`). Primitive names (`Int`, `Bool`,
|
||||
`Str`, `Unit`, `Float`) are the only bare names allowed in
|
||||
`.ail.json`. Run the migration tool to fix legacy fixtures."
|
||||
- `WorkspaceLoadError::BareCrossModuleTypeRef { module, name, candidates }`:
|
||||
"Module `M` contains bare type name `T` that does not resolve to a
|
||||
local type. AILang's `.ail.json` requires cross-module type
|
||||
references to be qualified. Candidates from imports: `[<list>]`.
|
||||
Run the migration tool to fix legacy fixtures."
|
||||
|
||||
The diagnostic message points the LLM-author at the migration tool
|
||||
explicitly, so a stale `.ail.json` produces a self-explanatory error.
|
||||
- `WorkspaceLoadError::BadCrossModuleTypeRef { module, name }`:
|
||||
"Module `M` references qualified type `<owner>.<type>` but `<owner>`
|
||||
is not a known module (or does not declare a type named `<type>`)."
|
||||
|
||||
The existing typechecker diagnostics (`UnknownType`, `AmbiguousType`,
|
||||
`UnknownCtor`, `PatternTypeMismatch`) keep their semantics but their
|
||||
underlying mechanism simplifies: they fire on qualified-name lookup
|
||||
failures, never on bare-vs-qualified mismatches.
|
||||
Existing typechecker diagnostics (`UnknownType`, `UnknownCtor`,
|
||||
`PatternTypeMismatch`) keep their semantics but fire less often —
|
||||
many ambiguity cases are caught earlier by the validator.
|
||||
|
||||
## Testing strategy
|
||||
|
||||
- **Schema validator unit tests** in `crates/ailang-core/src/workspace.rs`:
|
||||
- JSON with `Type::Con { name: "Foo" }` (bare non-primitive) is rejected with `BareTypeNameInJson`.
|
||||
- JSON with `Type::Con { name: "Int" }` (primitive bare) is accepted.
|
||||
- JSON with `Type::Con { name: "M.Foo" }` (qualified) is accepted.
|
||||
- JSON with nested qualification in args (e.g. `List<m.Foo>`) walks correctly.
|
||||
- **Schema validator unit tests** in
|
||||
`crates/ailang-core/src/workspace.rs::mod tests`:
|
||||
- JSON with `Type::Con { name: "LocalType" }` and a matching local
|
||||
TypeDef: accepted.
|
||||
- JSON with `Type::Con { name: "Ordering" }` and no local Ordering,
|
||||
no imports: rejected with `BareCrossModuleTypeRef`, empty candidates.
|
||||
- JSON with `Type::Con { name: "Ordering" }` and prelude imported:
|
||||
rejected with `BareCrossModuleTypeRef`, candidates = `["prelude.Ordering"]`.
|
||||
- JSON with `Type::Con { name: "Int" }` (primitive): accepted.
|
||||
- JSON with `Type::Con { name: "prelude.Ordering" }` and prelude
|
||||
imported: accepted.
|
||||
- JSON with `Type::Con { name: "Mystery.Type" }` and `Mystery` not
|
||||
a known module: rejected with `BadCrossModuleTypeRef`.
|
||||
- Nested qualification in args (e.g. `List<prelude.Ordering>`)
|
||||
walks correctly.
|
||||
|
||||
- **Migration tool tests**: a small synthetic 2-module workspace with
|
||||
bare type names; the migration tool produces a workspace where every
|
||||
Type::Con is qualified. Idempotent (running twice produces the same
|
||||
result).
|
||||
- **Migration tool tests**: synthetic workspace with a bare
|
||||
cross-module ref → migrated form has the qualifier. Idempotent
|
||||
(re-running produces the same result).
|
||||
|
||||
- **Form-B round-trip tests** (extend existing prose round-trip
|
||||
coverage if needed): `.ailx` with bare local names → parsed
|
||||
`.ail.json` with qualified names → printed `.ailx` with bare-trimmed
|
||||
local names. The `.ailx` text round-trips byte-stable; the `.ail.json`
|
||||
text is canonical (qualified).
|
||||
- **Form-B prose round-trip tests**: prose with bare-local type refs
|
||||
→ parsed `.ail.json` keeps them bare (they ARE local) → printed
|
||||
prose round-trips byte-stable. Cross-module qualified refs round-
|
||||
trip verbatim through both directions.
|
||||
|
||||
- **Existing workspace integration tests** pass with re-baselined
|
||||
hashes / snapshots. The five obsolete-mechanism removals in ct.2/ct.3
|
||||
each pinned by an existing fixture (e.g. iter-23.1.4's
|
||||
`ordering_match_via_prelude_prints_1`, iter-15a's
|
||||
cross-module Cons pattern fixture).
|
||||
- **Existing workspace integration tests**: pass with bit-identical
|
||||
hashes for intra-module-only fixtures; re-baselined hashes for the
|
||||
small set of cross-module fixtures affected by migration.
|
||||
|
||||
- **Iter 23.3 Task 4 fixture** (`examples/compare_primitives_smoke.ail.json`,
|
||||
recreated post-migration) compiles, runs, prints
|
||||
`"1\n2\n3\n1\n2\n3\n1\n2\n3\n"`.
|
||||
recreated post-migration with qualified `prelude.Ordering`): compiles,
|
||||
runs, prints `"1\n2\n3\n1\n2\n3\n1\n2\n3\n"`.
|
||||
|
||||
- **Bench scripts** (the three regression harnesses) run at milestone
|
||||
close. Hypothesis: no measurable regression — the new schema check
|
||||
is constant-time per load, the removed fallbacks are dead code paths
|
||||
not in the hot path.
|
||||
- **Bench scripts**: run at milestone close. Hypothesis: no measurable
|
||||
regression — validator is constant-time per type ref, removed
|
||||
fallbacks were not in the hot path.
|
||||
|
||||
## Acceptance criteria
|
||||
|
||||
1. `.ail.json` schema validation rejects bare non-primitive
|
||||
`Type::Con` names at `load_workspace`. Every accepted workspace's
|
||||
AST is canonical (qualified everywhere except primitives) by
|
||||
construction — no internal canonicalisation pass.
|
||||
1. `load_workspace` validates every `Type::Con` per the rule above.
|
||||
Bare cross-module refs and bad qualified refs are rejected with
|
||||
clear diagnostics naming the offending module + type + candidates.
|
||||
|
||||
2. Every existing fixture under `examples/` is migrated to canonical
|
||||
form. Code-search for `"name": "Ordering"` (or any other
|
||||
non-primitive bare reference) in `examples/*.ail.json` returns
|
||||
zero matches. The migration is a single bulk commit produced by
|
||||
2. Every fixture under `examples/` that previously relied on
|
||||
imports-fallback for bare cross-module refs is migrated to the
|
||||
qualified form. The migration is a single bulk commit produced by
|
||||
the migration tool, not hand-edited.
|
||||
|
||||
3. The five obsolete mechanisms are deleted (NOT retained as defensive
|
||||
guards):
|
||||
- `Pattern::Ctor` imports-fallback (iter 15a, `crates/ailang-check/src/lib.rs:2486-2521`)
|
||||
3. The four obsolete mechanisms in Components are deleted (NOT
|
||||
retained as defensive guards):
|
||||
- `Pattern::Ctor` imports-fallback (iter 15a, `~lib.rs:2486-2521`)
|
||||
- `Term::Ctor` synth imports-fallback (iter 23.1.3, `~lib.rs:1979-2025`)
|
||||
- `qualify_local_types` helper (iter 15a, `~lib.rs:1848`)
|
||||
- `lookup_ctor_by_type` codegen imports-fallback (iter 23.1.4, `crates/ailang-codegen/src/lib.rs`)
|
||||
- `apply_per_module_ctor_index_overlay` in mono (iter 23.2 fix `84dcc46`, `crates/ailang-check/src/mono.rs`)
|
||||
- `lookup_ctor_by_type` codegen imports-fallback (iter 23.1.4)
|
||||
- `apply_per_module_ctor_index_overlay` in mono (iter 23.2 fix `84dcc46`)
|
||||
|
||||
Code search confirms.
|
||||
|
||||
4. The codegen `lower_workspace_inner` implicit-prelude import_map
|
||||
entry (`be882c4`) STAYS — different problem (fn-name resolution
|
||||
for mono symbols).
|
||||
4. `qualify_local_types` (iter 15a, ~lib.rs:1848) STAYS and is
|
||||
audited for uniform application at all cross-module fn-lookup
|
||||
sites (ct.2). The codegen `lower_workspace_inner` implicit-prelude
|
||||
import_map (`be882c4`) STAYS — different problem (fn names).
|
||||
|
||||
5. `cargo build --workspace && cargo test --workspace` is FULL green
|
||||
at milestone close.
|
||||
@@ -239,82 +237,86 @@ failures, never on bare-vs-qualified mismatches.
|
||||
without any further canonical-form-related compiler changes.
|
||||
|
||||
7. DESIGN.md amended:
|
||||
- Decision 2 (content-addressed defs) names the rule explicitly:
|
||||
"AST Type::Con names are fully qualified (`module.Type`) except
|
||||
for primitives. Each AST object is context-free — it can be
|
||||
reasoned about and hashed without external module information."
|
||||
- The "hashes stay bit-identical" promises across DESIGN.md (lines
|
||||
1067, 1121, 1137, 1302, 1335, 1709, 1719, 2229) are reviewed and
|
||||
re-contextualised as promises within the new canonical-form
|
||||
version.
|
||||
- Decision 2 (content-addressed defs) gains the canonical-form
|
||||
rule: "Within a `.ail.json`, bare `Type::Con` names are
|
||||
local to the file's module; cross-module references MUST be
|
||||
qualified. Primitives are bare and have no module."
|
||||
- The "hashes stay bit-identical" promises across DESIGN.md are
|
||||
reviewed. Most intra-module fixtures' hashes do not shift under
|
||||
this migration; the few cross-module fixtures that DO shift are
|
||||
named explicitly in the JOURNAL entry.
|
||||
|
||||
8. JOURNAL entry documents the migration as a versioned canonical-form
|
||||
transition: one-time hash shift, deliberate, ratified by this
|
||||
spec's acceptance.
|
||||
8. JOURNAL entry documents the migration as a targeted canonical-form
|
||||
tightening: most fixtures unchanged, a small set of cross-module
|
||||
fixtures rewritten and re-baselined.
|
||||
|
||||
9. The three bench scripts exit 0 or any non-zero is ratified per
|
||||
audit-skill rules.
|
||||
|
||||
## Out of scope
|
||||
|
||||
- **Term-level names** (function names, var names, ctor names in
|
||||
patterns): unchanged. Bare ctor names in `Pattern::Ctor` continue
|
||||
to work — they're resolved against the (now-qualified) scrutinee's
|
||||
type, which gives unambiguous resolution.
|
||||
- **Term-level names** (function names, var names, pattern ctor names
|
||||
like `LT`): unchanged. Pattern ctor names resolve against the
|
||||
scrutinee's type's TypeDef (which is now unambiguous: bare =
|
||||
local-to-this-file, qualified = explicit cross-module). No
|
||||
imports-fallback needed in the pattern path.
|
||||
|
||||
- **Class names** (`class Eq`, `instance Eq Int`, `class Ord extends Eq`):
|
||||
same conceptual problem as type names but separate namespace. Today
|
||||
class names are bare in JSON; the `MethodNameCollision` check
|
||||
enforces global uniqueness across all classes. Whether class names
|
||||
should also be qualified is a related question deferred to a future
|
||||
spec — out of scope here. This spec touches Type::Con only.
|
||||
- **Class names** (`class Eq`, `instance Eq Int`, `class Ord extends Eq`,
|
||||
`Constraint.class`): same conceptual shape as Type::Con but separate
|
||||
namespace, separate uniqueness check (`MethodNameCollision`). Whether
|
||||
class names should also follow the same module-scoping rule is a
|
||||
separate spec question deferred to a future milestone. This spec
|
||||
touches Type::Con only.
|
||||
|
||||
- **Surface notation in `.ailx`**: bare local names continue to be
|
||||
the LLM-author convention. The prose parser qualifies on read; the
|
||||
prose printer trims on write. Round-trip is semantics-preserving.
|
||||
the LLM-author convention. Prose parser/printer bridge — no
|
||||
surface schema changes.
|
||||
|
||||
- **Schema version bump**: the `.ail.json` schema field stays
|
||||
`"ailang/v0"`. The canonical form change is a strictening of the
|
||||
existing schema (rejecting previously-tolerated input), not a new
|
||||
- **`.ail.json` schema version**: stays `"ailang/v0"`. The new
|
||||
validator rule is a tightening of acceptable inputs, not a new
|
||||
schema version. Migration is one-time and tool-assisted.
|
||||
|
||||
- **External clients**: AILang is pre-1.0 and not yet consumed by
|
||||
anything outside the repo.
|
||||
- **External clients**: AILang is pre-1.0.
|
||||
|
||||
## Migration impact (one-time)
|
||||
## Migration impact (one-time, bounded)
|
||||
|
||||
The migration is bounded and deliberate:
|
||||
The migration is much smaller than a "qualify everything" rewrite:
|
||||
|
||||
- **Every `.ail.json` fixture under `examples/` is rewritten** by the
|
||||
migration tool. The diff is mechanical — each bare non-primitive
|
||||
`Type::Con.name` gains its qualifier; every other byte stays the
|
||||
same. Commit as a single bulk migration in ct.1.
|
||||
- **Intra-module-only fixtures stay bit-identical.** This is the
|
||||
majority of `examples/`. Hash-stable, no rewrite.
|
||||
|
||||
- **Every canonical-JSON hash shifts** as a consequence. Re-baselined
|
||||
once in ct.4. Hash-pinned regression tests continue to pin form
|
||||
stability under future schema additions, at the new hashes.
|
||||
- **Cross-module fixtures** that today carry bare references (relying
|
||||
on iter 23.1.3's imports-fallback or similar) are rewritten: each
|
||||
bare cross-module Type::Con gains its qualifier. Bytes diff is
|
||||
surgical.
|
||||
|
||||
- **IR snapshot fixtures** under `crates/ail/tests/snapshots/` may
|
||||
shift if the IR text depends on type names (it does, via mangling).
|
||||
Re-baselined at ct.4.
|
||||
- **Hashes shift only for the migrated cross-module fixtures.**
|
||||
Hash-pinned regression tests touching those fixtures re-baselined
|
||||
in ct.4.
|
||||
|
||||
- **Prose snapshots** (`.prose.txt` round-trip baselines): if the
|
||||
prose printer correctly trims bare-local on output, the prose
|
||||
side of the round-trip stays byte-stable across the migration —
|
||||
only the `.ail.json` side changes.
|
||||
- **IR snapshot fixtures** under `crates/ail/tests/snapshots/`: if
|
||||
the program in question references prelude types in mangled
|
||||
symbols, those snapshots may shift. Bit-stable otherwise.
|
||||
|
||||
This is a versioned canonical-form transition. The previous form
|
||||
(bare-tolerant) was structurally incomplete; the new form
|
||||
(qualified-mandatory in JSON) is complete and the canonical-form
|
||||
invariant holds by construction.
|
||||
- **Prose snapshots** (`.prose.txt`): the prose surface didn't carry
|
||||
inline qualifiers for local refs to begin with. They round-trip
|
||||
bit-stable across the migration if the printer correctly trims
|
||||
qualifiers matching the current module name.
|
||||
|
||||
## Note on canonical-hash stability promises in DESIGN.md
|
||||
This is a targeted canonical-form tightening, not a workspace-wide
|
||||
rewrite. The previous form (bare-cross-module-tolerated via
|
||||
imports-fallback) becomes an explicit schema violation; the new form
|
||||
(bare = local, qualified = cross-module) is unambiguous and
|
||||
verifiable at load time.
|
||||
|
||||
The "hashes stay bit-identical when feature X is added" promises
|
||||
distributed across DESIGN.md are not promises about a specific
|
||||
bit-sequence for all time. They are promises within a canonical-form
|
||||
version: feature additions that preserve the canonical-form rule
|
||||
don't change hashes. This spec changes the canonical-form rule
|
||||
itself, so the promises are re-interpreted as: within the new
|
||||
canonical form, future feature additions continue to preserve
|
||||
hashes. Ct.4's DESIGN.md amendment makes this explicit.
|
||||
## Note on the previous spec revision
|
||||
|
||||
A first draft of this spec (committed at `b6a8404`) proposed
|
||||
qualifying *every* non-primitive Type::Con inline, even within
|
||||
intra-module references. The user's clarification on 2026-05-11
|
||||
(noting that `bench_mono_dispatch.ail.json` already has the module
|
||||
qualifier at the file top and references within are bare) led to
|
||||
this revised approach. The file IS the module-context unit; bare
|
||||
references within make sense because the surrounding `"name"` field
|
||||
disambiguates them. The qualification requirement applies only at
|
||||
the cross-module boundary, where ambiguity actually exists.
|
||||
|
||||
Reference in New Issue
Block a user