diff --git a/lib/check.ml b/lib/check.ml index 6e5be45..9a39427 100644 --- a/lib/check.ml +++ b/lib/check.ml @@ -1735,6 +1735,17 @@ and struct_target ctx (target : Ast.expr) : Tast.expr * string = | Types.Named n when Hashtbl.mem ctx.env.structs n -> t, n | Types.Ptr (Types.Named n) when Hashtbl.mem ctx.env.structs n -> mk t.Tast.loc (Types.Named n) (Tast.Deref t), n + (* A union's fields belong to one case, and which case it is holding is only + known after the tag has been read. [.field] would have to be a read that + might be reading something else, so it is not one: [match] is how a union + is opened, and it binds the fields it has proved are there. *) + | (Types.Named n | Types.Ptr (Types.Named n)) + when Hashtbl.mem ctx.env.unions n -> + fail target.Ast.loc + "%s is a union, and a union's fields belong to a case — which one it is \ + holding is what the tag says, so they are reached by (match ...), \ + whose arms bind the fields of the case they matched" + n | other -> fail target.Ast.loc "%s is not a struct, so it has no fields" (Types.to_string other) @@ -3665,7 +3676,27 @@ let check_global env (d : Ast.decl) : Tast.global option = let ginit = match init with | Ast.Zeroed -> { Tast.e = Tast.Zero ty; ty; loc = d.Ast.dloc } - | Ast.Uninit -> { Tast.e = Tast.Uninit ty; ty; loc = d.Ast.dloc } + | Ast.Uninit -> + (* Everywhere else [uninit] is an opt-out from ZII and the bytes are + whatever they were: a garbage f64 is a garbage number. A union is + the one type where that is qualitatively worse — the tag steers + control flow, a tag no case names falls past every comparison in a + [match], and the block after them is [unreachable], which LLVM is + entitled to assume cannot happen. So the one place where garbage + becomes "the optimiser may do anything" is refused by name, and the + zeroed form, which is the first declared case, is named beside it. *) + (match ty with + | Types.Named un when Hashtbl.mem env.unions un -> + fail d.Ast.dloc + "%s is a union, and uninit on one is refused: its tag steers \ + every match, and a tag no case names has no arm to reach. Drop \ + the uninit — a zeroed %s is %s, which is a real case" + (Types.to_string ty) un + (match Hashtbl.find_opt env.unions un with + | Some { Tast.cases = c :: _; _ } -> un ^ "." ^ c.Tast.vname + | _ -> "its first case") + | _ -> ()); + { Tast.e = Tast.Uninit ty; ty; loc = d.Ast.dloc } | Ast.Init v -> check (ctx ()) ~want:ty v in Some { Tast.gname = n; gty = ty; ginit; gconst = false; gfolded = false } diff --git a/test/test_acceptance.ml b/test/test_acceptance.ml index 00450a6..2e4f81a 100644 --- a/test/test_acceptance.ml +++ b/test/test_acceptance.ml @@ -1740,6 +1740,18 @@ ERR@7 unexpected token: not the kind the caller was reading incr failures; Printf.printf "FAIL %s\n said: %S\n" name m end); + (* uninit is an opt-out from ZII everywhere else and the bytes are just + bytes. On a union they steer control flow: a tag no case names falls + past every comparison in a match into the block LLVM is entitled to + assume cannot be reached. *) + refuses_src "uninit on a union global" + "(defunion U [A B])\n(defvar g U uninit)\n(defn main [] i32 0)" + "its tag steers every match"; + (* A union's fields belong to a case, so .field is not a read anyone can + do without having read the tag first. match is how one is opened. *) + refuses_src "reading a field of a union directly" + "(defunion U [(A [x i32])])\n(defn f [u U] i32 (.x u))" + "reached by (match ...)"; (* And a zeroed one is fine, which is the other half of the same rule: it is the first declared case, all bytes zero, and needs no encoder. *) (let name = "a zeroed union global" in