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
|
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.
|
*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
|
### 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
|
`(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")
|
(Rect w h) (if (= w h) "square" "rect")
|
||||||
(Tag name n) name))
|
(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
|
(defn main [] i32
|
||||||
;; A case with no fields is a whole value and is written as a name.
|
;; A case with no fields is a whole value and is written as a name.
|
||||||
(println (describe Shape.Empty))
|
(println (describe Shape.Empty))
|
||||||
@ -69,6 +79,14 @@
|
|||||||
(set v (Shape.Tag {.name "reassigned" .n 1}))
|
(set v (Shape.Tag {.name "reassigned" .n 1}))
|
||||||
(println (describe v)))
|
(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
|
;; The structural printer, which reads only the case in hand: the other
|
||||||
;; cases' fields are not there to read.
|
;; cases' fields are not there to read.
|
||||||
(print Shape.Empty) (println "")
|
(print Shape.Empty) (println "")
|
||||||
|
|||||||
@ -1657,7 +1657,7 @@ ERR@7 unexpected token: not the kind the caller was reading
|
|||||||
return value. *)
|
return value. *)
|
||||||
let unions_out =
|
let unions_out =
|
||||||
"empty\ndot on the diagonal\ndot\nsquare\ntagged\nsquare\n\
|
"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.Empty\n(Shape.Dot {.x 1.5 .y -2.5})\n\
|
||||||
(Shape.Tag {.name \"printed\" .n 9})\n\
|
(Shape.Tag {.name \"printed\" .n 9})\n\
|
||||||
(Cell {.id 7 .s (Shape.Rect {.w 1 .h 2})})\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
|
(* 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
|
a case owning a Vec is the refusal a struct field already carries, in
|
||||||
the same words and for the same reason. *)
|
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"
|
refuses_src "a union with no cases"
|
||||||
"(defunion U [])\n(defn f [u U] 0)"
|
"(defunion U [])\n(defn f [u U] 0)"
|
||||||
"declares no cases";
|
"declares no cases";
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user