sentinel-filled is now dead-beef, and takes the pattern

The author's revision. The name says what it writes, and the pattern is the
program's to choose: (dead-beef) is DEADBEEF, (dead-beef 0xBAADF00D) is
BA AD F0 0D. One byte-order rule covers both — a pattern's ascending bytes
are its big-endian bytes, which is how the hex literal reads left to right —
so every candidate DISCUSS.org listed is now spellable without the compiler
naming any of them.

The bare form is not a case a backend knows about: the checker writes
Tast.dead_beef_default in where the argument would have been, so
(dead-beef) and (dead-beef 0xDEADBEEF) are the same node and an acceptance
row prints both to say so.

The operand is an ordinary u32 expression, which is what the byte arm
already accepts for its byte. A literal is byte-reversed at compile time and
still reaches the loop as an immediate; a computed one is reversed at run
time, by llvm.bswap.i32 on one backend and bswap on the other, after which
the tail shifts its bytes out of the word rather than folding them. The
program runs a computed pattern over lengths 6 and 7 deliberately: that is
the case a constant-only implementation would pass by accident.

filled is untouched, and so is the fill boundary.
This commit is contained in:
Joseph Ferano 2026-09-20 18:33:05 +07:00
parent de3a3ed3c8
commit 7bd2c99353
10 changed files with 421 additions and 125 deletions

View File

@ -186,9 +186,18 @@ why MSVC/glibc-style debug allocators use a single repeated byte instead
Built, 2026-09-20, as two builtins rather than one — the author's answer to Built, 2026-09-20, as two builtins rather than one — the author's answer to
the cheap-byte-or-real-pattern question was "why not both? we need some sort the cheap-byte-or-real-pattern question was "why not both? we need some sort
of memset -1 right? and dead-beef can loop, that's fine". They are of memset -1 right? and dead-beef can loop, that's fine". They are
(filled BYTE) and (sentinel-filled), spelled the way [zeroed] is: the value (filled BYTE) and (dead-beef), spelled the way [zeroed] is: the value of
of whatever type is expected of them, so (set grid (filled 0xFF)) is how a whatever type is expected of them, so (set grid (filled 0xFF)) is how a place
place is filled and there is no place-taking form to learn beside [set]. is filled and there is no place-taking form to learn beside [set].
(dead-beef) writes the default DEADBEEF and (dead-beef 0xBAADF00D) writes the
pattern given, under one byte-order rule: a pattern's ascending bytes are its
big-endian bytes, which is how the hex literal reads left to right. So every
candidate listed above is spellable without the compiler naming any of them,
and the bare form is checked into the spelled-out default rather than being a
case a backend knows about. The pattern may be computed, not only written —
same rule as the byte arm, and both backends byte-reverse at run time when it
is.
The snag above is why there are two and not one with a wider operand. The The snag above is why there are two and not one with a wider operand. The
byte fill is one llvm.memset / one rep stosb; the 4-byte pattern is a loop on byte fill is one llvm.memset / one rep stosb; the 4-byte pattern is a loop on

99
FIX.org
View File

