Skip to content

7. Materials and Themes

The structure never writes a concrete block name. It carries mat_slot injection points, and a theme binds values to slots and selectors, the way CSS or dependency injection does. That separates structure (where the walls are) from style (which blocks, what detailing).

def cottage class=house size=9x7:
floor id=floor mat_slot=floor
walls id=walls class=outer mat_slot=wall height=4
roof id=roof kind=gable mat_slot=roof
window id=front_windows class=small side=front y=2 repeat=2 mat_slot=glass
theme medieval:
slot wall -> @cobblestone
slot roof -> @spruce_stairs
walls[class=outer] -> trim=@spruce_log # part detailing, via a selector (reserved)
window[class=small] -> frame=@spruce_wood

The cascade. A member collects the bindings of every selector row it matches, in source order, so when two rows bind the same key the member keeps the later value. CSS applies the same rule to two rules of equal weight. Bindings are reserved (below), so today the cascade decides what a member collects and nothing that is built.

Rows whose attributes partly overlap rely on that: window[class=small,side=front] refines window[class=small] for the members it selects, and the members only the wider row selects keep the wider row’s binding.

Two rows that select the same members are different. Same keyword and same attributes means they match member for member, so no member keeps the earlier row’s value of a key they both bind: that value is dead text whatever a binding comes to mean. That is E_DUPLICATE_SELECTOR (Lint §11.1). Sameness is by meaning: attribute order does not count, and class= / id= / mat_slot= values compare as label text, so window[class=small] and window[class="small"] are one selector. Rows that coincide but bind different keys are not reported. They compose, and splitting a long binding list over two lines is allowed.

Selector bindings are reserved. Which keys a row may bind on each keyword, and what each one paints, is not specified yet, and no lowering reads a binding: the two selector rows above build the same cottage as a theme without them, and no member’s block depends on them. Until that is specified the compiler reports each binding as an unreached key, W_IGNORED_ARGUMENT (Lint), whatever its key or value, on every row whose keyword it knows, matched or not and in an applied theme or not. A row whose keyword it does not know gets E_UNKNOWN_KEYWORD instead. A binding’s value is not resolved as a block either, because what the key would paint is not specified: frame=@no_such_block is reported exactly as frame=@spruce_wood or frame=42 is, with or without --target, and no binding makes check, compile or info refuse a build. The rest of a row is checked as before: E_THEME_SELECTOR_UNMATCHED and E_DUPLICATE_SELECTOR apply to it exactly as their own entries say.

def, theme, and site are unified by the same slot-bearing Component mechanism (Components, Editing, and Multi-building).

A theme binds canonical tokens, not raw block IDs. The backend resolves the ID, state names, state values, and serialization per (edition, version) (Versioning and Editions). An LLM never needs to know pillar_axis, little-endian NBT, or Bedrock’s weirdo_direction.

Tokens come in two tiers:

Tier Example What it means
Canonical block token @oak_planks, @water_cauldron, @oak_log[axis=x] A specific meaning in Minecraft. Silent meaning-breaking downgrades are forbidden, so @water_cauldron may never become cauldron.
Abstract material token @floor.wood.broadleaf, @roof.dark_wood An aesthetic choice. Theme policy MAY downgrade these (oak ↔ birch).
theme cottage:
slot floor -> @floor.wood.broadleaf # abstract: resolved by target and policy
theme exact_oak:
slot floor -> @oak_planks # canonical: pinned 1:1

A canonical token absorbs five patterns. The resolution table’s structure is in Versioning and Editions.

Pattern Example Policy
Rename 1:1 @dirt_path (was grass_path) Auto-resolve.
Split 1:N @cauldron[fluid=water] → water_cauldron Separate by meaning token.
Merge N:1 @oak_slab (was wooden_slab{variant}) Resolve per target.
New @cherry_planks Needs a requires >= floor.
Deleted Absent in the target version Hard error plus alternatives.

Only ID, state, and serialization differences may be absorbed. Concept absence and game-behaviour differences are not.