From 41025fc0acb2ea6525acf96d8b93347c7cff92c9 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Sat, 12 Sep 2026 10:42:50 +0700 Subject: [PATCH] A union is not a missing struct either MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- BUILT.md | 4 +++- lib/dev.ml | 12 +++++++++--- test/test_dev.ml | 38 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+), 4 deletions(-) diff --git a/BUILT.md b/BUILT.md index cb3c77b..710fe8c 100644 --- a/BUILT.md +++ b/BUILT.md @@ -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 diff --git a/lib/dev.ml b/lib/dev.ml index bd6c7d4..8e921a2 100644 --- a/lib/dev.ml +++ b/lib/dev.ml @@ -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 = diff --git a/test/test_dev.ml b/test/test_dev.ml index 6d83f6d..5ce5245 100644 --- a/test/test_dev.ml +++ b/test/test_dev.ml @@ -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