The case inside a struct literal is the same unwritable value as a bare one

const_defconst_init matched MakeCase at the top level only, so a case nested in
a struct literal got the general computed message, whose advice nobody could
follow. Emit.const found it by recursing; this finds it the same way, left to
right and first offender wins, which is the order the emitter spelled fields
in. FIX.org's claim that the messages did not change is rewritten to say what
did change about the general one.
This commit is contained in:
Joseph Ferano 2026-09-20 14:46:53 +07:00
parent ce93ac7622
commit 49ee8774d3
3 changed files with 53 additions and 10 deletions

23
FIX.org
View File

@ -1252,11 +1252,24 @@ what the refusal sees is an [Int]. That pass is integers only, which is why
a second folder and was not done; a test pins that there is not one.
** What came out
- [Emit.const]'s two refusals are gone. The general one and the data type case
one are the checker's now, word for word, so the messages a program sees did
not change. What is left in their place is a [failwith] in the file's own
idiom: no program reaches it, and it fires only if the checker's accepted set
and [Tast.const_init] ever stop agreeing.
- [Emit.const]'s two refusals are gone. Both are the checker's now. The data
type case one is word for word what it was. The general one gained what the
checker knows and the emitter did not: it names the constant — "the constant
c is computed" rather than "this one is computed" — and it spells the way
through, =(defvar c ...)= or a literal, with the integer arithmetic the
folding pass accepts named beside it. Both are located at the declaration
now rather than at the expression inside it, which is where every other
refusal about a global points and what [next-error] jumps to. What is left
in the emitter is a [failwith] in the file's own idiom: no program reaches
it, and it fires only if the checker's accepted set and [Tast.const_init]
ever stop agreeing.
- The case is searched for through the aggregates, which is what [Emit.const]
did by recursing: =(defconst g S (S {.u (U.B {.x 1})}))= is a case the image
cannot hold just as much as a bare one, and "this is computed" would be
advice nobody could act on. Left to right and first offender wins, so a
computed field written before a case field still gets the general message —
the emitter's own order, since it spelled the fields in order and failed at
the first one it could not spell.
- [emit_global]'s =gconst || const_init ginit= lost its left half. The form no
longer decides anything there; the initialiser does, and a zeroinitializer is
now always a defvar waiting for the startup function.

View File

@ -7501,10 +7501,33 @@ let no_union_const env loc n (v : Tast.expr) =
A data type case says it with its own message, which is the one thing a
reader could not work out from "this is computed": the case would have to be
serialised into the payload blob, and that is an encoder rather than an
order of operations. *)
order of operations. It is searched for the way [Emit.const] used to find it
down through the aggregates, because a case inside a struct literal is the
same unwritable value as a case on its own, and [(defconst g S (S {.u (U.B
{.x 1})}))] told about "computed" would be advice nobody could follow.
Left to right and stopping at the first value the image cannot hold, which
is [Emit.const]'s own order: it spelled the fields in order and failed at
the one it could not spell. So a computed field written before a case field
is still the general message, because that is the field a reader meets
first. *)
(* The first subexpression a constant image has no value for, descending
through the aggregates whose parts are themselves constants. [None] is a
value the linker can write, which is [Tast.const_init] arrived at from the
other side the two walk the same nodes, and this one keeps the offender
rather than the verdict. *)
let rec unwritable (e : Tast.expr) =
match e.Tast.e with
| Tast.Int _ | Tast.Float _ | Tast.Bool _ | Tast.Str _ | Tast.Unit
| Tast.Zero _ | Tast.Uninit _ | Tast.None_ -> None
| Tast.Make (_, es) | Tast.Arr es -> List.find_map unwritable es
| Tast.Some_ v -> unwritable v
| _ -> Some e
let const_defconst_init env loc n (v : Tast.expr) =
match v.Tast.e with
| Tast.MakeCase (dname, case, _) ->
match unwritable v with
| None -> ()
| Some { Tast.e = Tast.MakeCase (dname, case, _); _ } ->
fail loc
"a constant cannot be %s.%s — a data type's payload is a blob, and \
writing a case into one at link time needs a byte-level encoder that \
@ -7515,7 +7538,7 @@ let const_defconst_init env loc n (v : Tast.expr) =
(match Hashtbl.find_opt env.datas dname with
| Some { Tast.cases = c :: _; _ } -> c.Tast.vname
| _ -> "its first case")
| _ when not (Tast.const_init v) ->
| Some _ ->
fail loc
"a constant's value must be a compile-time constant — the constant %s \
is computed. A defvar may have a computed initialiser, because it runs \
@ -7524,7 +7547,6 @@ let const_defconst_init env loc n (v : Tast.expr) =
the constant a literal integer constants may also be written as \
arithmetic over literals and other constants, which is folded here"
n n
| _ -> ()
(* A global's initialiser runs at startup: from [main], after the runtime is
up, before a line of the program's own code. Nothing has established a

View File

@ -2090,6 +2090,14 @@ let () =
rejects_check "a computed defconst names the way through"
"(defn seed [] i64 7) (defconst c i64 (seed))"
~needle:"Write (defvar c ...)";
(* The search for a case goes down through the aggregates, because a case
inside a struct literal is the same value the image cannot hold and the
general message's advice make it a defvar, or write a literal is not
followable for one. [Emit.const] recursed for the same reason. *)
rejects_check "a data type case nested in a constant struct"
"(defdata U [A (B [x i32])]) (defstruct S [u U]) \
(defconst g S (S {.u (U.B {.x 1})}))"
~needle:"needs a byte-level encoder that does not exist";
accepts "a constant written as a literal"
"(defconst x u64 0xcbf29ce484222325)";
accepts "a constant written as arithmetic over other constants"