fieldtest: canonical-type-names — 5 examples, 9 findings

This commit is contained in:
2026-05-11 12:14:15 +02:00
parent c680c91f5b
commit 45dca692dd
11 changed files with 451 additions and 0 deletions
@@ -0,0 +1,289 @@
# Fieldtest — canonical-type-names — 2026-05-11
**Status:** Draft — awaiting orchestrator triage
**Author:** ailang-fieldtester (dispatched by skills/fieldtest)
## Scope
The canonical-type-names milestone made cross-module type references
load-time validated. Within a `.ail.json` module, bare `Type::Con`
names refer to the file's own definitions; cross-module references
MUST be qualified `<owning_module>.<TypeName>`; primitives stay bare.
Three new workspace-load diagnostics were added: `bare-cross-module-type-ref`,
`bad-cross-module-type-ref`, `qualified-class-name`. The prose Form-B
printer trims the owning-module qualifier when printing a definition
file's own types and keeps cross-module qualifiers verbatim.
## Examples
Five fixtures were authored, all `.ailx` first, parsed via
`ail parse`, then checked via `ail check`. All live at
`examples/` top-level rather than `examples/fieldtest/` because they
need cross-module imports (`prelude`, `std_maybe`) which the
workspace loader only finds beside the entry module — see Findings
for that friction.
### `examples/ct_1_ordering_signum.ailx` — sign-profile of an Int list
Reduces a list of Int to a sequence of `-1` / `0` / `+1` by pattern-
matching the result of `compare(n, 0)` against `prelude.Ordering`'s
three constructors. Drives recursion over a local `IntList` ADT.
- **Why it fits**: canonical happy-path exercise of cross-module
type reference (`prelude.Ordering`) AND local-type reference
(`IntList`) in one file.
- **Outcome**: parse OK; check OK ("ok (13 symbols across 2 modules)");
build OK; runs cleanly; stdout matches expected
`-1 / 0 / 1 / -1 / 1`.
- **Form-A round-trip**: `render → parse → JSON` is byte-identical.
### `examples/ct_2_bare_cross_module.ailx` — bare `Maybe` instead of `std_maybe.Maybe`
A consumer of `std_maybe` writes `(con Maybe (con Int))` in a fn
signature instead of `(con std_maybe.Maybe (con Int))`. This is what
an LLM author who forgot the qualification rule would naturally
produce.
- **Why it fits**: probes axis 1 (BareCrossModuleTypeRef).
- **Outcome**: parse OK; check fails with:
> Error: module `ct_2_bare_cross_module` contains bare type name
> `Maybe` that does not resolve to a local type. AILang's
> `.ail.json` requires cross-module type references to be
> qualified. Candidates from imports: `["std_maybe.Maybe"]`. Run
> `ail migrate-canonical-types` to fix legacy fixtures.
Diagnostic code: `bare-cross-module-type-ref`. JSON ctx includes
`module`, `name`, `candidates`. Exit code 1.
### `examples/ct_3_bad_qualified.ailx` — qualified `mystery.Widget` to unknown module
A consumer references `(con mystery.Widget)` where the module
`mystery` is not in the workspace.
- **Why it fits**: probes axis 2 (BadCrossModuleTypeRef — unknown
owner).
- **Outcome**: parse OK; check fails with:
> Error: module `ct_3_bad_qualified` references qualified type
> `mystery.Widget` but the owner module is not known or does not
> declare a type by that name
Diagnostic code: `bad-cross-module-type-ref`. Exit code 1.
### `examples/ct_3b_bad_qualified_known_module.ailx` — qualified `std_maybe.Widget` to known module
A consumer references `(con std_maybe.Widget)`. The module `std_maybe`
exists and is imported, but it has no `Widget` type def.
- **Why it fits**: probes axis 2 (BadCrossModuleTypeRef — known
owner, unknown type). This is the second branch of the
diagnostic's `or` clause.
- **Outcome**: parse OK; check fails with the identical-shape
diagnostic as ct_3, just with `std_maybe.Widget` substituted.
See finding **friction: BadCrossModuleTypeRef does not
distinguish unknown-owner from unknown-type-in-known-owner**.
### `examples/ct_4_qualified_class.ailx` — qualified `prelude.Eq` in an instance
A consumer defines a local `Box` ADT and writes
`(instance (class prelude.Eq) (type (con Box)) ...)` instead of
bare `(class Eq)`. Per the milestone's Out-of-scope note, class
names remain bare; a qualified form is a schema violation.
- **Why it fits**: probes axis 3 (QualifiedClassName).
- **Outcome**: parse OK; check fails with:
> Error: module `ct_4_qualified_class` contains qualified class
> name `prelude.Eq` in field `InstanceDef.class`. Class names are
> not module-qualified in this milestone; keep the bare form.
Diagnostic code: `qualified-class-name`. JSON ctx includes
`module`, `name`, `field`. Exit code 1.
## Findings
### [working] BareCrossModuleTypeRef diagnostic carries the full fix recipe
Example: `ct_2`. The diagnostic names the module, the offending bare
name, the qualified candidate(s) from imports, the migration tool
that automates the rewrite. An LLM author can act on this without
re-reading the spec. The `ctx` field on the JSON diagnostic is
structured (`module`, `name`, `candidates`) — machine-actionable.
Recommended downstream action: **carry-on**.
### [working] BadCrossModuleTypeRef catches both Surface and JSON authoring of qualified type refs
Examples: `ct_3`, `ct_3b`. The Surface syntax `(con mystery.Widget)`
DOES parse cleanly, which is the right behavior — Surface should
be the canonical authoring path, and authoring a deliberately wrong
qualifier should not be rejected at the lexer level. The validator
catches it later at workspace load. The diagnostic is clear about
what's wrong.
Recommended downstream action: **carry-on**.
### [working] QualifiedClassName fires on `(instance (class prelude.Eq) ...)`
Example: `ct_4`. The Surface form for an instance with a qualified
class is parseable (`(class prelude.Eq)` produces JSON
`"class": "prelude.Eq"`). The validator rejects it with a clear
"keep the bare form" recommendation and names the offending field
(`InstanceDef.class`).
Recommended downstream action: **carry-on**.
### [working] Form-A round-trip is byte-stable across cross-module refs
Example: `ct_1`. `ail render → ail parse → JSON` reproduces the
input file byte-for-byte. The prose Form-B printer trims the
owning-module qualifier for local types correctly (`IntList`
renders bare in `ct_1`'s prose, since `IntList` is a local def).
Cross-module ctors elide the type-name qualifier in prose
(`std_either_demo.prose` shows `Right(42)` not
`std_either.Either::Right(42)`), but the canonical JSON underneath
keeps the qualified form. This matches the spec's "prose drops
qualifiers for surface readability while JSON stays canonical"
intent.
Recommended downstream action: **carry-on**.
### [friction] BadCrossModuleTypeRef does not distinguish unknown-owner from unknown-type-in-known-owner
Examples: `ct_3` (unknown owner `mystery`) and `ct_3b` (known owner
`std_maybe`, unknown type `Widget`) produce *identical-shape*
diagnostics:
```
references qualified type `<owner>.<name>` but the owner module is
not known or does not declare a type by that name
```
The two cases suggest different fixes:
- Unknown owner: add an `(import <owner>)` clause, or fix the
typo in the owner prefix.
- Known owner, unknown type: fix the typo in the type name, or
check what types the owner actually exports.
The merged diagnostic leaves the LLM author guessing. When the owner
IS known, the diagnostic could list available type defs in the
owner's module the way `bare-cross-module-type-ref` lists candidates
from imports.
Recommended downstream action: **plan** (tidy iteration to split the
two cases and, in the known-owner branch, list available type names
as candidates).
### [friction] Workspace search does not look beyond the entry-module's directory
Encountered while placing fixtures. The agent spec convention says
fieldtest fixtures go under `examples/fieldtest/`. But every fixture
that imports `prelude` or `std_maybe` had to be moved to
`examples/` top-level because the workspace loader only finds
sibling `.ail.json` files in the same directory as the entry module.
There is no `--workspace-root` flag on `ail check` / `ail build`,
and the diagnostic
> module `prelude` not found (expected at <entry-dir>/prelude.ail.json)
names the precise lookup path but offers no remediation. The result
is that subdirectories are effectively forbidden for any consumer
of stdlib or prelude — an LLM author hoping to organise example
files into subdirectories has to either (a) keep everything flat or
(b) duplicate stdlib `.ail.json` files into each subdir.
This is orthogonal to canonical-type-names per se (it predates the
milestone) but it surfaced naturally during the field test and
shapes how the fixtures could be placed.
Recommended downstream action: **plan** (a small iteration to add
either a `--workspace-root` flag, or upward-search from the entry
module's directory; or **ratify** the current behavior in DESIGN.md
if the flat-workspace assumption is intentional).
### [friction] `ail check` on a `.ailx` source fails with a misleading JSON-parse error
Encountered immediately on the first invocation: `ail check
foo.ailx` produced:
> Error: schema/parse error in foo.ailx: json: expected value at
> line 1 column 1
The diagnostic blames `foo.ailx` for not being valid JSON, when the
real situation is that `ail check` only accepts `.ail.json`. The
LLM author has to learn (from `ail --help`) that `ail parse` does
the surface→JSON step, and then chain
`ail parse foo.ailx > foo.ail.json && ail check foo.ail.json`.
The clean fix would be either (a) `ail check` recognises `.ailx`
and parses internally, or (b) the diagnostic special-cases the
`.ailx` extension and points to `ail parse`. Today, neither holds,
and an LLM author's natural first command produces a misleading
diagnostic.
This is orthogonal to canonical-type-names per se. Orthogonal but
load-bearing — every fieldtest invocation hits this.
Recommended downstream action: **plan** (a small iteration to teach
`ail check` and `ail build` to accept `.ailx`, or to detect the
extension and emit a "did you mean `ail parse foo.ailx`?" hint).
### [friction] `(import prelude)` is rejected as reserved
Encountered while writing `ct_1`. The LLM-natural reach when writing
a cross-module program that consumes prelude is to write
`(import prelude)` in the module header. This is rejected:
> Error: module name `prelude` is reserved (auto-injected by the
> loader)
The diagnostic is clear and short. But the fact that prelude is
auto-injected and must NOT be explicitly imported is not documented
in any `.ailx` corpus example I can read (no example uses
`(import prelude)`, but also no comment explains why). DESIGN.md
mentions prelude as the autodiscovered module but does not say
"do not write `(import prelude)`". An author has to discover this
by trying and being told.
This is mild — it's a one-trip teaching diagnostic, and the
message is plain. But worth recording.
Recommended downstream action: **carry-on** (the diagnostic itself
is clear; the deeper fix is a DESIGN.md or per-module-comment hint,
not a code change).
### [spec_gap] No `.ailx` counterpart for `compare_primitives_smoke.ail.json`
Observation: the canonical happy-path example shipped with this
milestone (`examples/compare_primitives_smoke.ail.json`) exists
only as JSON. There is no `.ailx` counterpart. Surface IS the
LLM-author surface per Decision 6, so a milestone whose "happy
path" demonstrates cross-module type usage SHOULD have a `.ailx`
exhibit — otherwise the LLM author has to read raw JSON to
understand the pattern. My `ct_1_ordering_signum.ailx` partly fills
this gap (it exercises prelude.Ordering pattern-matching from a
Surface program) but it is not bit-identical to
`compare_primitives_smoke`.
Recommended downstream action: **plan** (tidy iteration: author
`examples/compare_primitives_smoke.ailx` to round-trip into the
existing JSON, OR retire `compare_primitives_smoke.ail.json` in
favour of `ct_1_ordering_signum.{ailx,ail.json}` as the new
canonical happy-path exhibit).
## Recommendation summary
| Finding | Class | Action |
|---|---|---|
| BareCrossModuleTypeRef carries full fix recipe | working | carry-on |
| BadCrossModuleTypeRef catches Surface-authored qualified refs | working | carry-on |
| QualifiedClassName fires on `(class prelude.Eq)` | working | carry-on |
| Form-A round-trip byte-stable | working | carry-on |
| BadCrossModuleTypeRef merges two distinguishable cases | friction | plan |
| Workspace search confined to entry dir | friction | plan or ratify |
| `ail check` on `.ailx` produces misleading JSON-parse error | friction | plan |
| `(import prelude)` rejected — diagnostic clear, doc-gap mild | friction | carry-on |
| No `.ailx` counterpart for `compare_primitives_smoke.ail.json` | spec_gap | plan |
The four milestone-scoped findings are all **working**. The three
**friction** items orthogonal to canonical-type-names are
pre-existing UX gaps surfaced by the field test. The two
canonical-type-names-internal friction items (BadCrossModuleTypeRef
case-merging; missing `.ailx` exhibit) are recommendable tidy
iterations.
+1
View File
@@ -0,0 +1 @@
{"defs":[{"ctors":[{"fields":[],"name":"INil"},{"fields":[{"k":"con","name":"Int"},{"k":"con","name":"IntList"}],"name":"ICons"}],"doc":"Local Int-list ADT; nothing fancy.","kind":"type","name":"IntList"},{"body":{"arms":[{"body":{"args":[{"lit":{"kind":"int","value":0},"t":"lit"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"-","t":"var"},"t":"app"},"pat":{"ctor":"LT","fields":[],"p":"ctor"}},{"body":{"lit":{"kind":"int","value":0},"t":"lit"},"pat":{"ctor":"EQ","fields":[],"p":"ctor"}},{"body":{"lit":{"kind":"int","value":1},"t":"lit"},"pat":{"ctor":"GT","fields":[],"p":"ctor"}}],"scrutinee":{"args":[{"name":"n","t":"var"},{"lit":{"kind":"int","value":0},"t":"lit"}],"fn":{"name":"compare","t":"var"},"t":"app"},"t":"match"},"doc":"Map an Int to -1/0/+1 via prelude.compare. Returns the sign as an Int.","kind":"fn","name":"signum","params":["n"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"Int"}],"ret":{"k":"con","name":"Int"}}},{"body":{"arms":[{"body":{"lit":{"kind":"unit"},"t":"lit"},"pat":{"ctor":"INil","fields":[],"p":"ctor"}},{"body":{"lhs":{"args":[{"args":[{"name":"h","t":"var"}],"fn":{"name":"signum","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"rhs":{"args":[{"name":"t","t":"var"}],"fn":{"name":"drain","t":"var"},"t":"app","tail":true},"t":"seq"},"pat":{"ctor":"ICons","fields":[{"name":"h","p":"var"},{"name":"t","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"xs","t":"var"},"t":"match"},"doc":"Print signum of every element of xs, in list order.","kind":"fn","name":"drain","params":["xs"],"type":{"effects":["IO"],"k":"fn","params":[{"k":"con","name":"IntList"}],"ret":{"k":"con","name":"Unit"}}},{"body":{"args":[{"args":[{"args":[{"lit":{"kind":"int","value":0},"t":"lit"},{"lit":{"kind":"int","value":3},"t":"lit"}],"fn":{"name":"-","t":"var"},"t":"app"},{"args":[{"lit":{"kind":"int","value":0},"t":"lit"},{"args":[{"lit":{"kind":"int","value":7},"t":"lit"},{"args":[{"args":[{"lit":{"kind":"int","value":0},"t":"lit"},{"lit":{"kind":"int","value":1},"t":"lit"}],"fn":{"name":"-","t":"var"},"t":"app"},{"args":[{"lit":{"kind":"int","value":2},"t":"lit"},{"args":[],"ctor":"INil","t":"ctor","type":"IntList"}],"ctor":"ICons","t":"ctor","type":"IntList"}],"ctor":"ICons","t":"ctor","type":"IntList"}],"ctor":"ICons","t":"ctor","type":"IntList"}],"ctor":"ICons","t":"ctor","type":"IntList"}],"ctor":"ICons","t":"ctor","type":"IntList"}],"fn":{"name":"drain","t":"var"},"t":"app"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"ct_1_ordering_signum","schema":"ailang/v0"}
+55
View File
@@ -0,0 +1,55 @@
; Fieldtest — canonical-type-names, axis 4: canonical happy path.
;
; Reduce a list of Int to a 'sign profile': for each element, print
; -1, 0, or +1 according to its compare-to-zero result. Exercises:
; - cross-module reference to prelude.Ordering at use site
; - pattern match against prelude.Ordering's three constructors LT/EQ/GT
; - returning a freshly chosen Int from each arm
; - simple recursion over a local List ADT
;
; Expected stdout (one per line):
; -1 ; signum -3
; 0 ; signum 0
; 1 ; signum 7
; -1 ; signum -1
; 1 ; signum 2
(module ct_1_ordering_signum
(data IntList
(doc "Local Int-list ADT; nothing fancy.")
(ctor INil)
(ctor ICons (con Int) (con IntList)))
(fn signum
(doc "Map an Int to -1/0/+1 via prelude.compare. Returns the sign as an Int.")
(type (fn-type (params (con Int)) (ret (con Int))))
(params n)
(body
(match (app compare n 0)
(case (pat-ctor LT) (app - 0 1))
(case (pat-ctor EQ) 0)
(case (pat-ctor GT) 1))))
(fn drain
(doc "Print signum of every element of xs, in list order.")
(type (fn-type (params (con IntList)) (ret (con Unit)) (effects IO)))
(params xs)
(body
(match xs
(case (pat-ctor INil) (lit-unit))
(case (pat-ctor ICons h t)
(seq (do io/print_int (app signum h))
(tail-app drain t))))))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(app drain
(term-ctor IntList ICons (app - 0 3)
(term-ctor IntList ICons 0
(term-ctor IntList ICons 7
(term-ctor IntList ICons (app - 0 1)
(term-ctor IntList ICons 2
(term-ctor IntList INil))))))))))
+1
View File
@@ -0,0 +1 @@
{"defs":[{"body":{"name":"m","t":"var"},"doc":"Identity over Maybe<Int>; signature uses bare `Maybe` instead of `std_maybe.Maybe`.","kind":"fn","name":"classify","params":["m"],"type":{"effects":[],"k":"fn","params":[{"args":[{"k":"con","name":"Int"}],"k":"con","name":"Maybe"}],"ret":{"args":[{"k":"con","name":"Int"}],"k":"con","name":"Maybe"}}},{"body":{"args":[{"args":[{"lit":{"kind":"int","value":99},"t":"lit"},{"args":[{"args":[{"lit":{"kind":"int","value":7},"t":"lit"}],"ctor":"Just","t":"ctor","type":"std_maybe.Maybe"}],"fn":{"name":"classify","t":"var"},"t":"app"}],"fn":{"name":"std_maybe.from_maybe","t":"var"},"t":"app"}],"op":"io/print_int","t":"do"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[{"module":"std_maybe"}],"name":"ct_2_bare_cross_module","schema":"ailang/v0"}
+31
View File
@@ -0,0 +1,31 @@
; Fieldtest — canonical-type-names, axis 1: BareCrossModuleTypeRef.
;
; A consumer of std_maybe deliberately writes a BARE `Maybe` in its
; fn signature instead of `std_maybe.Maybe`. Per ct.1's load-time
; validator, the canonical form requires cross-module type refs to
; be qualified. Expected: workspace-load error
; `BareCrossModuleTypeRef` naming the type and listing the qualified
; form as a candidate.
;
; This is what an LLM author would write if they forgot the
; cross-module-qualification rule — the rule fires here at load
; time, not deep inside the typechecker.
(module ct_2_bare_cross_module
(import std_maybe)
(fn classify
(doc "Identity over Maybe<Int>; signature uses bare `Maybe` instead of `std_maybe.Maybe`.")
(type
(fn-type
(params (con Maybe (con Int)))
(ret (con Maybe (con Int)))))
(params m)
(body m))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body
(do io/print_int (app std_maybe.from_maybe 99 (app classify (term-ctor std_maybe.Maybe Just 7)))))))
+1
View File
@@ -0,0 +1 @@
{"defs":[{"body":{"name":"w","t":"var"},"doc":"Identity at mystery.Widget — but `mystery` is not a workspace module.","kind":"fn","name":"passthrough","params":["w"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"mystery.Widget"}],"ret":{"k":"con","name":"mystery.Widget"}}},{"body":{"args":[{"lit":{"kind":"str","value":"unreachable"},"t":"lit"}],"op":"io/print_str","t":"do"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[],"name":"ct_3_bad_qualified","schema":"ailang/v0"}
+22
View File
@@ -0,0 +1,22 @@
; Fieldtest — canonical-type-names, axis 2: BadCrossModuleTypeRef.
;
; A consumer references a fully qualified type `mystery.Widget`
; where the module `mystery` does not exist in the workspace.
; Per ct.1's load-time validator, this should be rejected with
; `BadCrossModuleTypeRef` naming the unknown owner.
(module ct_3_bad_qualified
(fn passthrough
(doc "Identity at mystery.Widget — but `mystery` is not a workspace module.")
(type
(fn-type
(params (con mystery.Widget))
(ret (con mystery.Widget))))
(params w)
(body w))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_str "unreachable"))))
@@ -0,0 +1 @@
{"defs":[{"body":{"name":"w","t":"var"},"doc":"Identity at std_maybe.Widget — `std_maybe` is real, but `Widget` is not in it.","kind":"fn","name":"passthrough","params":["w"],"type":{"effects":[],"k":"fn","params":[{"k":"con","name":"std_maybe.Widget"}],"ret":{"k":"con","name":"std_maybe.Widget"}}},{"body":{"args":[{"lit":{"kind":"str","value":"unreachable"},"t":"lit"}],"op":"io/print_str","t":"do"},"kind":"fn","name":"main","params":[],"type":{"effects":["IO"],"k":"fn","params":[],"ret":{"k":"con","name":"Unit"}}}],"imports":[{"module":"std_maybe"}],"name":"ct_3b_bad_qualified_known_module","schema":"ailang/v0"}
@@ -0,0 +1,23 @@
; Fieldtest — canonical-type-names, axis 2 (variant b): BadCrossModuleTypeRef
; — known module, unknown type. Author qualifies as `std_maybe.Widget`
; where `std_maybe` IS a workspace module but does not declare a
; `Widget` type. Per ct.1's load-time validator, this should be
; rejected with `BadCrossModuleTypeRef`.
(module ct_3b_bad_qualified_known_module
(import std_maybe)
(fn passthrough
(doc "Identity at std_maybe.Widget — `std_maybe` is real, but `Widget` is not in it.")
(type
(fn-type
(params (con std_maybe.Widget))
(ret (con std_maybe.Widget))))
(params w)
(body w))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_str "unreachable"))))
+1
View File
@@ -0,0 +1 @@
{"defs":[{"ctors":[{"fields":[{"k":"con","name":"Int"}],"name":"MkBox"}],"doc":"A trivial Int wrapper.","kind":"type","name":"Box"},{"class":"prelude.Eq","kind":"instance","methods":[{"body":{"body":{"arms":[{"body":{"arms":[{"body":{"args":[{"name":"a","t":"var"},{"name":"b","t":"var"}],"fn":{"name":"==","t":"var"},"t":"app"},"pat":{"ctor":"MkBox","fields":[{"name":"b","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"y","t":"var"},"t":"match"},"pat":{"ctor":"MkBox","fields":[{"name":"a","p":"var"}],"p":"ctor"}}],"scrutinee":{"name":"x","t":"var"},"t":"match"},"effects":[],"paramTypes":[{"k":"con","name":"Box"},{"k":"con","name":"Box"}],"params":["x","y"],"retType":{"k":"con","name":"Bool"},"t":"lam"},"name":"eq"}],"type":{"k":"con","name":"Box"}}],"imports":[],"name":"ct_4_qualified_class","schema":"ailang/v0"}
+26
View File
@@ -0,0 +1,26 @@
; Fieldtest — canonical-type-names, axis 3: QualifiedClassName.
;
; A consumer defines a local Box type and writes an instance for
; `prelude.Eq` instead of bare `Eq`. Per ct.1's load-time validator,
; class names are NOT module-scoped (they remain workspace-flat,
; with MethodNameCollision enforcing uniqueness), so a qualified
; class name in an InstanceDef.class field is a schema violation.
(module ct_4_qualified_class
(data Box
(doc "A trivial Int wrapper.")
(ctor MkBox (con Int)))
(instance
(class prelude.Eq)
(type (con Box))
(method eq
(body
(lam (params (typed x (con Box)) (typed y (con Box))) (ret (con Bool))
(body
(match x
(case (pat-ctor MkBox a)
(match y
(case (pat-ctor MkBox b)
(app == a b)))))))))))