fieldtest: canonical-type-names — 5 examples, 9 findings
This commit is contained in:
@@ -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.
|
||||||
@@ -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"}
|
||||||
@@ -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))))))))))
|
||||||
@@ -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"}
|
||||||
@@ -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)))))))
|
||||||
@@ -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"}
|
||||||
@@ -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"))))
|
||||||
@@ -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"}
|
||||||
@@ -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)))))))))))
|
||||||
Reference in New Issue
Block a user