A union is not a missing struct either

layout searched only Tast.structs, so a declared union came back as "no struct
is named X" — which reads as "that type does not exist" about a type the
checker knows. Refused by kind beside the enum, and both refusals now have a
test: a new enum and a new union, evaluated into the session.
This commit is contained in:
Joseph Ferano 2026-09-12 10:42:50 +07:00
parent 404b8559c5
commit 41025fc0ac
3 changed files with 50 additions and 4 deletions

View File

@ -1042,7 +1042,9 @@ suffix would reintroduce the ambiguity the rule exists to remove, and a rule wit
rely on. The refusal carries `:candidates`, so a person is one copy-paste from the answer and a client has its
completion list — the same shape as `package_of` refusing a directory imported under two aliases rather than picking
one. An enum is refused by *kind* (`X is an enum, not a struct`): its members are erased to `i32` before `Tast.program`
exists, which is the same fact that makes a `defenum` unreloadable.
exists, which is the same fact that makes a `defenum` unreloadable. A union is refused the same way and for its own
reason — it is declared, and union *values* are milestone 6. Both are `Types.Named` at a use site, so falling through
to "no struct is named X" would say a type does not exist about one that plainly does.
**`render.ml` is not reused, and that is not a second walk.** It walks a *value* and emits the code that prints it;
this describes a *type* and emits text. What is shared is the spelling: field types go through `Types.to_string`, which

View File

@ -529,12 +529,18 @@ let layout t ~ty =
Wire.quote (Types.to_string f.Tast.fty) ])
s.Tast.fields) ]
| None ->
(* An enum is a type the checker knows and this op cannot describe: its
(* Two types the checker knows and this op cannot describe. An enum's
members are erased to i32 before [Tast.program] exists, which is the
same fact that makes a defenum unreloadable. Saying which kind it is
beats "no such type" for a name that plainly exists. *)
same fact that makes a defenum unreloadable; a union is declared and
has no values yet. Either way, saying which kind it is beats "no such
type" for a name that plainly exists. *)
if Hashtbl.mem t.session.Session.env.Check.enums ty then
error (ty ^ " is an enum, not a struct; its members are erased to i32")
else if
List.exists (fun (u : Tast.union) -> String.equal u.Tast.uname ty)
t.session.Session.program.Tast.unions
then
error (ty ^ " is a union, not a struct; union values are milestone 6")
else
let suffix = "/" ^ ty in
let candidates =

View File

@ -174,6 +174,44 @@ let () =
let r = request c "(:op \"layout\" :type \"Nonesuch\")" in
if status r <> "error" then fail "a type that does not exist got a layout";
(* A name that plainly exists and is not a struct is refused by *kind*.
Both of these are types the checker knows and this op cannot
describe, and "no struct is named X" would read as "X does not
exist". *)
let refusal r =
Option.value ~default:(status r) (Wire.string_field r "message")
in
let contains hay needle =
let n = String.length needle in
let rec go i =
i + n <= String.length hay
&& (String.equal (String.sub hay i n) needle || go (i + 1))
in
go 0
in
let r =
request c
"(:op \"eval\" :code \"(defenum Colour [red 0 green 1])\" :file \"/tmp/buf.flan\")"
in
if status r <> "ok" then fail "a new enum: %s" (refusal r)
else begin
let r = request c "(:op \"layout\" :type \"Colour\")" in
if status r <> "error" then fail "an enum answered a struct layout"
else if not (contains (refusal r) "is an enum") then
fail "an enum is refused as: %s" (refusal r)
end;
let r =
request c
"(:op \"eval\" :code \"(defunion Shape [(Circle [r f32])])\" :file \"/tmp/buf.flan\")"
in
if status r <> "ok" then fail "a new union: %s" (refusal r)
else begin
let r = request c "(:op \"layout\" :type \"Shape\")" in
if status r <> "error" then fail "a union answered a struct layout"
else if not (contains (refusal r) "is a union") then
fail "a union is refused as: %s" (refusal r)
end;
(* The identity rule, and the case NEXT.md named: a second [Blob] typed
into a package is [agent/Blob], the qualified name resolves, and the
bare one is refused with the names it could have meant rather than