A union recurses through a pointer, and not by value

check_finite already walked a union's cases, so a union containing itself by
value was refused before the emitter could try to lay it out -- which it would
have done forever, since payload_lay calls lay calls payload_lay. Asserted
both ways round: directly, and two unions through each other.

Through a pointer it works, and that is the shape a Form has, so it is in the
program rather than only in the prose: a Tree with a (Ptr Tree) field, matched
through a deref, summed recursively.

BUILT.md also records why match's fall-through is still unreachable rather
than a trap. It is only sound because no reachable program can hold a tag no
case names: Zero is tag 0, every construction writes a tag the checker
resolved, and uninit -- the one way to get bytes nobody wrote -- is refused on
a union for exactly this reason. The refusal is what pays for the unreachable.
This commit is contained in:
Joseph Ferano 2026-09-12 17:02:59 +07:00
parent d5901e809b
commit cd34c3fea9
3 changed files with 58 additions and 1 deletions

View File

@ -2124,6 +2124,33 @@ is a layout change, the same way reordering a struct's fields is.
field at all — that is a pointer the linker has to relocate and a byte array has nowhere to put a relocation. A
*zeroed* global is fine and needs none of it: it is the first declared case.
### Recursion, and the shape `Form` will have
A union that contains itself by value has no finite size, and `payload_lay`
would recurse forever laying one out rather than failing. It does not get the
chance: `check_finite` already walked a union's cases, so `(defunion T [Leaf
(Node [l T r T])])` is refused with *"T contains itself by value, so it has no
size — go through (Ptr T)"*, and so is a pair of unions that contain each
other. Through a pointer it works, and that is the shape a `Form` has:
```clojure
(defunion Tree [Leaf (Node [l (Ptr Tree) n i32])])
(defn depth [t (Ptr Tree)] i32
(match (deref t)
Leaf 0
(Node l n) (+ n (depth l))))
```
### Why `match`'s fall-through is still `unreachable`
The block after the last case comparison is `unreachable`, kept from the
`Option` path. That is only sound if no reachable program can hold a tag no
case names — and none can: `Zero` is tag 0, which is a real case; every
construction writes a tag the checker resolved; and `uninit`, the one way to
get bytes nobody wrote, is refused on a union for exactly this reason. The
refusal is what pays for the `unreachable`.
### The diagnostics bug, fixed
`(A {.x 1})` on a case of a union said **"unknown struct A"**, because `env` had no table of case names and could not

View File

@ -36,6 +36,16 @@
(Rect w h) (if (= w h) "square" "rect")
(Tag name n) name))
;; A union that names itself through a pointer. check_finite refuses one that
;; contains itself by value -- the emitter would recurse forever laying it out
;; -- and this is the shape that works instead.
(defunion Tree [Leaf (Node [l (Ptr Tree) n i32])])
(defn depth [t (Ptr Tree)] i32
(match (deref t)
Leaf 0
(Node l n) (+ n (depth l))))
(defn main [] i32
;; A case with no fields is a whole value and is written as a name.
(println (describe Shape.Empty))
@ -69,6 +79,14 @@
(set v (Shape.Tag {.name "reassigned" .n 1}))
(println (describe v)))
;; Recursive through a pointer, which is the shape a Form has: a union that
;; contains itself by value has no size and is refused, and (Ptr T) is what
;; breaks the cycle. 5 + 10 + 0.
(let [leaf Tree.Leaf
mid (Tree.Node {.l (addr leaf) .n 10})
top (Tree.Node {.l (addr mid) .n 5})]
(print (depth (addr top))) (println ""))
;; The structural printer, which reads only the case in hand: the other
;; cases' fields are not there to read.
(print Shape.Empty) (println "")

View File

@ -1657,7 +1657,7 @@ ERR@7 unexpected token: not the kind the caller was reading
return value. *)
let unions_out =
"empty\ndot on the diagonal\ndot\nsquare\ntagged\nsquare\n\
32\n0\n-1\nin a cell\nempty\n30\nreassigned\n\
32\n0\n-1\nin a cell\nempty\n30\nreassigned\n15\n\
Shape.Empty\n(Shape.Dot {.x 1.5 .y -2.5})\n\
(Shape.Tag {.name \"printed\" .n 9})\n\
(Cell {.id 7 .s (Shape.Rect {.w 1 .h 2})})\n"
@ -1702,6 +1702,18 @@ ERR@7 unexpected token: not the kind the caller was reading
(* The declaration's own refusals. A union with no cases has no value, and
a case owning a Vec is the refusal a struct field already carries, in
the same words and for the same reason. *)
(* A union that contains itself by value has no finite size, and the
emitter would recurse forever laying one out rather than failing. It is
refused where every other infinitely-sized type is, by the same walk,
which already traversed a union's cases. Both shapes: direct, and two
unions through each other. (Ptr T) breaks the cycle and is exercised in
the program above -- it is the shape a Form has. *)
refuses_src "a union that contains itself by value"
"(defunion T [Leaf (Node [l T r T])])\n(defn f [t T] 0)"
"T contains itself by value";
refuses_src "two unions that contain each other by value"
"(defunion A [(X [b B])])\n(defunion B [(Y [a A])])\n(defn f [a A] 0)"
"contains itself by value";
refuses_src "a union with no cases"
"(defunion U [])\n(defn f [u U] 0)"
"declares no cases";