yaffle IR: semantics
The types live in packages/compiler/src/ir/ir.ts. This document says what they mean. It is the
contract between the frontend (which produces IR) and the backends (which turn IR into code).
Both sides must follow it.
1. What the frontend has already done
- Monomorphized. Generic structs are instantiated per distinct type argument list, type aliases
are inlined, constants are folded, and
imports are resolved. Every struct referenced anywhere is inprogram.structs. - Lowered scoping. Bound values (
$version) and lexical references to an enclosing struct’s fields become implicit parameters (origin: "bound" | "outer"). Each use site passes them explicitly inIrStructType.args.$endianis the implicitendianparameter (origin: "endian", value type{ kind: "endian" }). A struct withusesEndian: falsemay ignore it.- Parameter order.
IrStruct.paramsis always:endian(defaultle) first, then the declared parameters in declaration order (explicit, and declared$parameters with originbound), then implicitboundparameters sorted by name, then implicitouterparameters sorted by owner and field. Every struct has theendianparameter, even withusesEndian: false. - Implicit names. A lowered
$versionis the parameterversion(boundName: "version"), suffixed with_boundif a declared parameter already has that name. An enclosing struct’s fieldcountis the parametercount(originouter), suffixed with_outeron a collision. A use site passes a field of the current struct if it binds the value before the use (or is the enclosing struct), and the current struct’s own implicit parameter otherwise.
- Parameter order.
- Resolved derivation. Every value the writer computes (derived, computed and constant
fields) has a
deriveexpression, except the bytes of streams and slice carriers (§3.7). - Normalized expressions.
- Constants are folded where the operands are literals, including enum and const references and
sizeof(Type)of fixed-size types. - A character code assigned to or compared with an integer whose endianness is inherited lowers
to
cond($endian == be, <big-endian value>, <little-endian value>). With fixed endianness it is a literal. Such a field is stillderiveKind: "constant", with a checkit == <same expr>. - Arithmetic (
+ - * / % << >> & | ^, unary-and~) always has typebigintorfloat(f64), and both operands have exactly the result type: the frontend insertsconverton integer fields. Comparisons don’t convert when both sides already have the same type. An integer literal compared with an int field takes that field’s type (version >= 3compares twou32). - String lengths are never
len(<string>). Derived string lengths ands.lengthuseencodedLength(s, unit, encoding), divided byunitfor 2- and 4-byte units.lenis used for arrays and bytes. x == nullisisNull(x)andx != nullis!isNull(x). There is no null literal.
- Constants are folded where the operands are literals, including enum and const references and
- Inferred value types. The value type of an integer
transformis the smallest standard integer type (u8/u16/u32/u64, i8/…/i64) holding the read expression’s static range (interval analysis); unbounded or beyond 64 bits givesi64(u64if non-negative). The read expression’s owntypestaysbigint. Untyped constants (const N = 4) get a type the same way. - Complete bit units. Every
IrBitsunit’s fields cover the whole storage unit: bits not declared become an implicit reserved field (name: null), so strict checks are well defined. - Checked: names, types, bindings on every path, layout paths, invertibility, and layout cycles. A program that reaches a backend is valid.
2. Reading
2.1 Context
A read happens in a context with:
buffer / source | the bytes (sync or async, random access) |
pos | absolute position of the next inline byte |
base | absolute position of the current base. The file start (0) at the root, changed by @base structs |
end | absolute end of the current region: the source size at the root, narrowed by @size layers and by the extent of a codec’s decoded output |
index | the index of the element being read, inside arrays |
strict | whether strict checks are on (root option, struct strict, or a strict layer) |
| params | the struct’s parameter values |
$offset = pos - base, $end = end - base, $index = index. All offsets in expressions
are relative to the base. Absolute positions never appear in values.
2.2 Members in order
Members are read in declaration order. Each field’s value becomes visible to later expressions under its name. Blocks and switches don’t introduce a scope: their fields are visible after them, as if declared inline. A field from a branch not taken is “absent”: reading it in an expression is a frontend error unless the expression is guarded. The frontend guarantees this.
- Inline field: read its type at
posand advanceposby the bytes consumed. placement: evaluateoffsetand read the type atbase + offsetwithout movingpos. The region’sendstays the same. WithemptyIsNull(an array target), a stored offset of 0 reads as an empty array.from: build the stream (§2.8) and read the type from it as a fresh region (base = 0, end = stream length).posdoesn’t move.virtual: the field occupies no bytes. Evaluatevirtual.read(typebigint, earlier fields only) and range-check the value against the field’s declared type, which is an integer prim (RANGE, like any typed location, §6). Nothing is read with the prim.- Checks: after reading the value, evaluate every check with
it= the value. A false check is aCHECKerror (CONFORMANCE.md §Errors). Strict-only checks run only in strict mode.
2.3 Types
| Type | Read |
|---|---|
prim | little/big endian per endian (inherit = the endian param). u24…i56 are N-byte integers; signed values are two’s complement and sign-extended. f16 is IEEE half. |
bool | one byte; nonzero → true (strict: anything other than 0 or 1 is a CHECK error). |
string | see the doc comment in ir.ts. Invalid UTF-8/16/32 is a CHECK error. ascii rejects bytes ≥ 0x80. latin1 maps bytes 1:1 to U+0000–U+00FF. |
bytes | count bytes, bytes until a terminator byte, or bytes up to end. |
array | elements per length. until: read an element, stop if it equals the terminator (compared as values). before: stop before an element that would equal the terminator; the terminator is not consumed. untilPredicate: read an element, stop if the predicate holds for it (it = the element); that element is included. rest: read elements while pos < end. If an element would cross end, that’s an EOF error. With elementPlacement, element i is read at base + elementPlacement($index = i), and with rest, reading stops when that offset is ≥ $end. Elsewhere, $index is the index in the nearest enclosing array. |
struct | evaluate args in the caller’s context, then read the struct’s members. If the struct is base, set base = pos (or the placement address) for its contents. Struct-level layers wrap the struct (they apply inside any use-site layers). |
enum | read the storage prim. An unknown value is a CHECK error unless open; open unknowns are returned as plain integers. |
pointer | read storage. Equal to nullValue → null. Otherwise target address = base + offset (owned and ref alike), or fieldAddress + offset if relative, where the offset is signed; with origin, it is base + origin + offset, where origin is evaluated like a computed field (§3.2: it may use offsetof of any field of the struct, also later ones, and offsetof(this)). Read the target there without moving pos. The target may be an array (T[n] *p). With emptyIsNull (always with nullValue: "0"), the target is an array and a stored 0 reads as an empty array, so the value is never null. Identity: within one parse, the same target address and the same target type yield the same value object. |
extern | call host size(view, ...args) with a view starting at pos. undefined means “need more bytes”: give a longer view, and at end that’s an EOF error. Then read(bytes[0..size], ...args). With fixedSize, skip size(). |
union | try each variant at pos. The first that reads without error (checks included) wins. If none does, that’s a UNION error listing each variant’s error. |
transform | read inner, then evaluate read with it = the inner value. |
layered | apply the layers from outermost (last) to innermost (first) around reading inner (§2.4). |
2.4 Layers on read
Processed from the outermost layer inwards:
| Layer | Read behaviour |
|---|---|
align(to) | skip forward until (pos - origin + offset) % to == 0. Strict: the skipped bytes must equal the fill. |
padBefore(n) / padAfter(n) | skip n bytes before or after the inner content. |
padUntil(size) | read the inner content, then skip until it occupies size bytes. Content larger than size is a CHECK error. |
alignEnd(to) | after the inner content, skip until (pos - origin + offset) % to == 0. |
size(n) | narrow end to pos + n, read the inner content inside it, then set pos to the window end. The leftover is padding (strict: must be zero). |
via(codec, args) | take the encoded bytes: the window [pos, end), unless the codec is streaming, in which case it reports how many bytes it consumed. Decode them and read the inner content from the decoded bytes as a fresh region (base = 0, end = decoded length). Then pos advances by the encoded length. If an inner size layer exists, its value is passed to the codec as outputSize. |
endian(v) | evaluate v (outside the layer) and use it as the endian argument for everything inside. |
strict | turn on strict mode inside. |
- Alignment origin.
alignandalignEndmeasure from the current base by default. Withfrom: "file"they measure from the start of the current byte space: the root source, a codec’s decoded region, or a stream, i.e. whatever$offsetwould count from if there were no@basein between.offset(default 0) aligns the position that many bytes ahead. The frontend emitsfromonly as"file"andoffsetonly when it isn’t 0. - Strict padding. A padding layer (
align,alignEnd,padBefore,padAfter,padUntil) withstrict: truechecks its own fill on read even outside strict mode. It may appear on field, block and struct layers. - Endian layers rebind
$endian: inside one, bothendian: "inherit"and every{ kind: "param", name: "endian" }expression, including theendianargument passed to nested structs, mean the layer’s value.
Padding fill: { kind: "byte" }, or { kind: "fn" } evaluated per byte index (0-based within that
padding run), keeping the low 8 bits.
2.5 Bits
Read the storage unit (prim, endianness per endian). Field value = (unit >> shift) & mask(width),
sign-extended for signed prims, converted for bool/enum. Reserved fields (name: null) must be
zero in strict mode.
2.6 Switch and blocks
- Block: evaluate
condition. If true, readmembers, otherwiseelse(if any). Applylayersaround the members. - Switch: evaluate the discriminant, then read the members of the first case with an equal
literal, else
default. If nothing matches and there is no default, that’s aCHECKerror.
2.7 Errors
Every error carries a path (field names and array indices from the root) and an absolute offset. Codes are in CONFORMANCE.md.
2.8 Streams
field: that bytes field’s value (the decoded bytes).each: the bytes field of every element of the array field, joined.from.splitis present only foreachstreams.slice:[start, end)of another stream.
Lazy implementations may decode pieces on demand. If every piece has a fixed decoded size, or the sizes are known from fields, an offset maps directly to a piece.
3. Writing
3.1 Input values
The writer takes an input value shaped like the generated input type: every field with
input: "required", optionally fields with input: "optional", and never fields with
input: "absent". If an absent field is present in the value, it is ignored. That way
serialize(parse(bytes)) works directly.
The field kinds of DESIGN.md §1 map to these properties: an input field is
input: "required" (with a derive only when it is normalized); a derived field has
deriveKind: "auto" (stream split points are also input: "optional"), except that fields
consumed by a stream and slice carriers are input: "absent" without a derive (§3.7); a
computed field has deriveKind: "formula" and input: "absent"; a constant has
deriveKind: "constant" and input: "absent"; an optional field has its default as
derive and input: "optional".
- A
derivethat usesitnormalizes the input (u32 flags = it | 0x80):itis the field’s input value, the field staysinput: "required", and the writer stores the value ofderive. Aderivewithoutitcomes withinput: "absent"or"optional". - A default (
u32 align ?= 0x100) isinput: "optional"with the default asderive,deriveKind: "formula"and no check. - A virtual field’s value comes from the input (
required, oroptionalwith its default asderive). It is inverted on write, so its parts are derived from it: theirderiveexpressions use>>,&,/,%,+and-on the virtual field’s value, withderiveKind: "auto"andinput: "absent"(parts may be bit fields). A virtual field is never a part of another one. After writing,virtual.readmust give back the input value, otherwise it’s aDERIVEerror.
3.2 The write context and derive
derive expressions are evaluated in the write context:
fieldreferences read the input value, or the value ofderivefor fields that have one. Such fields may reference each other as long as there is no cycle (the frontend checks this).sizeof/encodedSize/bytesof/encodetake the field’s final encoding.offsetoftakes the field’s final position (from the base).$indexis the element index, and params are as on read.
A derived field’s final value can depend on positions that are only known after layout, e.g.
u64 tableOfs derived from offsetof(table). Recommended writer shape (relocation style):
- Encode bottom-up into nodes: inline bytes plus a list of fixups (position, width, endianness, closure computing the value from final sizes and positions) plus a list of placeable targets.
- Run layout (§4) to assign every target a position.
- Evaluate and patch the fixups.
- Concatenate.
The frontend rejects layout cycles, i.e. a derived value whose own size depends on a position. Variable-size derived fields (varints, extern types) that feed into positions are a compile error.
3.3 Consistency
After writing, every field with a derive must satisfy its checks. The read-side expressions must
agree with what was written: an array written with N elements whose length expression evaluates to
M ≠ N is a DERIVE error, and so is a constant that would read back differently. A transform with
write must round-trip: read(write(v)) == v, or it’s an INPUT error. Backends may skip the
round-trip check for float transforms when the difference is below the half-ulp of the storage.
An untilPredicate array must end with an element satisfying the predicate, and no earlier
element may satisfy it. A before terminator is not written.
3.4 Pointers on write
- owned: the target is a placeable in the current base (the nearest enclosing
@basestruct, or the root). The pointer value is computed in a fixup:offset = targetPos - base(relative:targetPos - fieldPos; withorigin:targetPos - base - origin), and a null value writesnullValue. WithemptyIsNull, an empty target array writesnullValue(0); the same holds for the offset of aplacementwithemptyIsNull. - Identity: two pointers to the same object (reference identity in the input) share one
target. Deduplicating equal values happens only for
refpointers. - ref: the pointer gets the offset of the input object itself if it was placed, otherwise of an
already-placed, equal value of the same type: an element of an inline array, a field, or
another target. Equality is equality of encodings. The current base is searched first, then the
enclosing ones. If no such value exists, that’s a
REFwrite error. When several match, the first placed wins.
3.5 Layers on write
These mirror reading:
- size: written as the derived value (the frontend has derived the size field from
encodedSize). A size that doesn’t match the actual inner length is aDERIVEerror, unless the inner content is shorter and the remainder is padding. - via: encode the inner bytes with the codec. With an inner
sizelayer, passoutputSize. - padUntil: content larger than the size is an error.
- Fill bytes come from
fill.
3.6 Codec reuse (byte-exact rebuilds)
If a value came from a parse (same process, tracked by object identity) and the decoded content
of a via region is unchanged, the writer reuses the original encoded bytes instead of
re-encoding. Views get this naturally. The same applies to from pieces.
3.7 Streams on write
from field: the consumed bytes field isinput: "absent"and derived from the field read from it. It stays an output field unlesshidden.from arr[*].piece: in the element struct ofarr,pieceisinput: "absent"(its content comes from the stream), and fields whosederivemeasures it (encodedSize(piece, i)) areinput: "optional": split points, used when given for byte-exact rebuilds, otherwisefrom.splitdecides.- Slice carriers (
carrier: "slices"): a bytes field read only throughfromslices of itself (T x from raw[a..b]) isinput: "absent". On write it is a zero-filled buffer of the field’s own length (or, forrestlengths, the largest slice end) with every slice field’s encoding written at its start. Overlapping slices must produce identical bytes, otherwise it’s aDERIVEerror. Carriers are only single-level slices of afieldstream, and their length is a constant, an input-only expression, orrest: the frontend never marks a carrier whose length or slice bounds depend on the carrier itself.
4. Layout: placing pointer and at targets
4.1 Placeables
Within a base, a placeable is the target of an owned pointer or a field with placement
(unless fixed). Each placeable has a unit: its own inline bytes (“this”) plus the units of
its own placeables, ordered by its struct’s layout (default: this first, then its placeables in
declaration order, recursively, i.e. pre-order depth-first). A nested @base struct’s unit is
self-contained: its placeables are laid out inside its own region.
A placement is fixed: false exactly when its offset expression only uses fields that the writer
computes (derived or computed fields); constants, parameters and input fields make it fixed.
constraint comes from derived linear offsets (at sector * 0x800 → modulus 2048, remainder 0).
4.2 Base region
[base inline bytes][placeables per the base struct's layout]. A base’s this is always first,
because offsets count from it.
4.3 Layout items
Each placeable is placed at most once. The rules below are what the TS runtime’s layout.ts
implements. The other runtimes port it, and the language service’s layout simulator
(layoutSuggest.ts) mirrors it.
- Claims first. Before anything is placed, every placeable named by any item of a
container’s layout is claimed by that item and removed from every other unit, even when an
earlier
x[*]item would otherwise reach it (pulling groups out). - Then items in order:
- A path naming a placeable field (
curves,records[*].model) places the unit of each placeable it matches. - A path naming inline data or
[*]elements (records[*]where elements are inline in therecordstarget) places the remaining units of all placeables reachable from those elements, in their own layout order. thisplaces the struct’s own inline bytes.- A group (
volumes[*] { … },IrLayoutItem.group) runs per element of the array itspathnames (the path has no trailingeach), in order. If the element is a placeable (a pointer oratelement target), its own bytes come first unless a group item namesthis(as in a unit, rule 3); then the group’s items, relative to the element. Everything the nested items name is claimed by the group. Targets of elements the group doesn’t name are leftovers.
- A path naming a placeable field (
thiscomes first in a unit unless an item names it.- Leftovers (placeables no item placed) follow the items in default order.
- Empty targets (empty arrays and strings) are placed at the current position after alignment, and a writer accepts any stored offset for them.
- Fixed placements go exactly where their offset says. When layout reaches one, the running position moves to its end if that is further on.
4.4 Alignment and constraints
- Item layers (
align) apply to every structure placed by that item, unless a nested layout item specifies its own. Group layers apply to nested items without layers of their own. - Placement constraints (
constraint) add an alignment-like requirement: the writer moves the position forward to the next value ≡ remainder mod modulus. - Gaps are filled with zero.
Layout items that read fields. Arguments of a layout item’s layers (@align(align), @align(1 << shift)) are evaluated in the write context of the struct that owns the layout: its fields
(input values, or their derive), its parameters and $index where applicable, the same as its computed
fields. They never see fields of the placed target, so recs[*].body @align(count * 2) uses the
container’s count. Any alignment, constant or not, must be at least 1; 0 or a negative value
is a RANGE error, even when the item places nothing (arguments are evaluated per instance, like
computed fields). Arguments can’t depend on sizes or positions (sizeof, offsetof). Conformance:
conformance/cases/layout-fields.
4.5 Fixed placements and overlaps
A fixed placement goes exactly at the given offset, and overlapping another placed byte range is
an OVERLAP error. The same applies to elementPlacement arrays: element i goes at its computed
offset, and must not overlap element i+1.
5. Codecs, extern functions and extern types (host side)
Host implementations are found through yaffle.json targets.<lang>.externs[name] (a module
path per target). Shapes in TS:
// extern fn pathHash(char path[]) -> u32
export function pathHash(path: string): number;
// extern codec zstd / extern async codec oodle / … @ranged
export const zstd = {
decode(input: Uint8Array, ctx: { args: unknown[]; outputSize?: number; position?: number }): Uint8Array, // or Promise for async
encode(input: Uint8Array, ctx: { args: unknown[]; position?: number }): Uint8Array,
};
// @ranged: decode/encode may be called on any slice; ctx.position is the slice's offset within the encoded region.
// @streaming: decode returns { output: Uint8Array; consumed: number } and gets the rest of the region as input.
// extern type VarInt(u8 maxBytes) : u64
export const VarInt = {
size(bytes: Uint8Array, maxBytes: number): number | undefined,
read(bytes: Uint8Array, maxBytes: number): bigint,
write(value: bigint, maxBytes: number): Uint8Array,
};
Built-in extern types: uleb128 (value u64) and sleb128 (value i64) have builtin: true,
module: "" and exactly those ids. Every runtime implements them; they need no externs entry.
Built-in codecs that need no host: none. Built-in functions (crc32, adler32, …) are implemented
by each runtime.
6. Expressions: integers, floats and built-ins
- Exact arithmetic. Integer expression arithmetic is mathematically exact: no wraparound. A
result is checked when it lands in a typed location (array length, offset, field, argument). Out
of range is a
RANGEerror. - Division truncates toward zero.
%takes the sign of the dividend. Division by zero is aRANGEerror. - Shifts: the shift count must be in 0..63.
>>is arithmetic on negative values. - Bitwise operators (
& | ^ ~) work on the 64-bit two’s complement representation. - Mixed int/float arithmetic converts to f64 (the checker inserts
convert). roundrounds half away from zero.- Comparisons across int widths compare mathematical values.
- Equality of composite values.
==and!=on arrays, strings and bytes compare by value, element-wise; bytes may be compared with an array of integers (element-wise, as numbers). On structs they compare canonical JSON equality. - TS representation: values of up to 32 bits (and u40/u48 when ≤ 2^53) are
number; 64-bit fields arebigint. Generated code may compute innumberwhen it can prove the range stays below 2^53, and must usebigintotherwise.
Built-ins beyond the obvious:
encodedLength(s, unit, encoding)returns bytes, sochar16 t[n]derivesn = encodedLength(t, 2, "utf16") / 2.thisis only the argument ofsizeof(write context only; for a@basestruct its whole region including targets and@alignEnd),offsetof(the struct’s start from the base),bytesofandencode. Its type is the current struct.sum(arr)is the sum of an array of numbers (0 when empty), of typebigint(f64 for floats).min/maxwith one array argument give its smallest/largest element (the element type); an empty array is aRANGEerror.lengths(arr)is the element count of each element of an array of arrays (pointer targets are transparent), as an array: the derivation ofu16[lens[$index]] *lists[9] : u64, where$indexinside the element type is the index inlists.isNull(x)takes one argument of anullabletype.- Derived arrays.
map,filter,indicesWhereandsortedBytake an array and a{ kind: "lambda" }as their second argument (the only place a lambda appears; its body sees the enclosing context).uniquetakes an array,concatone or more. Result types:map→ an array of the lambda body’s type;filter,sortedBy,unique→ the input’s type (bytes stay bytes);indicesWhere→ an array ofbigint;concat→bytesif every input is bytes, else an array of the unified element type. A derive or check operand of array type assigned to (or compared with) a bytes field converts element-wise, range-checked (RANGE).sortedBykeys compare as numbers, strings by code points, or bytes lexicographically, and the sort is stable.
7. Generated API (every backend)
For each root: parse, parseAsync (if async or for async sources), serialize,
serializeAsync, safeParse, view (tier 2), toJson, fromJson. Names are adapted to each
language’s conventions (Rust: parse, serialize, to_json, …). Canonical JSON is defined in
CONFORMANCE.md. Root parameters (explicit + bound + endian) become parse/serialize options.
Output and input types
- Output type: every field with
output: true.- Fields of conditional blocks without else are optional.
- if/else blocks and switches become discriminated unions where a discriminant field exists (TS: a union of object types). Otherwise the fields from both branches are optional.
- Pointers have the target’s type, plus null if nullable.
- Unions are
{ type: "<Variant>", value }.
- Input type: the same, minus
absentfields, withoptionalfields optional.
8. Worked examples
yaffle ir file.yfl prints the IR for real sources. These sketches show the shapes.
8.1 Derived count
struct Table { u32 count Entry entries[count] }
{ "kind": "field", "name": "count", "type": { "kind": "prim", "prim": "u32", "endian": "inherit" },
"derive": { "kind": "call", "callee": { "kind": "builtin", "name": "len" },
"args": [{ "kind": "field", "name": "entries", "type": { "kind": "array", "element": { "kind": "struct", "struct": "Entry" } } }],
"type": { "kind": "bigint" } },
"deriveKind": "auto", "checks": [], "hidden": false, "output": true, "input": "absent" }
{ "kind": "field", "name": "entries",
"type": { "kind": "array", "element": { "kind": "struct", "struct": "Entry", "args": [{ "kind": "param", "name": "endian", "type": { "kind": "endian" } }] },
"length": { "kind": "count", "expr": { "kind": "field", "name": "count", "type": { "kind": "int", "bits": 32, "signed": false } } } },
"checks": [], "hidden": false, "output": true, "input": "required" }
8.2 Endianness block
struct TiffHeader { char order[2] in ("II", "MM") @endian(order == "MM" ? be : le) { u16 magic = 42 u32 firstIfd } }
This gives a block with layers [{ kind: "endian", value: cond(order == "MM", be, le) }]. magic
has derive: 42, deriveKind: "constant", a check it == 42, and input: "absent". order has
literals: ["II", "MM"], an in check, and input: "required".
8.3 Layered sizes
struct Packed { u32 expSize u32 compSize Asset body @size(expSize) @via(oodle) @size(compSize) }
body.typeislayered(struct Asset, [size(field expSize), via(oodle), size(field compSize)]).expSize.deriveisencodedSize(body, 0)andcompSize.deriveisencodedSize(body, 2).program.structshasPacked.async = truebecauseoodleis async.
8.4 Bound value lowering
struct Asset { u32 $version Record r }
struct Record { Motion m }
struct Motion { if ($version >= 144) { u32 extra } }
Motion.paramsgets{ name: "version", origin: "bound", boundName: "version", type: u32 }, andRecord.paramsgets the same, becauseRecordpasses it through.- In
Asset, the fieldrhas type{ kind: "struct", struct: "Record", args: [endian, field version] }. - In
Record,mpassesparam version.
8.5 at with a derived linear offset
u32 sector u8 data[size] at sector * 0x800
sector.deriveisoffsetof(data) / 0x800.data.placementis{ offset: sector * 0x800, constraint: { modulus: "2048", remainder: "0" }, fixed: false }.