@ -1618,12 +1618,19 @@ answer to the single-byte-or-four-byte question was "why not both? we need
some sort of memset -1 right? and dead-beef can loop, that's fine", so there some sort of memset -1 right? and dead-beef can loop, that's fine", so there
are two builtins and they are siblings of ~zeroed~, not a new shape. are two builtins and they are siblings of ~zeroed~, not a new shape.
Revised the same day: the pattern builtin was ~sentinel-filled~ and is now
~dead-beef~, and it gained an optional operand so the pattern is the
program's to choose. What did not change is ~filled~, or the fill boundary,
or the byte-order rule — the revision generalised the pattern, it did not
reopen what may be filled.
** The spellings ** The spellings
~(filled BYTE)~ and ~(sentinel-filled)~, both value forms driven by the type ~(filled BYTE)~ and ~(dead-beef)~ / ~(dead-beef PATTERN)~, all value forms
expected of them, exactly as ~(zeroed)~ is: driven by the type expected of them, exactly as ~(zeroed)~ is:
: (set grid (filled 0xFF)) : (set grid (filled 0xFF))
: (set frame (sentinel-filled)) : (set frame (dead-beef))
: (set frame (dead-beef 0xBAADF00D))
A place-taking ~(filled place byte)~ was the other candidate and was not A place-taking ~(filled place byte)~ was the other candidate and was not
taken. ~zeroed~ already answers "the all-bytes-X value of whatever this is taken. ~zeroed~ already answers "the all-bytes-X value of whatever this is
@ -1633,9 +1640,29 @@ nothing. The cost is real and is paid on purpose: a fill in a position that
expects no type is refused ("filled needs to know the type it is filling"), expects no type is refused ("filled needs to know the type it is filling"),
which is ~zeroed~'s own refusal worn by both siblings. which is ~zeroed~'s own refusal worn by both siblings.
~sentinel-filled~ takes no operand. The pattern is fixed — that is the whole ~dead-beef~ takes the pattern or leaves it out, and leaving it out is
point of it, since a hex dump only reads DEADBEEF if nobody can change it — *defined as* writing the default: the checker's zero-argument arm builds the
and a parameterised one would be a different builtin. same ~Tast.Int 0xDEADBEEF~ the spelled-out call would have, so ~(dead-beef)~
and ~(dead-beef 0xDEADBEEF)~ are the same IR node by construction and no
backend has a second path for the bare form. An acceptance row prints both
and pins that they agree.
The pattern is an ordinary ~u32~ /expression/, not a literal — the byte arm's
rule at four times the width. A literal out of range meets ~in_range~'s
located "does not fit in u32"; anything computed is guaranteed by its type
instead, since a ~u32~ cannot be out of ~u32~ range. Refusing a computed one
would have been a restriction with no mechanism behind it: neither backend
needs the number early.
/Reading note, for whoever reviews this./ The revision asked for "a u32-range
constant ... decide literal-only vs any constant expression from what the
byte-fill arm already accepts". The byte-fill arm accepts any ~u8~
expression, runtime ones included, and the same instruction asked that both
backends handle "eax loaded from a value, not an immediate" — which only
exists if a computed pattern is legal. So the operand is any ~u32~
expression. That is a strict superset of constants-only: every program the
narrower reading allows behaves identically here. Tighten it to literals if
that was the intent; nothing else depends on the breadth.
** The fill boundary — what may be overwritten with raw bytes ** The fill boundary — what may be overwritten with raw bytes
*Numbers, and structs and fixed arrays built out of numbers. Nothing else.* *Numbers, and structs and fixed arrays built out of numbers. Nothing else.*
@ -1675,31 +1702,53 @@ into the image. A ~defvar~ is fine and goes through the startup function on
both backends, which ~programs/fill.flan~ pins. both backends, which ~programs/fill.flan~ pins.
** The byte order, which is the specification ** The byte order, which is the specification
DE AD BE EF in *ascending address order*, so ~xxd~ reads "deadbeef". A *A pattern's ascending bytes are its big-endian bytes* — exactly how the hex
little-endian store of those four bytes is the i32 0xEFBEADDE, and literal reads left to right. So ~(dead-beef)~ lays down DE AD BE EF and ~xxd~
~Emit.sentinel_bytes~ / ~Emit.sentinel_word~ are the one place either is reads "deadbeef"; ~(dead-beef 0xBAADF00D)~ lays down BA AD F0 0D. One rule,
written — x86.ml reads both out of Emit rather than repeating them, so the both arities.
two backends cannot drift.
On a little-endian machine the word a 4-byte store must therefore leave is
the *byte reversal* of the pattern, which is all ~Emit.word_of_pattern~ is
(~bytes_of_pattern~ beside it is the ascending list). Those two are the one
place the order is written; x86.ml reads them out of Emit rather than
repeating them, and ~Tast.dead_beef_default~ is the one place 0xDEADBEEF is
written. Three definitions, no duplicates, so the default and the
parameterised case cannot drift.
A literal pattern is reversed at compile time and reaches the loop as an
immediate — the default's generated code is exactly what it was before the
pattern became an operand. A computed one is evaluated and reversed at run
time, by ~llvm.bswap.i32~ on one backend and ~bswap eax~ (0F C8, new) on the
other.
*Tail behaviour.* A size that is not a multiple of four ends on a prefix of *Tail behaviour.* A size that is not a multiple of four ends on a prefix of
the pattern: 1 byte over is DE, 2 is DE AD, 3 is DE AD BE. Both backends the ascending bytes: 1 byte over is DE, 2 is DE AD, 3 is DE AD BE.
write the tail byte by byte from the same list. ~programs/fill.flan~ has all Equivalently, tail byte k is ~(word >> 8k) & 0xFF~ — which is what the
four lengths — 8, 9, 6 and 7. computed path actually does, by shifting, since there is no constant to fold.
~programs/fill.flan~ has all four lengths (8, 9, 6, 7) and, crucially, runs a
computed pattern over lengths 6 and 7: that is the case a constant-only
implementation would pass by accident.
** The backends ** The backends
- *LLVM (emit.ml).* The byte fill is one ~llvm.memset~ with the byte as an - *LLVM (emit.ml).* The byte fill is one ~llvm.memset~ with the byte as an
operand instead of a zero — the same call the existing bulk zero makes, and operand instead of a zero — the same call the existing bulk zero makes, and
the reason the single-byte fill is the cheap one. The sentinel cannot be a the reason the single-byte fill is the cheap one. The pattern fill cannot
memset at all (the intrinsic takes one repeated i8, which is the snag be a memset at all (the intrinsic takes one repeated i8, which is the snag
DISCUSS.org named), so it is a counted loop over dwords in the DISCUSS.org named), so it is a counted loop over dwords in the
header/body/exit shape ~emit_while~ writes, with the counter as an header/body/exit shape ~emit_while~ writes, with the counter as an
entry-block alloca that ~mem2reg~ promotes. Every store is ~align 1~, entry-block alloca that ~mem2reg~ promotes. Every store is ~align 1~,
because a ~[7 u8]~ array is a legal thing to fill. because a ~[7 u8]~ array is a legal thing to fill. A computed pattern goes
through ~llvm.bswap.i32~ (newly declared) and the loop stores an SSA value
rather than a constant; the tail then shifts and truncates.
- *x86 (x86.ml).* ~rep stosb~ for the byte fill — ~zero_loc~'s three - *x86 (x86.ml).* ~rep stosb~ for the byte fill — ~zero_loc~'s three
registers with the program's byte in ~al~ instead of a zero — and ~rep registers with the program's byte in ~al~ instead of a zero — and ~rep
stosd~ (new, 0xf3 0xab) for the sentinel, with 0xEFBEADDE in ~eax~. The stosd~ (new, 0xf3 0xab) for the pattern, with the stored word in ~eax~. A
byte operand is evaluated *before* ~rdi~ is loaded, because evaluating it computed pattern is loaded and run through ~bswap~ (new, 0F C8); the tail
may call and a call clobbers ~rdi~. walks the bytes out of ~rax~ with ~shr~ by an immediate (new, C1 /5), which
is used rather than ~shift_cl~ precisely because ~rep stosd~ leaves ~rcx~
at zero. Either operand is evaluated *before* ~rdi~ is loaded, because
evaluating one may call and a call clobbers ~rdi~; ~rep stosd~ does not
touch ~rax~, which is what lets the tail keep reading the word out of it.
- The one asymmetry: ~emit.ml~ needs a ~Tast.Set~ arm of its own to fill the - The one asymmetry: ~emit.ml~ needs a ~Tast.Set~ arm of its own to fill the
place rather than a temporary, because its value path returns an SSA value. place rather than a temporary, because its value path returns an SSA value.
~x86.ml~ needs none — a ~Set~ there already lowers its value into the ~x86.ml~ needs none — a ~Set~ there already lowers its value into the
@ -1712,9 +1761,11 @@ four lengths — 8, 9, 6 and 7.
~dune test --root .~ green (exit 0, no FAIL lines). Three acceptance rows ~dune test --root .~ green (exit 0, no FAIL lines). Three acceptance rows
over ~test/programs/fill.flan~ — default, ~-O0~ and ~--x86~ — and the three over ~test/programs/fill.flan~ — default, ~-O0~ and ~--x86~ — and the three
outputs diffed against each other by hand before the rows were written: outputs diffed against each other by hand before the rows were written:
byte-identical — and the same three rows confirmed to actually run, by byte-identical, with a fourth build (~--dev~) added at the rename: four-way
breaking one expectation on purpose and watching all three report. Fourteen identical. The three rows were confirmed to actually run, by breaking one
checker rows in ~test_flan.ml~: two accepting, and twelve refusals covering expectation on purpose and watching all three report. Seventeen checker rows
the boundary, both arities, both no-expected-type positions, the byte's in ~test_flan.ml~: four accepting (both ~dead-beef~ arities and a computed
pattern among them), and thirteen refusals covering the boundary, both
arities, both no-expected-type positions, the byte's range, the pattern's
range and the ~defconst~ rule. Per the sweep policy the ~@x86~ and range and the ~defconst~ rule. Per the sweep policy the ~@x86~ and
~@sanitize~ sweeps were not run here. ~@sanitize~ sweeps were not run here.

View File

