The structural printer reads only the case in hand

print, the REPL inspector and the break buffer's locals all walk a concrete
type through render.ml, and a union fell through its Named arm to <Shape>.
It now recovers the case from the tag by a chain of comparisons -- the same
shape the enum arm already had, and for the same reason: the name is erased
before any backend sees it -- and reads the fields of that case only. Reading
the others would be reading a payload that is not there.

It prints (Shape.Dot {.x 1.5 .y -2.5}), which is what the source would write.

The union table has to reach the walk, so Render.ctx grew a field and its
three construction sites in session.ml and one in check.ml pass it. That is
the whole of the session.ml change.

test/programs/unions.flan is the program: a case with no fields, a case wider
than another, a case holding a string, a union in a struct, a union through a
call in both directions, ZII, reassignment, and printing. Its layout was
checked against clang's for the same declaration -- 32 bytes aligned 8 with
the payload at offset 8, and 40/8 for the struct holding it.
This commit is contained in:
Joseph Ferano 2026-09-12 16:53:26 +07:00
parent 675241e226
commit 03f7609201
4 changed files with 147 additions and 4 deletions

View File

@ -1121,7 +1121,8 @@ and var ctx loc ~want name =
(Tast.MakeCase (uname, c.Tast.vname, []))) (Tast.MakeCase (uname, c.Tast.vname, [])))
| Some (uname, c) -> | Some (uname, c) ->
fail loc fail loc
"%s is a case of the union %s, and a union value names both — write %s.%s" name uname uname c.Tast.vname "%s is a case of the union %s, and a union value names both — \
write %s.%s" name uname uname c.Tast.vname
| None -> | None ->
if Hashtbl.mem ctx.env.fns name then if Hashtbl.mem ctx.env.fns name then
unimplemented loc unimplemented loc
@ -1479,12 +1480,14 @@ and check_struct ctx ~want loc name kvs =
misspelling. It can now, so it says what was meant. *) misspelling. It can now, so it says what was meant. *)
| Some (uname, c) -> | Some (uname, c) ->
fail loc fail loc
"%s is a case of the union %s, not a struct — a union value names both, as (%s.%s {.field value ...})" "%s is a case of the union %s, not a struct — a union value names \
both, as (%s.%s {.field value ...})"
name uname uname c.Tast.vname name uname uname c.Tast.vname
| None -> | None ->
if Hashtbl.mem ctx.env.unions name then if Hashtbl.mem ctx.env.unions name then
fail loc fail loc
"%s is a union, and a union value names the case as well as the type — write (%s.%s {.field value ...}) for one of %s" "%s is a union, and a union value names the case as well as the \
type write (%s.%s {.field value ...}) for one of %s"
name name (first_case_name ctx.env name) (case_list ctx.env name) name name (first_case_name ctx.env name) (case_list ctx.env name)
else fail loc "unknown struct %s" name) else fail loc "unknown struct %s" name)
| Some s -> | Some s ->
@ -3116,6 +3119,7 @@ and named_call ctx ~want loc name args =
let rc = let rc =
{ Render.structs = { Render.structs =
Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.structs []; Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.structs [];
unions = Hashtbl.fold (fun _ v acc -> v :: acc) ctx.env.unions [];
enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) ctx.env.enums []; enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) ctx.env.enums [];
emit = emitter; emit = emitter;
alloc = (fun ty -> fresh_slot ctx ty) } alloc = (fun ty -> fresh_slot ctx ty) }
@ -3219,7 +3223,8 @@ and named_call ctx ~want loc name args =
reaches for a constructor, and neither is one. *) reaches for a constructor, and neither is one. *)
let uname, c = Hashtbl.find ctx.env.cases name in let uname, c = Hashtbl.find ctx.env.cases name in
fail loc fail loc
"%s is a case of the union %s — write (%s.%s {.field value ...}), or %s.%s on its own when it has no fields" "%s is a case of the union %s — write (%s.%s {.field value ...}), \
or %s.%s on its own when it has no fields"
name uname uname c.Tast.vname uname c.Tast.vname name uname uname c.Tast.vname uname c.Tast.vname
else if Hashtbl.mem ctx.env.structs name then else if Hashtbl.mem ctx.env.structs name then
fail loc fail loc

