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:
parent
d5901e809b
commit
cd34c3fea9
27
BUILT.md
27
BUILT.md
@ -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
|
||||
|
||||
@ -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 "")
|
||||
|
||||
@ -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";
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user