@ -4873,12 +4873,29 @@ and named_call ctx ~want loc name args =
(* [zeroed]'s two siblings, and the same shape exactly: a value of whatever (* [zeroed]'s two siblings, and the same shape exactly: a value of whatever
type is expected of it, so [(set grid (filled 0xFF))] is how a place is type is expected of it, so [(set grid (filled 0xFF))] is how a place is
filled and there is no second spelling to learn. What they add over filled and there is no second spelling to learn. What they add over
[zeroed] is the byte [(filled b)] repeats one the program picks, and [zeroed] is the bytes [(filled b)] repeats one the program picks, and
[(sentinel-filled)] repeats the four bytes DE AD BE EF and with it the [(dead-beef)] repeats four and with them the question [zeroed] never
question [zeroed] never has to ask: zero is a value every type can have, has to ask: zero is a value every type can have, and 0xDE is not.
and 0xDE is not. [unfillable] above is the whole of the answer. *) [unfillable] above is the whole of the answer.
| "filled" | "sentinel-filled" ->
arity loc name (if String.equal name "filled" then 1 else 0) args; [dead-beef] takes the pattern or leaves it out, and leaving it out is
defined as writing the default: the [None] arm below builds the same
literal the source would have, so [(dead-beef)] and
[(dead-beef 0xDEADBEEF)] are the same node by construction and no
backend has a second path for the bare form.
The pattern is an ordinary u32 expression, which is the byte arm's rule
at four times the width a literal out of range meets [in_range]'s
located "does not fit in u32", and anything computed is guaranteed by
its type instead. Both builtins take a value rather than only a literal
for the same reason: refusing one would be a restriction with no
mechanism behind it, since neither backend needs the number early. *)
| "filled" | "dead-beef" ->
let is_byte = String.equal name "filled" in
if is_byte then arity loc name 1 args
else if List.length args > 1 then
fail loc "%s takes the pattern or nothing at all, given %d arguments"
name (List.length args);
(match want with (match want with
| Some ty when ty <> Types.Never -> | Some ty when ty <> Types.Never ->
(match unfillable ctx.env [] ty with (match unfillable ctx.env [] ty with
@ -4907,16 +4924,27 @@ and named_call ctx ~want loc name args =
| _ -> "it carries a tag that names a case, and no byte pattern \ | _ -> "it carries a tag that names a case, and no byte pattern \
names a real one") names a real one")
| None -> ()); | None -> ());
if String.equal name "filled" then if is_byte then
let b = check ctx ~want:(Types.Int Types.U8) (List.hd args) in let b = check ctx ~want:(Types.Int Types.U8) (List.hd args) in
mk loc ty (Tast.Fill (ty, b)) mk loc ty (Tast.Fill (ty, b))
else mk loc ty (Tast.Sentinel ty) else
let pat =
match args with
| [ a ] -> check ctx ~want:(Types.Int Types.U32) a
(* The bare form, written out. Not a default a backend applies:
the node that leaves here is the one the spelled-out call would
have left, which is what makes the equivalence a fact about the
IR rather than a promise two emitters keep separately. *)
| _ ->
mk loc (Types.Int Types.U32)
(Tast.Int (Int64.of_int32 Tast.dead_beef_default, Types.U32))
in
mk loc ty (Tast.DeadBeef (ty, pat))
| _ -> | _ ->
fail loc fail loc
"%s needs to know the type it is filling — use it where one is \ "%s needs to know the type it is filling — use it where one is \
expected, as in (set grid (%s))" expected, as in (set grid (%s))"
name name (if is_byte then "filled 0xFF" else name))
(if String.equal name "filled" then "filled 0xFF" else name))
(* The one half of a destructuring [let] that [Parse] cannot do on its own. (* The one half of a destructuring [let] that [Parse] cannot do on its own.
Everything else about a pattern is bindings and field accesses it already Everything else about a pattern is bindings and field accesses it already
@ -6815,10 +6843,11 @@ let builtins : (string * string * string) list =
"Every byte of whatever it is being stored into set to one byte, as in \ "Every byte of whatever it is being stored into set to one byte, as in \
(set grid (filled 0xFF)). Numbers only, and structs and fixed arrays \ (set grid (filled 0xFF)). Numbers only, and structs and fixed arrays \
built out of them."); built out of them.");
("sentinel-filled", "sentinel-filled [] T", ("dead-beef", "dead-beef [u32?] T",
"The bytes DE AD BE EF repeating over whatever it is being stored into, \ "A four-byte pattern repeating over whatever it is being stored into, \
so a hex dump reads DEADBEEF. A size that is not a multiple of four \ written so a hex dump reads it left to right: 0xDEADBEEF with no \
ends on a prefix of the pattern. Same types [filled] takes."); argument, the u32 given otherwise. A size that is not a multiple of \
four ends on a prefix of the pattern. Same types [filled] takes.");
("destructure~nth", "destructure~nth [[n T] i32 i32 i32] T", ("destructure~nth", "destructure~nth [[n T] i32 i32 i32] T",
"Written by the compiler for a destructuring let, and unspellable: the \ "Written by the compiler for a destructuring let, and unspellable: the \
reader makes ~ a delimiter, so no source symbol can name this."); reader makes ~ a delimiter, so no source symbol can name this.");

View File

@ -937,29 +937,46 @@ let emit_bulk_zero f ptr ty =
end else false end else false
| _ -> false | _ -> false
(* ── The sentinel pattern ────────────────────────────────────────────── (* ── The dead-beef pattern ─────────────────────────────────────────────
DE AD BE EF, in ascending address order, so [xxd] over the filled storage [(dead-beef)] writes DE AD BE EF in ascending address order, so [xxd] over
reads "deadbeef" and not "efbeadde". That is the whole reason the builtin the filled storage reads "deadbeef" and not "efbeadde". That is the whole
exists, so the byte order is the specification and not an implementation reason the builtin exists, so the byte order is the specification and not
detail: the list below is the order the bytes land in memory, and the i32 an implementation detail.
beside it is the little-endian word that puts them there. Both backends
write the same four bytes x86 through [rep stosd] with the same word in
[eax], this file through a store of the same constant and the acceptance
program reads the bytes back by index to keep them honest.
A run whose size is not a multiple of four ends on a prefix: DE, then DE [(dead-beef V)] writes V's four bytes under the same rule, and the rule is
AD, then DE AD BE. The tail is written byte by byte, from the same list, so what makes the two arities one thing: *a pattern's ascending bytes are its
there is one source for the order. *) big-endian bytes*, which is exactly how the hex literal reads left to
let sentinel_bytes = [ 0xDE; 0xAD; 0xBE; 0xEF ] right. So [(dead-beef 0xBAADF00D)] lays down BA AD F0 0D. Nothing here
knows about the bare form: the checker writes [Tast.dead_beef_default] in
where the argument would have been, so this file only ever sees a pattern.
(* The four bytes as the i32 a little-endian store of them would leave, folded On a little-endian machine the word a 4-byte store must leave is therefore
from the list rather than written out, so the two cannot drift apart. *) the byte reversal of the pattern, which is all [word_of_pattern] is. Both
let sentinel_word = backends go through here x86 through [rep stosd] with this word in [eax],
this file through a store of it and the acceptance program reads the
bytes back by index to keep them honest.
A run whose size is not a multiple of four ends on a prefix of the
ascending bytes: one over is DE, two is DE AD, three is DE AD BE. *)
(* The pattern's bytes in the order they land in memory, which is
most-significant first the order the literal is written in. *)
let bytes_of_pattern (v : int32) =
List.map
(fun k ->
Int32.to_int
(Int32.logand (Int32.shift_right_logical v (8 * k)) 0xFFl))
[ 3; 2; 1; 0 ]
(* The i32 a little-endian store leaves those bytes as: the same list folded
back the other way, so one definition answers for both and a change to the
byte order cannot reach only half of it. *)
let word_of_pattern (v : int32) =
List.fold_left List.fold_left
(fun acc b -> Int32.logor (Int32.shift_right_logical acc 8) (fun acc b -> Int32.logor (Int32.shift_right_logical acc 8)
(Int32.shift_left (Int32.of_int b) 24)) (Int32.shift_left (Int32.of_int b) 24))
0l sentinel_bytes 0l (bytes_of_pattern v)
let term f fmt = let term f fmt =
Printf.ksprintf Printf.ksprintf
@ -1192,10 +1209,18 @@ let emit_byte_fill f ptr ty (byte : string) =
"call void @llvm.memset.p0.i64(ptr align %d %s, i8 %s, i64 %d, i1 false)" "call void @llvm.memset.p0.i64(ptr align %d %s, i8 %s, i64 %d, i1 false)"
align ptr byte size align ptr byte size
(* [(sentinel-filled)] over the same. [llvm.memset] takes one repeated i8 and (* [(dead-beef PATTERN)] over the same. [llvm.memset] takes one repeated i8
nothing else, so a four-byte pattern cannot be a call and has to be a loop and nothing else, so a four-byte pattern cannot be a call and has to be a
the snag DISCUSS.org named before this was built, and the reason the two loop the snag DISCUSS.org named before this was built, and the reason the
builtins are two and not one with a wider operand. two builtins are two and not one with a wider operand.
The word is either a folded constant or an SSA value, and [word] below is
just its spelling: a literal pattern is byte-reversed here at compile time
and a computed one by [llvm.bswap.i32] at run time, after which the loop
and the tail are the same instructions either way. That is deliberate
the case a constant-only implementation would quietly get wrong is a
computed pattern over a length that is not a multiple of four, and there
is only one tail here for it to get wrong.
The counter is an entry-block alloca, which is how every local in this file The counter is an entry-block alloca, which is how every local in this file
is spelled and what [mem2reg] promotes; the loop is the same is spelled and what [mem2reg] promotes; the loop is the same
@ -1204,9 +1229,12 @@ let emit_byte_fill f ptr ty (byte : string) =
Every store is [align 1]: the pattern is laid down over bytes, and the Every store is [align 1]: the pattern is laid down over bytes, and the
storage's own alignment may be 1 a [7 u8] array is a legal thing to fill. storage's own alignment may be 1 a [7 u8] array is a legal thing to fill.
The tail's up-to-three bytes are written out straight, from the same list
the word was folded from. *) The tail is the first up-to-three bytes of the *stored word*, lowest
let emit_sentinel f ptr ty = address first, which is byte k = (word >> 8k) & 0xFF. For a constant that
folds to [bytes_of_pattern]'s list; for a computed word it is a shift and
a truncate, ~tail~ times, unrolled because the count is known here. *)
let emit_dead_beef f ptr ty ~(word : string) ~(folded : int32 option) =
let size, _ = lay f.md ty in let size, _ = lay f.md ty in
let words = size / 4 and tail = size mod 4 in let words = size / 4 and tail = size mod 4 in
if words > 0 then begin if words > 0 then begin
@ -1226,7 +1254,7 @@ let emit_sentinel f ptr ty =
ins f "%s = mul i64 %s, 4" off c; ins f "%s = mul i64 %s, 4" off c;
let p = fresh f in let p = fresh f in
ins f "%s = getelementptr i8, ptr %s, i64 %s" p ptr off; ins f "%s = getelementptr i8, ptr %s, i64 %s" p ptr off;
ins f "store i32 %ld, ptr %s, align 1" sentinel_word p; ins f "store i32 %s, ptr %s, align 1" word p;
let n = fresh f in let n = fresh f in
ins f "%s = add i64 %s, 1" n c; ins f "%s = add i64 %s, 1" n c;
ins f "store i64 %s, ptr %s" n i; ins f "store i64 %s, ptr %s" n i;
@ -1236,7 +1264,15 @@ let emit_sentinel f ptr ty =
for k = 0 to tail - 1 do for k = 0 to tail - 1 do
let p = fresh f in let p = fresh f in
ins f "%s = getelementptr i8, ptr %s, i64 %d" p ptr (words * 4 + k); ins f "%s = getelementptr i8, ptr %s, i64 %d" p ptr (words * 4 + k);
ins f "store i8 %d, ptr %s, align 1" (List.nth sentinel_bytes k) p match folded with
| Some v ->
ins f "store i8 %d, ptr %s, align 1" (List.nth (bytes_of_pattern v) k) p
| None ->
let sh = fresh f in
ins f "%s = lshr i32 %s, %d" sh word (8 * k);
let b = fresh f in
ins f "%s = trunc i32 %s to i8" b sh;
ins f "store i8 %s, ptr %s, align 1" b p
done done
(* ── Constants ─────────────────────────────────────────────────────── *) (* ── Constants ─────────────────────────────────────────────────────── *)
@ -1706,9 +1742,10 @@ and value_at f (e : Tast.expr) : string =
let tmp = alloca f ty in let tmp = alloca f ty in
emit_byte_fill f tmp ty bv; emit_byte_fill f tmp ty bv;
load f tmp ty load f tmp ty
| Tast.Sentinel ty -> | Tast.DeadBeef (ty, pat) ->
let word, folded = dead_beef_word f pat in
let tmp = alloca f ty in let tmp = alloca f ty in
emit_sentinel f tmp ty; emit_dead_beef f tmp ty ~word ~folded;
load f tmp ty load f tmp ty
| Tast.Local _ | Tast.Global _ | Tast.Field _ | Tast.Deref _ -> | Tast.Local _ | Tast.Global _ | Tast.Field _ | Tast.Deref _ ->
(* Everything that denotes a location is a load from its address. *) (* Everything that denotes a location is a load from its address. *)
@ -1769,7 +1806,9 @@ and value_at f (e : Tast.expr) : string =
the bulk zero above takes, and the reason [(set grid (filled 0xFF))] the bulk zero above takes, and the reason [(set grid (filled 0xFF))]
is one memset. *) is one memset. *)
| Tast.Fill (_, b) -> emit_byte_fill f ptr ty (value f b) | Tast.Fill (_, b) -> emit_byte_fill f ptr ty (value f b)
| Tast.Sentinel _ -> emit_sentinel f ptr ty | Tast.DeadBeef (_, pat) ->
let word, folded = dead_beef_word f pat in
emit_dead_beef f ptr ty ~word ~folded
| _ -> | _ ->
let v' = value f v in let v' = value f v in
ins f "store %s %s, ptr %s" (ll ty) v' ptr); ins f "store %s %s, ptr %s" (ll ty) v' ptr);
@ -1896,6 +1935,24 @@ and load f ptr ty =
ins f "%s = load %s, ptr %s" t (ll ty) ptr; ins f "%s = load %s, ptr %s" t (ll ty) ptr;
t t
(* The word a [(dead-beef PATTERN)] store must leave, and whether it is known
here. A literal pattern is reversed at compile time and reaches the loop as
an immediate, which is what keeps the default's code exactly what it was
before the pattern became an operand. Anything else is evaluated and
reversed by [llvm.bswap.i32] the pattern is a u32 the program computed,
and the byte order it is written in is not a property of how it was
spelled. The [int32] comes back so the tail can fold too. *)
and dead_beef_word f (pat : Tast.expr) : string * int32 option =
match pat.Tast.e with
| Tast.Int (n, _) ->
let v = word_of_pattern (Int64.to_int32 n) in
(Printf.sprintf "%ld" v, Some (Int64.to_int32 n))
| _ ->
let v = value f pat in
let t = fresh f in
ins f "%s = call i32 @llvm.bswap.i32(i32 %s)" t v;
(t, None)
(* The address of an expression that denotes a location. Anything else is (* The address of an expression that denotes a location. Anything else is
spilled to a temporary first, so [(at (f) 0)] on a returned array works. *) spilled to a temporary first, so [(at (f) 0)] on a returned array works. *)
and addr f (e : Tast.expr) : string = and addr f (e : Tast.expr) : string =
@ -3664,6 +3721,7 @@ let header = {|; Generated by flan. The layout is C's: no object headers anywher
@flan_frame_head = external global ptr @flan_frame_head = external global ptr
declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg) declare void @llvm.memset.p0.i64(ptr nocapture writeonly, i8, i64, i1 immarg)
declare i32 @llvm.bswap.i32(i32)
declare void @flan_rt_init(i32, ptr) declare void @flan_rt_init(i32, ptr)
declare void @flan_argv(ptr) declare void @flan_argv(ptr)
declare void @flan_write_stdout(ptr, i64) declare void @flan_write_stdout(ptr, i64)