View File

@ -35,6 +35,10 @@ type emitter = {
type ctx = { type ctx = {
structs : Tast.structure list; structs : Tast.structure list;
(* The declared unions. [Types.Named] covers a struct and a union alike, so
which list the name is in is what says which this is the same
arrangement the checker and the emitter use. *)
unions : Tast.union list;
enums : (string * (string * int64) list) list; enums : (string * (string * int64) list) list;
emit : emitter; emit : emitter;
(* A slot in the *caller's* frame. Only the slice arm needs one, and it needs (* A slot in the *caller's* frame. Only the slice arm needs one, and it needs
@ -132,6 +136,59 @@ let rec render c depth (e : Tast.expr) : Tast.expr list =
(Tast.If (is_some, (Tast.If (is_some,
do_ ((lit "(some " :: render c (depth + 1) some) @ [ lit ")" ]), do_ ((lit "(some " :: render c (depth + 1) some) @ [ lit ")" ]),
lit "none")) ] lit "none")) ]
(* A union, printed as the source would write it: the case is recovered
from the tag by a chain of comparisons, exactly as an enum's member name
is, and only the case in hand has its fields read. Reading the others
would be reading a payload that is not there. *)
| Types.Named n
when List.exists (fun (u : Tast.union) -> String.equal u.Tast.uname n)
c.unions ->
let u =
List.find (fun (u : Tast.union) -> String.equal u.Tast.uname n) c.unions
in
let tag = { Tast.e = Tast.Field (e, 0); ty = Types.Int Types.I32; loc } in
let one i (v : Tast.variant) otherwise =
let is =
{ Tast.e =
Tast.Prim (Tast.Eq,
[ tag; { Tast.e = Tast.Int (Int64.of_int i, Types.I32);
ty = Types.Int Types.I32; loc } ]);
ty = Types.Bool; loc }
in
let full = n ^ "." ^ v.Tast.vname in
let body =
if v.Tast.vfields = [] then lit full
else
let shown = List.filteri (fun i _ -> i < max_span) v.Tast.vfields in
let parts =
List.concat
(List.mapi
(fun i (f : Tast.field) ->
let fv =
{ Tast.e = Tast.CaseField (e, v.Tast.vname, i);
ty = f.Tast.fty; loc }
in
(if i = 0 then [] else [ lit " " ])
@ [ lit ("." ^ f.Tast.fname ^ " ") ]
@ render c (depth + 1) fv)
shown)
in
do_ ((lit ("(" ^ full ^ " {") :: parts)
@ (if List.length v.Tast.vfields > max_span then [ lit " ..." ]
else [])
@ [ lit "})" ])
in
unit_ (Tast.If (is, body, otherwise))
in
(* The fallback is a tag no case names, which only a scribbled-over union
could hold. Showing the number is more use than showing a case it is
not. *)
let base =
do_ [ lit ("<" ^ n ^ " tag ");
c.emit.ei64 (cast (Types.Int Types.I64) tag); lit ">" ]
in
[ List.fold_left (fun acc x -> x acc) base
(List.rev (List.mapi one u.Tast.cases)) ]
| Types.Named n -> | Types.Named n ->
(match (match
List.find_opt (fun (s : Tast.structure) -> String.equal s.Tast.sname n) List.find_opt (fun (s : Tast.structure) -> String.equal s.Tast.sname n)

View File

@ -463,6 +463,7 @@ let render_locals ?(origin = "<locals>") t ~frame ~(fn : Tast.fn) ~bound
let extra = ref [] and nslots = ref 0 in let extra = ref [] and nslots = ref 0 in
let c = let c =
{ Render.structs = t.program.Tast.structs; { Render.structs = t.program.Tast.structs;
unions = t.program.Tast.unions;
enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums []; enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums [];
emit = dev_emitter; emit = dev_emitter;
alloc = (fun ty -> alloc = (fun ty ->
@ -582,6 +583,7 @@ let render_globals ?(origin = "<globals>") t ~(globals : Tast.global list)
let extra = ref [] and nslots = ref 0 in let extra = ref [] and nslots = ref 0 in
let c = let c =
{ Render.structs = t.program.Tast.structs; { Render.structs = t.program.Tast.structs;
unions = t.program.Tast.unions;
enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums []; enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums [];
emit = dev_emitter; emit = dev_emitter;
alloc = (fun ty -> alloc = (fun ty ->
@ -651,6 +653,7 @@ let eval_expr ?(origin = "<eval>") t src : change =
let extra = ref [] and nslots = ref (Array.length base) in let extra = ref [] and nslots = ref (Array.length base) in
let c = let c =
{ Render.structs = t.program.Tast.structs; { Render.structs = t.program.Tast.structs;
unions = t.program.Tast.unions;
enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums []; enums = Hashtbl.fold (fun k v acc -> (k, v) :: acc) t.env.Check.enums [];
emit = dev_emitter; emit = dev_emitter;
alloc = (fun ty -> alloc = (fun ty ->

78
test/programs/unions.flan Normal file
View File

@ -0,0 +1,78 @@
;;;; Union values: declaring one, making one, matching one, printing one.
;;;;
;;;; The layout claim is the load-bearing one, so it is asserted rather than
;;;; described: a union is a tag and room for the largest case, aligned to the
;;;; widest member of any case, which is C's struct { int tag; union {...}; }.
;;;; That is what the macro expander will need to agree with byte for byte, so
;;;; `Shape` here is deliberately the shape a Form has: a case with no fields,
;;;; a case whose members are wider than another's, and a case holding a
;;;; string -- the three things a payload blob has to hold without disturbing
;;;; the alignment of any of them.
(defunion Shape
[Empty
(Dot [x f64 y f64])
(Rect [w i32 h i32])
(Tag [name string n u8])])
;; A union crosses a call boundary in both directions, as a parameter and as a
;; return type -- a value that cannot do that is not a value.
(defn area [s Shape] f64
(match s
(Rect w h) (* (f64 w) (f64 h))
(Dot _x _y) 0.0
_ -1.0))
(defn widen [n i32] Shape (Shape.Rect {.w n .h (* n 2)}))
;; A union as a struct field, which is the path that makes its size and
;; alignment visible to something other than a slot.
(defstruct Cell [id i32 s Shape])
(defn describe [s Shape] string
(match s
Empty "empty"
(Dot x y) (if (= x y) "dot on the diagonal" "dot")
(Rect w h) (if (= w h) "square" "rect")
(Tag name n) name))
(defn main [] i32
;; A case with no fields is a whole value and is written as a name.
(println (describe Shape.Empty))
(println (describe (Shape.Dot {.x 2.0 .y 2.0})))
(println (describe (Shape.Dot {.x 1.0 .y 2.0})))
(println (describe (Shape.Rect {.w 3 .h 3})))
(println (describe (Shape.Tag {.name "tagged" .n 7})))
;; ZII: omitted fields are zeroed, exactly as in a struct literal.
(println (describe (Shape.Rect {.w 0})))
;; Returned from a call, then matched.
(print (i64 (area (widen 4)))) (println "")
(print (i64 (area (Shape.Dot {.x 9.0 .y 9.0})))) (println "")
(print (i64 (area Shape.Empty))) (println "")
;; Through a struct field, and copied: assigning a Cell copies the union's
;; bytes, so the copy's payload must be the original's.
(let [c (Cell {.id 1 .s (Shape.Tag {.name "in a cell" .n 3})})
d c]
(println (describe (.s d)))
;; A zeroed union is the first declared case -- Empty -- which is what
;; makes case order part of the contract.
(let [z (Cell {.id 2})]
(println (describe (.s z)))))
;; A local assigned a second case: the tag moves and the payload is rewritten.
(let [v Shape.Empty]
(set v (Shape.Rect {.w 5 .h 6}))
(print (i64 (area v))) (println "")
(set v (Shape.Tag {.name "reassigned" .n 1}))
(println (describe v)))
;; The structural printer, which reads only the case in hand: the other
;; cases' fields are not there to read.
(print Shape.Empty) (println "")
(print (Shape.Dot {.x 1.5 .y -2.5})) (println "")
(print (Shape.Tag {.name "printed" .n 9})) (println "")
(print (Cell {.id 7 .s (Shape.Rect {.w 1 .h 2})})) (println "")
0)