4. Compilation Model
4.1 Phase evaluation
Section titled “4.1 Phase evaluation”Write commands flat and in any order. The compiler assigns each one to a phase and evaluates the phases in this fixed order:
massing floor / walls / volumeenvelope roof / stairopenings door / windowfixtures sign / painting / frame / bed / sensors / actuatorslogic_synth redstone: Logic IR → Netlist IRlogic_place cell placementlogic_route routing → Placement IR, delay determinedraw escape hatchA window written after roof is still cut as an opening in the wall. Source order never decides
what a member means — nor, per §4.8, what the artifact
is: permuting two members that share no voxel leaves the whole BlockArray equal, palette
numbering included, and not only the voxels.
circuit marks a routing region and writes no voxel, so it belongs to no phase. The three
logic_* phases follow fixtures because port coordinates are not fixed until sensors and
actuators are placed in 3D. See Redstone.
Last-wins applies only to local overrides within the same phase, and raw always runs last. Two
different members contesting a voxel inside a phase resolve the same way and are reported. See
§4.8.
struct keep size=11x9floor id=base mat_slot=floorwalls id=shell mat_slot=wall height=5roof id=roof kind=gable mat_slot=roof overhang=1window id=front_windows side=front y=2 offset=2 size=2x2 mat_slot=glass # still cut as an openingdoor id=entry side=front at=center4.2 Target axes
Section titled “4.2 Target axes”The target is the pair (edition, version). Neither is written in the source; only the backend
knows them. See Versioning and Editions.
cairn compile build.crn --edition java --target 1.21.4cairn compile build.crn --edition bedrock --target 1.21.40--edition is required and --target alone is refused. “1.21” means different things on Java and
Bedrock, and Java’s DataVersion has nothing to do with Bedrock’s block_version.
4.3 Gable roof voxel rules
Section titled “4.3 Gable roof voxel rules”roof kind=gable [overhang=N] mat_slot=... lowers to two opposite stair slopes meeting at a ridge.
The four roof kinds (gable, shed, hip, flat) share the overhang and wall-top conventions
below. Their layouts are this section and the three that follow. overhang= is read whatever the
kind; slope_to= belongs to shed alone, and written on any other kind it is read by nothing and
reported as W_IGNORED_ARGUMENT (Lint §11.3).
Material. A sloped roof takes its material from mat_slot= and it MUST be in the stair family
— an id whose path ends in _stairs. The geometry attaches facing, half, and shape to
whatever it paints, and a whole block cannot carry them. A binding outside the family is
E_INCOMPATIBLE_MATERIAL and stops the build. With no mat_slot= at all the roof falls back to
minecraft:spruce_stairs. The registry pack’s four roof species (roof.dark_wood,
roof.light_wood, roof.warm_wood, roof.cool_wood) all resolve inside the family.
A binding inside the family that carries blockstates of its own keeps its id and loses those states
to the geometry, with W_DEFERRED_MEMBER. An eave stair kind=stairs follows the same material
rule, but takes its states from its own arguments.
Those arguments are facing=out | in, half=top | bottom and
shape=straight | outer_left | outer_right, each a bare identifier, defaulting to out, top and
straight. An identifier outside the list is not yet supported, and the stair is deferred with
W_DEFERRED_MEMBER. A value of another shape (half="bottom", facing=1) is an unreadable value:
the stair is built with that key’s default, and the value is reported as W_IGNORED_ARGUMENT —
also when the stair is refused for another reason, with a note saying it is not built either way
(Lint §11.3).
Ridge axis. The ridge runs along the long horizontal axis of the footprint. A square footprint
(size=WxW) ties to x, giving an east-west ridge.
Ridge height. A gable rises ceil(short_span / 2) voxels above the wall top, where
short_span is min(dims.x, dims.z) after the overhang inflation.
Layers. Layer 0 seats on the wall top and is a pair of slope rows, or one row when
short_span is 1 and the two converge. Each layer above steps inward by one on each side. The
topmost layer is the apex:
- odd span: one
half=topstair on the centre row. - even span: two
half=topstairs on the adjacent meeting rows, so the ridge leaves no open V.
A short_span of 1 or 2 rises exactly one layer, which is layer 0, and has no apex course.
Overhang. overhang=N inflates the voxel grid by N on both horizontal axes
(Dims.x = size.w + 2N, Dims.z = size.h + 2N). Floors, walls, doors, and windows keep their
authored coordinates and shift inward by +N. The roof spans the full inflated box, so eaves and
gable ends extend past the wall ring.
Stair orientation. Slope rows are half=bottom, shape=straight with facing pointed at the
ridge: south on the -z slope and north on the +z slope for an x-axis ridge, east / west
mirrored for a z-axis ridge.
An even span’s two apex stairs each face away from the ridge (north on the -z row, south on
the +z row for an x-axis ridge). Facing inward would leave a 0.5 × 0.5 undercut along both outer
faces for the roof’s whole length; facing outward moves that void under the ridge.
An odd span’s single apex stair is half=top with the low-slope facing. One cell has two outer
faces and a stair serves one, so the void is unavoidable and the rule fixes the choice.
4.4 Shed roof voxel rules
Section titled “4.4 Shed roof voxel rules”roof kind=shed slope_to=front|back|left|right [overhang=N] mat_slot=... lowers to a single stair
slope rising toward the wall named in slope_to=. Rows are shaped like a gable’s low slope
(half=bottom, shape=straight), but only one slope is emitted, so the opposite wall keeps its
authored height.
- Slope axis.
slope_to=front|backrises alongz;slope_to=left|rightrises alongx. The high edge sits on the named wall, the low edge on the opposite one. - Height. A shed rises
slope_spanvoxels above the wall top:dims.zforfront|backanddims.xforleft|right, both after overhang inflation. Each layer steps inward by 1 from the low edge asyrises. - Stair orientation. Every slope stair points at the high edge:
front→facing=south,back→north,left→west,right→east. The top layer is capped with one row athalf=topand the same facing. slope_to=is required. It has no default. A missing or unknown value isW_DEFERRED_MEMBERrather than a guessed direction.
4.5 Hip roof voxel rules
Section titled “4.5 Hip roof voxel rules”roof kind=hip [overhang=N] mat_slot=... lowers to a four-sided stair pyramid: all four walls
slope inward toward a centre ridge.
-
Ridge axis and height. As
gable: long axis, square ties tox,ceil(short_span / 2)above the wall top. -
Layer layout. Layer
L ∈ 0..extra_heightis the inset rectangle frame[L, dims.x − 1 − L] × [L, dims.z − 1 − L]. Layer0seats on the wall top and is always this frame, even when it is also the last layer:Edge States north row ( z = L)facing=south, shape=straightsouth row ( z = dims.z − 1 − L)facing=north, shape=straightwest column ( x = L)facing=east, shape=straighteast column ( x = dims.x − 1 − L)facing=west, shape=straightNW / NE corners facing=southwithouter_left/outer_rightSW / SE corners facing=northwithouter_right/outer_left -
Apex. The apex closes what the frames below it raised, so it applies only when
extra_height > 1. A square footprint caps with a singlehalf=topstair (odd short span) or a2x2block of them (even short span). A rectangular footprint caps with a row ofhalf=topstairs spanning the inset interior along the long axis. Apex facings follow the gable rule:southfor an x-ridge,eastfor a z-ridge. -
Overhang. As
gable.
4.6 Flat roof voxel rules
Section titled “4.6 Flat roof voxel rules”roof kind=flat [overhang=N] mat_slot=... lowers to one layer of solid blocks at
y = wall_top + 1, spanning the whole inflated bounding box.
- Material. Every deck cell is the
mat_slot=binding’s id with no blockstate, falling back tominecraft:spruce_planks. A deck is whole blocks, so any id is valid where a sloped roof would refuse: a stair among them is a stair in its default state. - Height. A flat roof adds
1toDims.ywhatever the footprint, sosize=WxHwithwalls height=KgivesDims.y = 1 + K + 1. - No slope arguments.
slope_to=, kind-specific facings, and ridge axes do not apply.
4.7 Level grouping and volume derivation
Section titled “4.7 Level grouping and volume derivation”level y=N groups members and places each of them N voxels above the struct’s base plane. The
level line itself lowers to no blocks; every member under it lowers as if written in the body with
N added to its vertical coordinate.
The volume a struct lowers into is derived, never written:
Dims.x = size.W + 2 × overhangDims.z = size.H + 2 × overhangDims.y = 1 + wall_top + roof_extraEach term counts only the members that will paint:
overhang: the largestoverhang=on any roof that will draw (akind=the compiler knows, plus aslope_to=if that kind isshed).wall_top: the largestN + heightover the walls whosemat_slot=resolves,Nbeing the enclosing level’s offset and0in the body.roof_extra: the tallest per-kind contribution from §4.3–§4.6.1: the base plane, which every struct has.
Members inside a level count in all three: a struct whose only walls sit under level y=5 is as
tall as one that writes them directly.
Not every role lowers at a non-zero offset. walls, door, window, stair, and
pressure_plate read N as the base their geometry is measured from. A floor and a roof are
planes a struct has one of, so under level y=N with N > 0 each fires W_DEFERRED_MEMBER and
lowers to nothing.
A member that does not paint does not size the volume. The overhang= of a level-scoped roof
does not widen the footprint, and its height does not raise Dims.y. The same holds for every way
a member drops out: a roof with no kind= or a shed with no slope_to= does not widen the
footprint, and walls whose material does not resolve do not raise Dims.y.
The material half applies to walls and not to roof, because a roof whose mat_slot= does not
resolve falls back to a material of its own and still draws. A themeless struct shows the
asymmetry: its walls lower to air and reserve nothing, while a roof kind=gable over them still
draws and still seats its ridge above them.
“Does the material resolve” is asked against the pinned target, so a block only some versions
declare can change Dims.y between two --target values. An id the pinned target does not declare
is E_UNKNOWN_ID, so no artifact ships from that shape.
4.8 Within-phase conflicts and the palette
Section titled “4.8 Within-phase conflicts and the palette”Across phases, the phase order decides: a door cut through walls is massing followed by
openings, and the hole is the point. Inside one phase, only source order separates two members,
which is what §4.1 grants to “local overrides within the same phase”.
That grant is for an author restating a member. Two footprints that intersect by accident are a
different thing, so the compiler keeps the last write and emits W_PHASE_CONFLICT naming both
members and how many voxels changed hands. The build is unchanged; the author is told that a line
they could move is deciding the result.
Two cases are not conflicts:
- A cell whose value does not change, as when two
wallsof one material meet over shared rows. - A member writing over itself, as when a
window’srepeat=/step=stamps overlap.
The palette of an evaluated body (a struct, a def, and each place that instantiates one)
is a set with a canonical rendering, not a log of the writes that filled it. It lists the blocks
that body contains, and it lists them in one fixed order: air at slot 0, then every other entry
ascending by (id, properties).
The id decides first. When two entries share an id, each one’s properties are sorted by name and the
two lists are then compared pair by pair, so the value decides once the names agree
(facing=north before facing=south), and a shorter bag sorts before a longer one that begins with
it (oak_stairs[facing=north] before oak_stairs[facing=north,half=top]). Sorting the names is what
makes the order independent of how a state literal happens to spell its properties:
@oak_stairs[half=top,facing=north] and @oak_stairs[facing=north,half=top] name one block, and
which of them a theme wrote first must not move a slot.
Slot 0 is the one exception to “the blocks that body contains”: air holds it whatever happens, so a
fully paved volume still carries the entry. A slot that was never painted at all is the other, and it
is kept on purpose — dropping it would delete the only evidence a released build carries that a
generator interned a material for geometry it does not emit.
Two rules follow, and both exist because the numbering reaches the .nbt, cairn info’s per-entry
rows, and resolved_ir_hash:
- A material whose last voxel a later phase covered is dropped, and the remaining slots renumber onto the gap. Otherwise two sources differing only in which member lost would produce different artifacts for the same build.
- Two members that share no voxel do not order each other. Nothing about the finished grid depends
on which of their lines came first, so nothing about the artifact may either — which is what §4.1
opens by promising, and it is a claim about the bytes and not only about the voxels. Sorting is
what makes it true: the palette is derived from the finished grid alone, so any permutation of the
source that leaves the grid alone leaves the whole
BlockArrayalone.
The sort is over the resolved state, so it is stable across editions and versions in the sense that matters: it does not consult source spans, member ids, phases, or paint order. It is not a claim that two targets agree on the palette, because two targets need not agree on the ids in it.
A walkway’s array is laid by the connect pass rather than by the phases, and is not covered here.