View File

@ -674,7 +674,7 @@ let rec value f (e : Tast.expr) : string =
storage to write over: a Flan struct is a JS object here, not a run of storage to write over: a Flan struct is a JS object here, not a run of
bytes, so there is nothing for 0xFF or DEADBEEF to mean. Refused by name bytes, so there is nothing for 0xFF or DEADBEEF to mean. Refused by name
rather than approximated, which is this file's rule. *) rather than approximated, which is this file's rule. *)
| Tast.Fill _ | Tast.Sentinel _ -> | Tast.Fill _ | Tast.DeadBeef _ ->
unsupported unsupported
"js: a byte fill has no meaning on this backend — a struct is an \ "js: a byte fill has no meaning on this backend — a struct is an \
object here and not a run of bytes" object here and not a run of bytes"

View File

@ -81,14 +81,21 @@ and expr_kind =
that byte is an *expression* a memset's operand, not a literal and that byte is an *expression* a memset's operand, not a literal and
every reader of [Zero] treats it as a leaf with nothing under it. every reader of [Zero] treats it as a leaf with nothing under it.
[(sentinel-filled)]: the four bytes DE AD BE EF repeating, ascending [(dead-beef PATTERN)]: the pattern's four bytes repeating, ascending
through the storage, so a hex dump reads DEADBEEF. It carries no operand through the storage, so a hex dump reads the pattern left to right
because the pattern is fixed; that is the whole point of it, and a DEADBEEF by default, and whatever u32 was written otherwise. The operand
parameterised one would be a different builtin. A size that is not a is always present by the time it reaches here: [(dead-beef)] is checked
multiple of four ends on a prefix of the pattern DE, DE AD, DE AD BE into [(dead-beef 0xDEADBEEF)], so a backend has one shape to lower and
which both backends produce identically. *) the default cannot acquire a code path of its own. A size that is not a
multiple of four ends on a prefix of the pattern DE, DE AD, DE AD BE
for the default which both backends produce identically.
The operand is an expression and not an [int32] for the same reason
[Fill]'s byte is: it may be computed. A literal one is folded by each
backend into the immediate it always was; anything else is evaluated and
byte-reversed at run time. *)
| Fill of Types.t * expr | Fill of Types.t * expr
| Sentinel of Types.t | DeadBeef of Types.t * expr
| Local of int (* slot index into the frame *) | Local of int (* slot index into the frame *)
| Global of string | Global of string
| Prim of prim * expr list | Prim of prim * expr list
@ -373,8 +380,12 @@ let rec walk (f : expr -> unit) (e : expr) =
let gos = List.iter go in let gos = List.iter go in
match e.e with match e.e with
| Int _ | Float _ | Bool _ | Str _ | Unit | Zero _ | Uninit _ | Local _ | Int _ | Float _ | Bool _ | Str _ | Unit | Zero _ | Uninit _ | Local _
| Global _ | None_ | FnAddr _ | Break _ | Continue _ | Sentinel _ -> () | Global _ | None_ | FnAddr _ | Break _ | Continue _ -> ()
| Fill (_, b) -> go b (* Both carry an operand, and it must be walked: a call written inside a
fill's byte or a dead-beef's pattern is a call, and [Reach] roots what it
finds here. A leaf row would drop it silently the match would still
compile. *)
| Fill (_, b) | DeadBeef (_, b) -> go b
| Prim (_, es) | Call (_, es) | Do es | Make (_, es) | MakeCase (_, _, es) | Prim (_, es) | Call (_, es) | Do es | Make (_, es) | MakeCase (_, _, es)
| Arr es | InvokeRestart (_, _, es, _, _, _) -> gos es | Arr es | InvokeRestart (_, _, es, _, _, _) -> gos es
| CallPtr (c, es) -> go c; gos es | CallPtr (c, es) -> go c; gos es
@ -412,6 +423,13 @@ and walk_place f (p : place) =
rather than an error. A data type case written into the image would need a rather than an error. A data type case written into the image would need a
byte-level encoder that could not encode a string field at all; written as a byte-level encoder that could not encode a string field at all; written as a
store at startup it needs nothing. *) store at startup it needs nothing. *)
(* The pattern [(dead-beef)] means, and the name the builtin is spelled after.
It lives here rather than in a backend because it is a fact about the
language what the bare form of a builtin means and because the checker
is what writes it in: a [DeadBeef] node always carries its pattern, so no
emitter has a default of its own to keep in step with this one. *)
let dead_beef_default = 0xDEADBEEFl
let rec const_init (e : expr) = let rec const_init (e : expr) =
match e.e with match e.e with
| Int _ | Float _ | Bool _ | Str _ | Unit | Zero _ | Uninit _ | None_ -> true | Int _ | Float _ | Bool _ | Str _ | Unit | Zero _ | Uninit _ | None_ -> true

