diff --git a/docs/specs/2026-05-11-fieldtest-canonical-type-names.md b/docs/specs/2026-05-11-fieldtest-canonical-type-names.md new file mode 100644 index 0000000..9c323a1 --- /dev/null +++ b/docs/specs/2026-05-11-fieldtest-canonical-type-names.md @@ -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 `.`; 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 `.` 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 )` 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 /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. diff --git a/examples/ct_1_ordering_signum.ail.json b/examples/ct_1_ordering_signum.ail.json new file mode 100644 index 0000000..b00f554 --- /dev/null +++ b/examples/ct_1_ordering_signum.ail.json @@ -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"} diff --git a/examples/ct_1_ordering_signum.ailx b/examples/ct_1_ordering_signum.ailx new file mode 100644 index 0000000..0c5f398 --- /dev/null +++ b/examples/ct_1_ordering_signum.ailx @@ -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)))))))))) diff --git a/examples/ct_2_bare_cross_module.ail.json b/examples/ct_2_bare_cross_module.ail.json new file mode 100644 index 0000000..ca711e6 --- /dev/null +++ b/examples/ct_2_bare_cross_module.ail.json @@ -0,0 +1 @@ +{"defs":[{"body":{"name":"m","t":"var"},"doc":"Identity over Maybe; 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"} diff --git a/examples/ct_2_bare_cross_module.ailx b/examples/ct_2_bare_cross_module.ailx new file mode 100644 index 0000000..a43f5dc --- /dev/null +++ b/examples/ct_2_bare_cross_module.ailx @@ -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; 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))))))) diff --git a/examples/ct_3_bad_qualified.ail.json b/examples/ct_3_bad_qualified.ail.json new file mode 100644 index 0000000..b037616 --- /dev/null +++ b/examples/ct_3_bad_qualified.ail.json @@ -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"} diff --git a/examples/ct_3_bad_qualified.ailx b/examples/ct_3_bad_qualified.ailx new file mode 100644 index 0000000..27084ce --- /dev/null +++ b/examples/ct_3_bad_qualified.ailx @@ -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")))) diff --git a/examples/ct_3b_bad_qualified_known_module.ail.json b/examples/ct_3b_bad_qualified_known_module.ail.json new file mode 100644 index 0000000..c53cfd0 --- /dev/null +++ b/examples/ct_3b_bad_qualified_known_module.ail.json @@ -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"} diff --git a/examples/ct_3b_bad_qualified_known_module.ailx b/examples/ct_3b_bad_qualified_known_module.ailx new file mode 100644 index 0000000..c31574c --- /dev/null +++ b/examples/ct_3b_bad_qualified_known_module.ailx @@ -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")))) diff --git a/examples/ct_4_qualified_class.ail.json b/examples/ct_4_qualified_class.ail.json new file mode 100644 index 0000000..f9f4872 --- /dev/null +++ b/examples/ct_4_qualified_class.ail.json @@ -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"} diff --git a/examples/ct_4_qualified_class.ailx b/examples/ct_4_qualified_class.ailx new file mode 100644 index 0000000..3df2536 --- /dev/null +++ b/examples/ct_4_qualified_class.ailx @@ -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)))))))))))