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 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 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` 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; **`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 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) ]) Wire.quote (Types.to_string f.Tast.fty) ])
s.Tast.fields) ] s.Tast.fields) ]
| None -> | 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 members are erased to i32 before [Tast.program] exists, which is the
same fact that makes a defenum unreloadable. Saying which kind it is same fact that makes a defenum unreloadable; a union is declared and
beats "no such type" for a name that plainly exists. *) 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 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") 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 else
let suffix = "/" ^ ty in let suffix = "/" ^ ty in
let candidates = let candidates =

View File

@ -174,6 +174,44 @@ let () =
let r = request c "(:op \"layout\" :type \"Nonesuch\")" in let r = request c "(:op \"layout\" :type \"Nonesuch\")" in
if status r <> "error" then fail "a type that does not exist got a layout"; 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 (* 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 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 bare one is refused with the names it could have meant rather than