View File

@ -407,6 +407,21 @@ let rep_stosb b = u8 b 0xf3; u8 b 0xaa
string instruction here has no such limit. *) string instruction here has no such limit. *)
let rep_stosd b = u8 b 0xf3; u8 b 0xab let rep_stosd b = u8 b 0xf3; u8 b 0xab
(* [bswap] on a 32-bit register: the byte reversal a computed [(dead-beef V)]
pattern needs, because V's ascending bytes are its big-endian ones and a
store leaves them little-endian. [Emit.word_of_pattern] is the same
operation folded at compile time for a literal. No REX.W this is the
32-bit form, and the 64-bit one would reverse eight bytes. *)
let bswap32 b ~dst =
rex b ~w:false ~r:0 ~x:0 ~m:dst; u8 b 0x0f; u8 b (0xc8 lor (dst land 7))
(* Logical shift right by an immediate, which the tail of a computed pattern
walks its bytes with. [shift_cl] above is the same opcode group taking the
count in [cl]; this one takes it in the instruction, so it does not need
[rcx] and [rcx] is exactly what [rep stosd] has just left at zero. *)
let shr_imm b ~dst ~n =
rex b ~w:true ~r:0 ~x:0 ~m:dst; u8 b 0xc1; modrm_r b ~r:5 ~m:dst; u8 b n
(* ── SSE ─────────────────────────────────────────────────────────────── *) (* ── SSE ─────────────────────────────────────────────────────────────── *)
let sse_rm b ~pfx ~op ~r ~mm = mem_op b ~r ~op:[ 0x0f; op ] ~w:false ~pfx:[ pfx ] ~mm let sse_rm b ~pfx ~op ~r ~mm = mem_op b ~r ~op:[ 0x0f; op ] ~w:false ~pfx:[ pfx ] ~mm
@ -1609,7 +1624,7 @@ and lower_at f (e : Tast.expr) (dst : loc) : unit =
value straight into the place's location, so filling a place and filling value straight into the place's location, so filling a place and filling
a temporary are already the same line. *) a temporary are already the same line. *)
| Tast.Fill (ty, b) -> fill_value f dst ty b | Tast.Fill (ty, b) -> fill_value f dst ty b
| Tast.Sentinel ty -> sentinel_value f dst ty | Tast.DeadBeef (ty, pat) -> dead_beef_value f dst ty pat
| Tast.None_ -> zero_value f dst t | Tast.None_ -> zero_value f dst t
(* Reading an uninitialised value gives whatever the slot held: stable (* Reading an uninitialised value gives whatever the slot held: stable
garbage rather than LLVM's [poison]. The one construct where the two garbage rather than LLVM's [poison]. The one construct where the two
@ -2144,24 +2159,48 @@ and fill_value f (dst : loc) (ty : Types.t) (b : Tast.expr) =
rep_stosb f.b rep_stosb f.b
end end
and sentinel_value f (dst : loc) (ty : Types.t) = and dead_beef_value f (dst : loc) (ty : Types.t) (pat : Tast.expr) =
let n = sizeof f.md ty in let n = sizeof f.md ty in
let words = n / 4 and tail = n mod 4 in let words = n / 4 and tail = n mod 4 in
if words > 0 then begin (* Whether the word is known here. A literal is reversed at compile time and
addr_into f ~reg:rdi dst; loaded as an immediate, which leaves the default's code exactly what it
movabs f.b ~dst:rax was before the pattern became an operand; anything else is evaluated and
(Int64.logand (Int64.of_int32 Emit.sentinel_word) 0xFFFFFFFFL); reversed by [bswap]. The evaluation happens first in both cases, because
movabs f.b ~dst:rcx (Int64.of_int words); it is an arbitrary expression that may call and a call clobbers [rdi]. *)
rep_stosd f.b let folded =
end; match pat.Tast.e with
List.iteri | Tast.Int (v, _) -> Some (Int64.to_int32 v)
(fun k byte -> | _ -> None
if k < tail then begin in
movabs f.b ~dst:rax (Int64.of_int byte); let load_word () =
store_int f.b ~src:rax match folded with
~mm:(lmem f (shift dst (words * 4 + k)) ~scratch:r11) ~size:1 | Some v ->
end) movabs f.b ~dst:rax
Emit.sentinel_bytes (Int64.logand (Int64.of_int32 (Emit.word_of_pattern v)) 0xFFFFFFFFL)
| None ->
let l = eval f pat in
load_int f.b ~dst:rax ~mm:(lmem f l ~scratch:r11) ~size:4 ~signed:false;
bswap32 f.b ~dst:rax
in
if n > 0 then begin
load_word ();
if words > 0 then begin
addr_into f ~reg:rdi dst;
movabs f.b ~dst:rcx (Int64.of_int words);
(* [rep stosd] touches rdi and rcx and leaves rax alone, which is what
lets the tail below keep reading the word out of it. *)
rep_stosd f.b
end;
(* The tail is the first up-to-three bytes of the stored word, lowest
address first byte k is (word >> 8k) & 0xFF either way. A folded word
could store constants instead, but walking [rax] covers both with one
sequence, and the count is at most three. *)
for k = 0 to tail - 1 do
if k > 0 then shr_imm f.b ~dst:rax ~n:8;
store_int f.b ~src:rax
~mm:(lmem f (shift dst (words * 4 + k)) ~scratch:r11) ~size:1
done
end
and zero_value f (dst : loc) (ty : Types.t) = and zero_value f (dst : loc) (ty : Types.t) =
if is_agg ty then zero_loc f dst (sizeof f.md ty) if is_agg ty then zero_loc f dst (sizeof f.md ty)

View File

@ -1,16 +1,22 @@
;;;; (filled b) and (sentinel-filled) — the two byte fills, read back as bytes. ;;;; (filled b) and (dead-beef), the two byte fills, read back as bytes.
;;;; ;;;;
;;;; The whole point of this program is that every row is a *byte* and not a ;;;; The whole point of this program is that every row is a *byte* and not a
;;;; value: the sentinel's contract is "a hex dump reads DEADBEEF", which is a ;;;; value: the pattern fill's contract is "a hex dump reads the pattern left
;;;; claim about which byte lands at which address, and only reading the bytes ;;;; to right", which is a claim about which byte lands at which address, and
;;;; back in address order can check it. The u32 rows are the same claim from ;;;; only reading the bytes back in address order can check it. The u32 rows
;;;; the other side — a little-endian load of DE AD BE EF is 0xEFBEADDE, which ;;;; are the same claim from the other side — a little-endian load of
;;;; is 4022250974, so a backend that wrote the word the other way round would ;;;; DE AD BE EF is 0xEFBEADDE, which is 4022250974, so a backend that wrote
;;;; print 3735928559 here and be caught. ;;;; the word the other way round would print 3735928559 here and be caught.
;;;; ;;;;
;;;; The lengths are chosen for the tail. 8 is a whole number of patterns, 9 ;;;; The lengths are chosen for the tail. 8 is a whole number of patterns, 9
;;;; ends on DE, 6 ends on DE AD, and 7 ends on DE AD BE — the three truncated ;;;; ends on DE, 6 ends on DE AD, and 7 ends on DE AD BE — the three truncated
;;;; endings and the one that is not truncated at all. ;;;; endings and the one that is not truncated at all.
;;;;
;;;; The rows that matter most are the last group: a pattern that is *not* a
;;;; literal, over a length that is not a multiple of four. That is the case a
;;;; constant-only implementation would pass by accident — the word reaches
;;;; the loop in a register and the tail bytes have to be shifted out of it
;;;; rather than folded, on both backends.
(defstruct Words [a u32 b u32]) (defstruct Words [a u32 b u32])
;;;; A computed global initialiser: a fill is never a constant the linker can ;;;; A computed global initialiser: a fill is never a constant the linker can
@ -21,6 +27,19 @@
(defn bytes4 [b [4 u8]] () (defn bytes4 [b [4 u8]] ()
(dotimes [i 4] (print (at b i)) (print " "))) (dotimes [i 4] (print (at b i)) (print " ")))
;;;; A u32 that no literal rule can see through: (u32 0xBAADF00D) will not do,
;;;; because the cast checks its operand as an i32 first and 0xBAADF00D is not
;;;; one. A parameter's declared type is what makes the literal a u32, and it
;;;; is also what stops the checker folding it at the fill.
(defn u32of [x u32] u32 x)
;;;; The pattern crosses a call boundary, so nothing can fold it at the fill.
(defn beef7 [pat u32] ()
(let [a (array 7 u8)]
(set a (dead-beef pat))
(dotimes [i 7] (print (at a i)) (print " "))
(println "")))
(defn main [] i32 (defn main [] i32
;; One byte, repeated. 0xFF is the -1 fill a debug allocator wants. ;; One byte, repeated. 0xFF is the -1 fill a debug allocator wants.
(let [a (array 4 u8)] (let [a (array 4 u8)]
@ -43,30 +62,48 @@
;; Four bytes, ascending, with nothing truncated. ;; Four bytes, ascending, with nothing truncated.
(let [a (array 8 u8)] (let [a (array 8 u8)]
(set a (sentinel-filled)) (set a (dead-beef))
(dotimes [i 8] (print (at a i)) (print " ")) (dotimes [i 8] (print (at a i)) (print " "))
(println "")) ; 222 173 190 239 x2 (println "")) ; 222 173 190 239 x2
;; The bare form is the spelled-out default, and this is the row that says
;; so: the same eight bytes from (dead-beef 0xDEADBEEF).
(let [a (array 8 u8)]
(set a (dead-beef 0xDEADBEEF))
(dotimes [i 8] (print (at a i)) (print " "))
(println "")) ; identical to the row above
;; The three truncated tails. ;; The three truncated tails.
(let [a (array 9 u8)] (let [a (array 9 u8)]
(set a (sentinel-filled)) (set a (dead-beef))
(dotimes [i 9] (print (at a i)) (print " ")) (dotimes [i 9] (print (at a i)) (print " "))
(println "")) ; ... ends on 222 (println "")) ; ... ends on 222
(let [a (array 6 u8)] (let [a (array 6 u8)]
(set a (sentinel-filled)) (set a (dead-beef))
(dotimes [i 6] (print (at a i)) (print " ")) (dotimes [i 6] (print (at a i)) (print " "))
(println "")) ; ... ends on 222 173 (println "")) ; ... ends on 222 173
(let [a (array 7 u8)] (let [a (array 7 u8)]
(set a (sentinel-filled)) (set a (dead-beef))
(dotimes [i 7] (print (at a i)) (print " ")) (dotimes [i 7] (print (at a i)) (print " "))
(println "")) ; ... ends on 222 173 190 (println "")) ; ... ends on 222 173 190
;; A pattern of the program's own choosing, laid down left to right the same
;; way: 0xBAADF00D is BA AD F0 0D, which is 186 173 240 13.
(let [a (array 6 u8)]
(set a (dead-beef 0xBAADF00D))
(dotimes [i 6] (print (at a i)) (print " "))
(println "")) ; 186 173 240 13 186 173
;; The same pattern read as words rather than as bytes, which is what says ;; The same pattern read as words rather than as bytes, which is what says
;; which end the DE is at. ;; which end the DE is at.
(let [w (Words {})] (let [w (Words {})]
(set w (sentinel-filled)) (set w (dead-beef))
(print (.a w)) (print " ") (print (.b w)) (println ""))
(let [w (Words {})]
(set w (dead-beef 0xBAADF00D))
(print (.a w)) (print " ") (print (.b w)) (println "")) (print (.a w)) (print " ") (print (.b w)) (println ""))
;; A nested aggregate: a fixed array of structs is plain data all the way ;; A nested aggregate: a fixed array of structs is plain data all the way
@ -78,11 +115,30 @@
;; The globals. [gfill] was filled before main ran; [gsent] is filled here. ;; The globals. [gfill] was filled before main ran; [gsent] is filled here.
(dotimes [i 4] (print (at gfill i)) (print " ")) (dotimes [i 4] (print (at gfill i)) (print " "))
(println "") ; 65 65 65 65 (println "") ; 65 65 65 65
(set gsent (sentinel-filled)) (set gsent (dead-beef))
(dotimes [i 5] (print (at gsent i)) (print " ")) (dotimes [i 5] (print (at gsent i)) (print " "))
(println "") ; 222 173 190 239 222 (println "") ; 222 173 190 239 222
;; A fill in value position rather than as the value of a [set]: the same ;; A fill in value position rather than as the value of a [set]: the same
;; bytes, reached through a temporary instead of through the place. ;; bytes, reached through a temporary instead of through the place.
(bytes4 (filled 0xFF)) (println "") ; 255 255 255 255 (bytes4 (filled 0xFF)) (println "") ; 255 255 255 255
;; ── The computed pattern ──────────────────────────────────────────
;; None of these can be folded: the pattern is a parameter, or arithmetic
;; the checker does not evaluate. Seven bytes is one whole pattern and a
;; three-byte tail, so the tail bytes have to come out of the register the
;; word is in rather than out of a constant.
(beef7 0xDEADBEEF) ; 222 173 190 239 222 173 190
(beef7 0xBAADF00D) ; 186 173 240 13 186 173 240
(let [p (u32of 0xBAADF00D)
a (array 6 u8)]
(set a (dead-beef p))
(dotimes [i 6] (print (at a i)) (print " "))
(println "")) ; 186 173 240 13 186 173
;; A pattern that is genuinely computed, not merely held in a slot, and read
;; back as a word so the byte order of the runtime path is pinned too.
(let [p (u32of 0xBAAD0000)
w (Words {})]
(set w (dead-beef (bit-or p (u32of 0xF00D))))
(print (.a w)) (print " ") (print (.b w)) (println ""))
0) 0)

View File

@ -1256,20 +1256,42 @@ let () =
rows are the same four bytes read back as a u32 0xEFBEADDE and they rows are the same four bytes read back as a u32 0xEFBEADDE and they
are what a backend that wrote the word the other way round would fail. are what a backend that wrote the word the other way round would fail.
The three short rows are the truncated tails a size that is not a The three short rows are the truncated tails a size that is not a
multiple of four ends on. *) multiple of four ends on.
Rows four and five are identical on purpose: [(dead-beef)] and
[(dead-beef 0xDEADBEEF)] are the same node by construction, and this is
where that stops being a claim about the checker and becomes one about
the program.
The last four rows are the ones a constant-only implementation would
pass by accident. Their pattern arrives in a register a parameter, a
slot, and a [bit-or] the checker does not fold so the word is
byte-reversed at run time and the tail bytes are shifted out of it
rather than folded. 186 173 240 13 is 0xBAADF00D read left to right,
and the closing 233876922 is that same pattern's stored word, which is
the runtime path's byte order pinned against the literal path's the
identical number appears above it from a folded [(dead-beef
0xBAADF00D)]. *)
let fill_out = let fill_out =
"255 255 255 255 \n7 7 7 7 \n0 0 0 0 \n\ "255 255 255 255 \n7 7 7 7 \n0 0 0 0 \n\
222 173 190 239 222 173 190 239 \n\
222 173 190 239 222 173 190 239 \n\ 222 173 190 239 222 173 190 239 \n\
222 173 190 239 222 173 190 239 222 \n\ 222 173 190 239 222 173 190 239 222 \n\
222 173 190 239 222 173 \n\ 222 173 190 239 222 173 \n\
222 173 190 239 222 173 190 \n\ 222 173 190 239 222 173 190 \n\
4022250974 4022250974\n4294967295 4294967295\n\ 186 173 240 13 186 173 \n\
65 65 65 65 \n222 173 190 239 222 \n255 255 255 255 \n" 4022250974 4022250974\n233876922 233876922\n\
4294967295 4294967295\n\
65 65 65 65 \n222 173 190 239 222 \n255 255 255 255 \n\
222 173 190 239 222 173 190 \n\
186 173 240 13 186 173 240 \n\
186 173 240 13 186 173 \n\
233876922 233876922\n"
in in
outputs "byte and sentinel fills" "programs/fill.flan" fill_out; outputs "byte and dead-beef fills" "programs/fill.flan" fill_out;
outputs ~opt:"-O0" "byte and sentinel fills, -O0" "programs/fill.flan" outputs ~opt:"-O0" "byte and dead-beef fills, -O0" "programs/fill.flan"
fill_out; fill_out;
outputs ~x86:true "byte and sentinel fills, --x86" "programs/fill.flan" outputs ~x86:true "byte and dead-beef fills, --x86" "programs/fill.flan"
fill_out; fill_out;
(* dyn if: truthiness -- M2 queue item 7. A dyn scrutinee is tested for (* dyn if: truthiness -- M2 queue item 7. A dyn scrutinee is tested for
nil/false vs. everything else, Clojure's rule, on both backends; a nil/false vs. everything else, Clojure's rule, on both backends; a

View File

@ -1957,7 +1957,7 @@ let () =
~needle:"steers every read of it"; ~needle:"steers every read of it";
(* ── What may be filled with raw bytes ───────────────────────────── (* ── What may be filled with raw bytes ─────────────────────────────
[(filled b)] and [(sentinel-filled)] are [zeroed]'s siblings, and the [(filled b)] and [(dead-beef)] are [zeroed]'s siblings, and the
boundary is the whole of what is new about them: zero is a value every boundary is the whole of what is new about them: zero is a value every
type can have and 0xDE is not, so the checker says which types survive type can have and 0xDE is not, so the checker says which types survive
arbitrary bytes. The accepting side is programs/fill.flan; what is here arbitrary bytes. The accepting side is programs/fill.flan; what is here
@ -1969,12 +1969,19 @@ let () =
one is a root pointing at nothing. *) one is a root pointing at nothing. *)
accepts "a fixed array of numbers may be filled" accepts "a fixed array of numbers may be filled"
"(defn f [] () (let [a (array 4 u8)] (set a (filled 0xFF))))"; "(defn f [] () (let [a (array 4 u8)] (set a (filled 0xFF))))";
accepts "a struct of numbers may be sentinel-filled" accepts "a struct of numbers may be dead-beefed"
"(defstruct S [a i32 b f64]) \ "(defstruct S [a i32 b f64]) \
(defn f [] () (let [s (S {})] (set s (sentinel-filled))))"; (defn f [] () (let [s (S {})] (set s (dead-beef))))";
(* Both arities, and a pattern that is not a literal — the operand is an
ordinary u32 expression, which is the byte arm's rule at four times the
width. *)
accepts "dead-beef takes a pattern"
"(defn f [] () (let [a (array 4 u8)] (set a (dead-beef 0xBAADF00D))))";
accepts "dead-beef takes a computed pattern"
"(defn f [p u32] () (let [a (array 4 u8)] (set a (dead-beef p))))";
rejects_check "a struct holding a dyn cannot be filled" rejects_check "a struct holding a dyn cannot be filled"
"(defstruct S [a i32 d dyn]) \ "(defstruct S [a i32 d dyn]) \
(defn f [] () (let [s (S {})] (set s (sentinel-filled))))" (defn f [] () (let [s (S {})] (set s (dead-beef))))"
~needle:"a root pointing at nothing"; ~needle:"a root pointing at nothing";
rejects_check "a Vec cannot be filled" rejects_check "a Vec cannot be filled"
"(defn f [] () (let [v (vec-new i32)] (set v (filled 0xFF))))" "(defn f [] () (let [v (vec-new i32)] (set v (filled 0xFF))))"
@ -2009,8 +2016,8 @@ let () =
rejects_check "a fill in a position with no expected type" rejects_check "a fill in a position with no expected type"
"(defn f [] () (print (filled 0xFF)))" "(defn f [] () (print (filled 0xFF)))"
~needle:"needs to know the type it is filling"; ~needle:"needs to know the type it is filling";
rejects_check "a sentinel fill in a position with no expected type" rejects_check "a dead-beef in a position with no expected type"
"(defn f [] () (print (sentinel-filled)))" "(defn f [] () (print (dead-beef)))"
~needle:"needs to know the type it is filling"; ~needle:"needs to know the type it is filling";
(* The byte is a u8 and the ordinary literal rule applies to it — there is (* The byte is a u8 and the ordinary literal rule applies to it — there is
no range check of this builtin's own, and there does not need to be. *) no range check of this builtin's own, and there does not need to be. *)
@ -2020,9 +2027,16 @@ let () =
rejects_check "filled takes exactly one byte" rejects_check "filled takes exactly one byte"
"(defn f [] () (let [a (array 4 u8)] (set a (filled))))" "(defn f [] () (let [a (array 4 u8)] (set a (filled))))"
~needle:"takes 1 argument"; ~needle:"takes 1 argument";
rejects_check "sentinel-filled takes no argument" rejects_check "dead-beef takes at most one pattern"
"(defn f [] () (let [a (array 4 u8)] (set a (sentinel-filled 1))))" "(defn f [] () (let [a (array 4 u8)] (set a (dead-beef 1 2))))"
~needle:"takes 0 arguments, given 1"; ~needle:"takes the pattern or nothing at all, given 2";
(* The pattern is four bytes, so a wider literal is a typo rather than
something to truncate. The refusal is [in_range]'s, located at the
literal this builtin has no range check of its own and does not need
one, exactly as the byte arm does not. *)
rejects_check "a dead-beef pattern out of u32 range"
"(defn f [] () (let [a (array 4 u8)] (set a (dead-beef 0x1DEADBEEF))))"
~needle:"does not fit in u32";
(* A fill is never a value the linker can write into the image, so a (* A fill is never a value the linker can write into the image, so a
defconst of one is refused by the constant rule rather than by anything defconst of one is refused by the constant rule rather than by anything
of this feature's own. A defvar is fine: its initialiser runs at of this feature's own. A defvar is fine: its initialiser runs at