11. Lint and Constraint Validation
The compiler reports warnings and errors with line numbers. Every message MUST carry the self-correction triple: what is wrong / valid candidates in the target / a suggested fix. That shape is what feeds the loop in Evaluation Framework.
11.1 Diagnostic codes
Section titled “11.1 Diagnostic codes”This section is the catalog: every code a stable command can raise has a row here, and a code with no row is a bug in this section rather than a code outside it. Where a code’s rule belongs to another chapter, the row states what the code means and links there for the rule — the row is what a reader with a code in hand needs to find, and the chapter is where the behaviour is defined.
The redstone pipeline’s E_LOGIC_* / W_LOGIC_* codes are outside that promise. They are reachable
only through cairn synth --experimental-logic-synth, which is Internal tier
(Compatibility) — nothing about those strings is guaranteed, so a row here would
state a contract that does not exist. Redstone and Logic names the ones its own rules
turn on.
Duplicates
Section titled “Duplicates”| Code | Meaning |
|---|---|
E_DUPLICATE_SIZE |
A header declares more than one size=. |
E_DUPLICATE_SLOT |
A theme body declares the same slot twice. |
E_DUPLICATE_ARG |
A key= is repeated in one argument list. |
E_DUPLICATE_ID |
Two members share an id= in the same body scope. |
E_DUPLICATE_SELECTOR |
Two selector rows in one theme select the same members and bind the same key. |
E_DUPLICATE_ITEM |
Two top-level items of the same kind share a name. |
E_DUPLICATE_HEADER |
A single-valued @directive is declared more than once. |
E_DUPLICATE_SELECTOR compares selectors by meaning, not by text: attribute order does not count,
and class= / id= / mat_slot= compare as label text, so small and "small" are one value.
Rows that bind different keys compose and are not reported. Neither are rows whose attributes
partly overlap (see Materials and Themes §7.1).
E_DUPLICATE_ITEM treats theme / def / struct / site as four separate namespaces, so one
name may appear once in each. For the first three the first declaration resolves and the rest bind
nothing. Two site blocks of one name instead merge into a shared site::NAME::PLACE_ID
namespace: every place with a distinct id= still builds, only a repeated id= collides, and
east_of= does not reach across the blocks.
E_DUPLICATE_HEADER covers @cairn and @intended_targets. @requires is excluded: its floors
compose to the strictest across every line, so a second one adds a constraint (§5.3).
Syntax and structure
Section titled “Syntax and structure”| Code | Meaning |
|---|---|
E_PARSE |
The source did not parse. |
E_UNKNOWN_KEYWORD |
The statement keyword is not in the known-keyword table. |
E_UNKNOWN_ARGUMENT |
A key= outside the vocabulary of the member’s keyword, written as an argument or inside the member’s own [key=value], or outside the size= / class= a struct / def header takes. |
W_IGNORED_ARGUMENT |
A key= inside that vocabulary, or inside a struct / def header’s, that no pass read on the line it was written on, or any key= bound by a theme selector row, which no pass reads yet. |
E_MISPLACED_BINDING |
A -> value tail on a member whose keyword cannot emit a signal (§14.2). |
E_MISPLACED_MEMBER |
The keyword is known, but the enclosing body has no reader for it. |
E_UNEXPECTED_POSITIONAL |
A bare value on a line that reads none (§5.1). |
E_UNSUPPORTED_NESTING |
A member carries an indented body that nothing reads. |
E_TYPE_MISMATCH_LABEL |
A label-typed key’s value is not an identifier or string. |
E_TYPE_MISMATCH_SIZE |
A size= value is not a WxH literal. |
E_CONNECT_ARITY |
A connect row’s shape is not FROM.PORT to TO.PORT. |
E_INVALID_REQUIRES |
A requires expression that is not a version floor, written as the @requires header or as a def / theme body line (§5.3). |
W_INVALID_CAIRN_VERSION |
An @cairn value that is not a YYYY.M[.PATCH] language version (§5.3). |
W_FUTURE_CAIRN_VERSION |
An @cairn value naming a language version later than the compiler reading it. |
E_MISPLACED_MEMBER fires on a place / connect inside a struct or def, or a geometry
keyword among a site’s rows. It is reported once at the offending row, and anything indented
under it goes with it.
E_UNSUPPORTED_NESTING: only level y=N inside a struct or def groups members, and a site
body is a flat list. Reported once per dropped subtree, at its root.
E_TYPE_MISMATCH_LABEL: the label-typed keys are id=, class=, mat_slot=, use=, and
theme=. For use= and theme= a mistyped value looks the same as an absent key to the resolver.
This code says the key is on the line but unusable; E_INCOMPLETE_PLACE says it is missing.
E_CONNECT_ARITY: connect FROM.PORT to TO.PORT is the one form that reads positionals. The code
covers a missing half, a missing or replaced to keyword, extra trailing positionals, and an
endpoint that is not a one-dot PLACE.PORT reference. The two endpoints are reported separately,
since they are independent fix sites.
E_INVALID_REQUIRES: the accepted shape is an optional edition, version, >=, and a version
label, with whitespace optional. The code covers a word before version that is not an edition,
any other operator, a missing version, a component that does not begin with a digit or does not fit
in a u32, a - with no readable pre-release tag after it, and text after the version. It does
not cover a well-formed label the target edition has no DataVersion for — that is
E_REQUIRES_UNORDERABLE, and it is not a syntax fact. Both spellings of the floor are checked, and
by the same rule: the @requires header, and the member-level requires line a def or a theme
may carry (§10.4). A part no build
instantiates is checked too — the mistake is in the line, not in whether anything reads it.
W_INVALID_CAIRN_VERSION: the accepted shape is YYYY.M or YYYY.M.PATCH — a four-digit year, a
month 1 … 12, and an optional patch, every component decimal digits. A leading zero on the month
is accepted and does not change it: 2026.06 and 2026.6 are one version, the first being
calver.org’s YYYY.0M and the second the YYYY.M a Cargo version field can carry. The code
covers a value that is not two or three components, a component that is empty or not digits, a
year that is not four digits, a month outside the calendar, a patch past u32, and a second word
after the version — the header value is a whole line, so @cairn 2026.6 draft names a version and
then something else.
This is deliberately stricter than @requires, which reads a Minecraft label out of Mojang’s
namespace and only has to order it, so a component there need only begin with a digit and a
pre-release tag is a label too. @cairn names Cairn’s own version, so every component is digits,
2026.13 is a month that does not exist and 1.2 is a semver rather than a year.
W_FUTURE_CAIRN_VERSION is the one thing a compiler can usefully say about a file written against
a language newer than itself: a keyword or argument added after this build is reported as
E_UNKNOWN_KEYWORD or E_UNKNOWN_ARGUMENT, and only the header knows those findings may be about
the version gap rather than about the lines they name. That reaches what a later language adds
within the shapes this one has. A whole new syntactic form — a directive, a top-level item — is
E_PARSE, and parsing precedes every check pass (§11.3), so this finding
cannot appear in the case the version gap explains best.
There the header is read out of the text — one line, matched by a leading @cairn and read to end
of line — and E_PARSE carries a note saying the file declares a later language than the build
reading it. A note rather than a second finding, because a source that does not parse reports
E_PARSE alone and that stays true. Only a value that reads as a version earns it: a malformed one
is W_INVALID_CAIRN_VERSION’s business, and repeating that on an unrelated parse failure would be
noise.
The two codes never both fire on one directive — a value that is not read as a version has no version to compare.
Materials and targets
Section titled “Materials and targets”| Code | Meaning |
|---|---|
E_UNKNOWN_ID |
A resolved block ID the pinned target does not declare. |
E_VERSION_CAP |
--target is below an @requires floor the source declares (versioning-editions §10.4). |
E_REQUIRES_UNORDERABLE |
An @requires floor names a version the target edition’s DataVersion table cannot place (versioning-editions §10.4). |
E_INTENDED_TARGET_CAP |
Every version @intended_targets names is below a floor the same file declares. |
W_INTENDED_TARGET_CAP |
Some, not all, of them are. |
W_INTENDED_TARGET_UNSUPPORTED |
@intended_targets names a version no --target of the edition can build. |
E_INCOMPATIBLE_MATERIAL |
A member whose geometry attaches blockstates is bound to a material that cannot carry them. |
E_MISSING_MATERIAL |
A member whose only route to a block is mat_slot= was written without one. |
E_UNRESOLVED_SLOT |
A member’s mat_slot= names a slot the bound theme does not declare. |
E_UNKNOWN_SLOT_TARGET |
A slot NAME -> VALUE whose value is neither a canonical nor an abstract material token (Materials and Themes). |
E_THEME_SELECTOR_UNMATCHED |
A theme selector row that matches no member in the file. |
E_THEME_VARIANT_MISSING |
The pinned edition can bind none of a theme’s per-edition variants. |
E_INCOMPLETE_PLACE |
A place row omits id=, use=, or theme= (§9.3). |
E_UNKNOWN_ABSTRACT_TOKEN |
A mat_slot= resolves to an abstract material token the offered pack’s catalog does not declare (Materials and Themes). |
W_ABSTRACT_TOKEN_DEFERRED |
The same token with no catalog offered at all, so there is nothing to lift it against. |
W_STATE_LITERAL_UNCHECKED |
A canonical token’s state literal (@oak_log[axis=x]) was taken as written, since nothing checks its properties and values against the target until E_STATE_DOMAIN is implemented (Versioning and Editions). |
W_NO_THEME_BOUND |
A scope has no theme bound to it, so every mat_slot= member in it lowers to air. |
W_THEME_VARIANT_REBOUND |
A place theme= names one edition’s variant and the pinned edition bound a different one (Versioning and Editions). |
E_UNKNOWN_ID and E_INCOMPATIBLE_MATERIAL are raised during block-array lowering, so only the
commands that lower report them: cairn compile, cairn lower, cairn info, and cairn check --edition E --target V. E_UNKNOWN_ID further needs a pinned target, so the two commands that
raise it are cairn compile --target and cairn check --edition E --target V — info and
lower lower against no version. A cairn check with no --target runs no lowering at all and
reaches neither code.
W_STATE_LITERAL_UNCHECKED is raised by the same lowering, on every state literal it reads from a
theme slot or a connect … path=, so cairn compile, cairn lower, cairn info and cairn check --edition E --target V report it under either edition. It stands in for E_STATE_DOMAIN: until
the compiler holds a table of each block’s states, a property the block lacks or a value outside
its domain is written into the structure file unchanged, and the warning is what keeps that from
being silent.
cairn check --target exists so a CI job can gate on the check command and still see the
lowering-stage findings a compile would refuse on: an id the target does not declare passed
check at exit 0 and stopped cairn compile at exit 1, and the information that decides it — the
one (edition, version) pair — was not on check’s command line. It requires --edition for the
reason Compilation Model §4.2 refuses --target alone, and runs the
same lowering pass against the same table compile does, so a lost scope earns the same
E_PARTIAL_BUILD. What it does not do is anything compile writes: no artifact, no lockfile, and
no @requires floor enforcement — a lock certifies a build, and only the command that produces
one holds --target to the floors (E_VERSION_CAP). Leaving the flag off is unchanged behaviour,
so no source that passes today starts failing. See
Versioning and Editions §10.4.
E_INCOMPATIBLE_MATERIAL today means a sloped roof or an eave stair bound outside the stair
family (Compilation Model §4.3).
E_THEME_VARIANT_MISSING fires only under --edition, and is reported once per logical theme
however many scopes read it, since they all want the same edit in the same theme block. Every
placement naming it is still refused. A module that declares such a theme but never reads a
mat_slot= from it is not reported: the build is byte-identical with or without the pin.
E_THEME_SELECTOR_UNMATCHED is a warning despite its prefix. A rule that matches nothing overrides
nothing, so every member keeps the material it would have had with the rule deleted; the finding is
about the author’s intent rather than about what was built. The prefix is part of the code string,
which is Stable (Compatibility Tiers), so it stays as written — read severity from
the severity field rather than from the first letter.
E_UNKNOWN_SLOT_TARGET is an error on the opposite test: a slot bound to nothing lowers every
mat_slot= pointing at it to air, so a theme whose slots are all mistyped builds a hollow shell of
the requested extent at exit 0.
E_MISSING_MATERIAL and E_UNRESOLVED_SLOT are the two halves of one split, and no member
earns both: the key is absent for the first and present-but-unusable for the second. They have
different repairs, which is why they are different codes — the same division E_INCOMPLETE_PLACE
and E_TYPE_MISMATCH_LABEL draw on a place row.
E_MISSING_MATERIAL applies to the roles that put nothing anywhere without one. floor and
walls reach the palette through the applied theme’s slot map and have no default block, so a
bare one contributes no voxel. A window without a mat_slot= is an opening, carved to air,
and is not reported — that is how a narrow slit is punched through a wall without choosing a
species for it; a door is always a carve and reads no material at all. A roof, stair or
pressure_plate paints a default block and is not reported either. cairn check raises it, before any lowering, and it needs no theme: a module
that declares none is still told which of its members name no material.
E_UNRESOLVED_SLOT is reported at most once per member per bound theme. A def body is
resolved once as its own scope and once again for every place that instantiates it, and each of
those resolutions binds a theme; a repeat against a theme already reported for that member is
dropped. Two placements naming two themes are two findings: each names the theme it is about and
each is a separate edit. Which resolution a surviving finding came from is not specified, because
two of them can bind the same theme and still judge the slot differently.
The difference is edition-variant softening. Without an --edition pin, a slot that any sibling
variant of the picked theme declares counts as known and is not reported: the concrete binding is
edition-specific and comes into scope only once a pin narrows the theme to one variant
(Versioning and Editions §10.7). A
place ... theme=NAME is softened the same way only when NAME names the logical theme — naming
a variant asks about that variant’s slots alone. Two placements can therefore bind one theme and
disagree about one slot.
A def nothing places is still resolved, against the theme the module picks, so a file of defs
and no site is checked as long as the module has a single logical theme to pick. Where it
declares more than one — or none at all — no theme binds to the def’s own scope and its
mat_slot= names are not judged until a place chooses one.
E_INCOMPLETE_PLACE names every key the row is short of, and the row is dropped from the build.
E_UNKNOWN_ABSTRACT_TOKEN and W_ABSTRACT_TOKEN_DEFERRED differ on whether anything could have
answered. A pack was offered and does not declare the token, so the build stops with a suggestion
towards the closest one it does declare; no pack was offered, so nothing was asked and the cell
degrades to air with a warning. The second is the path a library caller reaches — LSP highlighting,
or a cairn check with no pack — and it is a warning for that reason: refusing there would refuse
every source read without a pack.
W_NO_THEME_BOUND is the same shape one level up: a mat_slot= that resolves against no theme at
all has no slot map to read, so the member contributes no voxel. A module that binds no theme and
reads no mat_slot= is not reported.
The three @intended_targets codes weigh the file’s stated intent against its own floor
(versioning-editions §10.4). A version
the edition cannot build is W_INTENDED_TARGET_UNSUPPORTED and is not also weighed against a floor:
“this target does not exist here” is what the author acts on, and a cap beside it would send them to
edit a line that is not what stops the build. The rest — the versions the edition can build — are
counted among themselves: every one of them below a floor is E_INTENDED_TARGET_CAP, since the
file can then be built for nothing it says it is for and the first cairn compile --target naming
any of them is E_VERSION_CAP, while some of them is W_INTENDED_TARGET_CAP, because the header
is a hint (§5.3) and the versions above the floor still build. A version that
was never buildable is not in that count either way: it answers for none of the list, and letting it
would report a file nothing can build as half a problem. One header can earn a cap code and the
unsupported code at once; the versions it names are not all wrong in the same way.
All three are per-edition answers, since a floor and a target label are ordered in one edition’s
DataVersion table. Every command that gates on cairn check reports the two cap codes — check,
info, lower, compile, synth — each weighing the header in the tables of the editions it is
about: the one --edition names, the ones cairn info --editions lists, or both where the command
names none. A finding either edition reaches is reported, because the contradiction is between two
lines of the file however it is later built; one span carries one cap finding, and two editions
disagreeing about how far it reaches report the error. W_INTENDED_TARGET_UNSUPPORTED waits until
exactly one edition is in scope: a version Java cannot build is routinely the Bedrock target the
author means, so with both in scope the question has not been asked.
Sites and placements
Section titled “Sites and placements”| Code | Meaning |
|---|---|
E_INVALID_PLACE_ID |
A place id= is empty or carries ., :, /, \ or whitespace. |
E_DUPLICATE_PLACE_ID |
Two place rows in one site share an id=. |
E_OUTPUT_NAME_COLLISION |
Two artifacts a build would write share a file name, ignoring case: a struct and a place of one name, one id= placed in two sites, or two walkways whose names flatten alike. |
E_INVALID_PLACE_ORIGIN |
A place carries an at= other than origin, or combines at= with east_of= / north_of= (§9.3). |
E_UNRESOLVED_PLACE_REF |
A place use=, an east_of= / north_of=, or a connect endpoint names a place or def that does not exist. |
E_UNRESOLVED_THEME_REF |
A place theme= names a theme the module does not declare. |
W_UNUSED_DEF |
A def no place use= references. |
E_INVALID_PLACE_ID is about round-tripping rather than taste. The scope key
site::SITE::PLACE, and every walkway key parsed back out of one, is built with . and : as
separators, so an id carrying one cannot be read back. The id is also the name of the file the
placement is written to in the output directory, and a / or \ would make that name a path: a
relative id with one lands in a subdirectory, and an absolute id replaces the output directory
altogether. Both separators are refused on every platform, so whether an id is accepted does not
depend on the host that checks it. id= accepts a string literal, which is what let the value
through.
E_DUPLICATE_PLACE_ID names both spans. The first row wins for everything that references the id
and the duplicate is dropped, so a reference resolving to “the other one” is not a second finding.
E_OUTPUT_NAME_COLLISION follows from how artifacts are named
(§9.3.4): a struct by its name, a place by
its id= alone, and a walkway by its site and ports. Every one of them lands in the same output
directory, so two can name one file, and if both are built, a build can keep only one of them. The
check runs before lowering. It leaves out a sizeless struct and a place of a sizeless def,
which lowering drops, and counts two connect rows for one pair of ports, in either order, as one
walkway, which is what lowering lays. It still counts a struct or placement that
W_STRUCTURE_TOO_LARGE drops, and a walkway whose search area is past the router’s cap, which
W_WALKWAY_BLOCKED reports and does not lay, so the finding says the two files would be one.
Names are compared ignoring case, because Hut and hut are one file on the case-insensitive file
systems macOS and Windows use by default, and whether a source builds should not depend on the host
that builds it. The finding is raised with the other site findings, before lowering, so cairn check reports it without a --target. A name declared twice is not this code: a second struct hut is E_DUPLICATE_ITEM, and a second id= in one site is E_DUPLICATE_PLACE_ID.
E_UNRESOLVED_PLACE_REF and E_UNRESOLVED_THEME_REF each carry a nearest-match suggestion when
one fits the spell cap (did you mean). Both are errors because substituting
something for the name would build a site the source did not describe.
Connections and walkways
Section titled “Connections and walkways”| Code | Meaning |
|---|---|
E_UNRESOLVED_PORT |
A connect A.PORT to B.PORT names a port the referenced def does not expose. |
E_AMBIGUOUS_PORT |
The port id matches more than one member of the referenced def. |
E_MISSING_PATH_MATERIAL |
A connect row carries no path=, so the walkway has no material to lay. |
W_DUPLICATE_WALKWAY |
A connect repeats a (from, to) pair an earlier row in the same site already laid. |
W_INVALID_WALKWAY_IDENT |
A site, place or port identifier in a connect contains __, or a place or port identifier starts or ends with _. |
W_DEFERRED_CONNECT |
A connect targets a place that was itself refused, so there is nothing to connect. |
W_WALKWAY_BLOCKED |
Cells of the fallback path overlapped an existing structure and were dropped. |
E_UNRESOLVED_PORT is the port half of the place.port shape alone; the place half is
E_UNRESOLVED_PLACE_REF. E_AMBIGUOUS_PORT takes the first match for lowering and reports the
collision, since the repair is to rename one of the colliding members rather than to pick for the
author.
E_MISSING_PATH_MATERIAL is an error where the other absent-material codes are warnings: a walkway
that degrades to air leaves two buildings looking connected in the source and unconnected in the
world, with nothing in the report to say so.
W_INVALID_WALKWAY_IDENT is the same round-trip rule as the . / : half of
E_INVALID_PLACE_ID, on a different separator. __ joins the from and to halves of a walkway’s scope key, so b__c in one half and
c__home2 in the other encode to one string. A _ next to that separator merges into it the same
way: a.p_ to b.p and a.p to _b.p both encode to a.p___b.p, and a port named _ leaves a key
that splits back into an empty port. Only the end of the from port and the start of the to place
touch the separator. A connect can be written in either direction, so the start of every place
and the end of every port can end up there. The other four edges, the end of a place and the start
of a port, sit next to a . whichever way the row is written and cannot merge; they are refused
too, so the rule stays one sentence: no place or port id starts or ends with _. The site is exempt
from the edge rule, since :: separates it from both neighbours. The row is dropped and the finding
names the segment to rename. The walkway it asked for is lost, so cairn compile refuses the build
with E_PARTIAL_BUILD, as it does for every row that lays nothing.
W_DEFERRED_CONNECT follows whatever refused the place — an incomplete row, a mistyped key, a
failed origin selector, an unresolved use= or theme=. It is a warning because the finding that
has the repair is the one on the place, and reporting the connect as a second error would send
the author to a line that is correct.
Lowering
Section titled “Lowering”| Code | Meaning |
|---|---|
W_DEFERRED_MEMBER |
A member the block-array pass does not lower, so the scope builds without it. |
W_STRUCT_NO_SIZE |
A struct declares no size=WxH, so lowering can derive no extent and skips it. |
W_DEF_NO_SIZE |
The same on a def, so every place use= of it is skipped. |
W_STRUCTURE_TOO_LARGE |
A scope’s derived extent exceeds the volume the block-array pass will allocate for. |
W_PHASE_CONFLICT |
Two members in one phase wrote one voxel to different blocks (§4.4). |
E_PARTIAL_BUILD |
At least one requested scope, or a walkway a connect row asked for, did not lower, so the run produced less than was asked for. |
W_STRUCT_NO_SIZE and W_DEF_NO_SIZE are one rule split by what carries it, so a filter matching
on code can tell a struct that will not build from a template that will not instantiate.
W_STRUCTURE_TOO_LARGE fires on a combination: size=, walls height=, roof overhang= and
level y= are each range-checked on their own, and this is the product of them being out of reach.
A warning rather than an error, matching the two above — the scope is skipped and the rest of the
build is unaffected.
W_DEFERRED_MEMBER keeps a partial build inspectable rather than failing the module: the rest of
the scope lowers, and the finding names what is missing from it.
E_PARTIAL_BUILD is the run-level counterpart, and the one error among these: a warning above says
a scope builds without something, and this says a scope the command was asked for did not build at
all. A scope that lowers to air alone did not build either, whatever left it empty: every member
deferred, no theme gave its mat_slot= members a block, or it declares no member. It is reported
once for the run, naming how many of the requested scopes were lost, by cairn compile and by a
cairn check --edition E --target V that runs the same lowering pass.
W_PHASE_CONFLICT is last-wins reported rather than refused. Compilation Model
grants last-wins to local overrides within one phase, which is what an author restating a member
is; two footprints that happen to intersect is not, and the grid cannot tell the two apart. The
resolution the spec mandates still happens — the finding says which voxel it happened at.
Truth tables
Section titled “Truth tables”| Code | Meaning |
|---|---|
E_TRUTH_TABLE_EMPTY |
An assert truth(...) with no rows, or none whose output is 0 or 1. |
E_TRUTH_TABLE_CONFLICT |
Two rows assign the same input combination different outputs. |
W_TRUTH_TABLE_DUPLICATE_ROW |
Two rows cover the same input combination without contradicting each other. |
W_TRUTH_TABLE_PARTIAL |
The rows leave input combinations unassigned. |
Both codes are reported on the later row. A row is compared with every earlier row that shares a
combination with it, so whether a table is refused does not depend on the order its rows are
written in: 0- -> 1 after 00 -> 1; 01 -> 0 is E_TRUTH_TABLE_CONFLICT, though it agrees with
the first of the two. A conflict takes precedence over a duplicate on the same row, and its note is
at the first row assigning the combination it names the other output. Any other finding’s note is
at the first row assigning the combination it names, so every finding about one combination points
at the same row. The spec does not say which of two conflicting rows an evaluator would read,
because the repair is to decide which row is wrong.
A - makes the same combination reachable from rows that do not look alike, so both codes are
about the combination rather than about the pattern: 0- and -1 both assign 01. The fix
differs with the shape. A row inside an earlier one — 01 under 0- — is deleted. Two rows that
merely cross are narrowed, because deleting either would lose the combinations only it assigns.
When the earlier row is itself reported for overlapping a row before it, the fix is to settle that
row first instead: in 0- -> 1; -0 -> 1; 10 -> 1, -0 is told to narrow away from 0-, and
deleting 10 because -0 stands for it would, together with that, leave 10 unassigned.
A - output is the one shape that is neither a conflict nor a repeat: the row declines to
constrain its combinations, so there is nothing for a concrete output to contradict, and nothing
for it to agree with either. The pair is W_TRUTH_TABLE_DUPLICATE_ROW, and its fix names the
asymmetry — whichever of the two rows is deleted, the table reads differently afterwards. A table
with rows but no 0 or 1 output among them constrains nothing, which is why
E_TRUTH_TABLE_EMPTY covers it too; the sentence differs from the no-rows one, because the repair
is to change a row rather than to add one.
The two warnings are warnings because every row present is still a real constraint. One table can
earn both W_TRUTH_TABLE_DUPLICATE_ROW and W_TRUTH_TABLE_PARTIAL: a repeated row, or one inside
an earlier one, fills no combination the table did not already have. Two rows that cross are the
exception — the coverage finding is withheld there, because a count taken without the later row
would name a combination the table does assign.
Semantic categories
Section titled “Semantic categories”Beyond the codes above, lint covers:
| Category | Checks |
|---|---|
| Geometry | AABB expansion: a window outside the wall, a door hanging in mid-air. |
| attachment | A frame, painting, sign, button, lever, or torch on a valid attachment face. |
| entity_aabb | Entities not clipping walls or paths, not blocking a door’s swing, not cramming. |
| support | Hanging lanterns, torches, campfires, and gravity blocks such as gravel. |
| fluid | Consistency of water source, flow, and waterlogged. |
| version_caps / parity | Whether a state or entity schema is usable in the target (Versioning and Editions). |
| edit_stability | Whether an intent_state change ripples into an unrelated member’s resolved_state. |
| redstone | Per-tick simulation against the declared truth table and temporal assertions; timing conflicts, QC dependence, routing congestion (Redstone). |
| AABB interference | On overlap, priority-merge or reject. Boundary blockstate re-resolution is the IR layer’s job. |
“did you mean”
Section titled ““did you mean””Three diagnostics reject an identifier against a closed vocabulary: an unknown keyword, an unknown
mat_slot= name, and an unknown --target version. Each attaches a did you mean `X`? note when
a candidate sits within a length-scaled Damerau-Levenshtein cap of ≤ 1 edit for 1–3 characters, ≤ 2
for 4–6, and ≤ 3 beyond. The closed-set listing (expected one of: ...) is always printed as well.
11.2 Machine-readable payload
Section titled “11.2 Machine-readable payload”Every command that takes --format json — check, info, parse and lower — writes exactly one
JSON document to stdout under it, for every input it is given. check’s document is an array of findings, so a source that does not parse is
that array with one E_PARSE element. info’s is the report; where there is no report — a parse
failure, or any error-severity finding — it writes {"diagnostics": [ ... ]} instead, told apart
from a report by its keys and by the exit code. Which pass raised the finding does not change that:
the strict per-edition dry-run sees what the edition-neutral gate unions away, and a refusal it
raises writes the same document, after every requested edition has been walked.
Which pass raised the finding does decide what the document carries, and this is the contract rather than an accident. Where the edition-neutral pass refuses, its warnings are elements of the document beside the errors. Where the per-edition pass refuses, the document carries the errors alone: the neutral warnings have already been reported as text, and a per-edition warning belongs to a row the run is about to discard, so it reads on stderr under the note naming its edition. Warnings on a run that still has a report are reported as text on stderr in both formats.
A run-level refusal is not an element of check’s array: an unshipped --target and a lowering
that lost a scope (E_PARTIAL_BUILD) are facts about the command line and about the build rather
than findings at a span, so both are reported on stderr and by the exit code in either format —
the shape compile gives them. The array still carries every finding the run did reach, so it is
a report of what was checked rather than an empty document.
This is settled rather than pending: check --format json has no machine-readable form for a
run-level refusal, and a consumer reads the verdict from the exit code. Stderr is prose for a person
in either format. It is not part of this contract, and a consumer should treat it as unstructured
text rather than parse it. What the consumer can tell without it is that the run was refused at
the run level rather than by a finding: an exit of 1 over an array that holds no element with
"severity": "error" means exactly that, down to a [] over a source with nothing to report. A
refusal can also arrive on a run that has error-severity findings of its own, and then the array
reads like any other error failure; that the run was also refused at the run level is said only
on stderr. No
other failure reads that way — a source that does not parse is an array carrying E_PARSE, and a
file that cannot be read writes no document at all. Which of the two refusals it was, and what the
build lost, is said only on stderr.
One info refusal is a run-level refusal of the same kind: a palette carrying a blockstate a
registry pack was expected to refuse costs that edition its portability row, and names no span in
the source. Such a blockstate is either a leak in the pack or the compiler, which the author cannot
repair, or a state literal on a stair in the source — a facing or half value outside the Java
domain (@oak_stairs[facing=up]), or a key other than facing / half / shape — which nothing
checks against the target until E_STATE_DOMAIN is implemented. The refusal names both. A literal
on any other block is counted unsupported instead. It reads as prose on stderr in both formats.
The document is still written — the promise is one document per input, not one element per
refusal — so a run refused by nothing else writes {"diagnostics": []} and says the rest with its
exit code.
parse’s product is the AST and lower’s is the block-array IR. A dump is not a report, so a
failure is not the dump with a hole in it: each writes the same {"diagnostics": [ ... ]} document
info does, told apart from the dump by its keys and by the exit code. lower writes it for a
source that does not parse and for one that fails a later pass, since neither leaves an IR worth
dumping. Under every other --format the findings read as prose on stderr.
--format json renders one object per finding:
| Field | Type | Notes |
|---|---|---|
code |
string | Stable E_* / W_* identifier; same string as the gcc-style format. |
severity |
string | "error" or "warning". |
line |
integer | 1-based line of the primary span’s first byte. |
col |
integer | 1-based column of the same byte, in Unicode scalar values. |
end_line |
integer | 1-based line of the span’s exclusive end boundary. |
end_col |
integer | 1-based column of the same boundary. |
primary |
string | The human-readable message. |
notes |
array | [{line?, col?, message}]. Omitted when empty. |
data |
object | Code-specific payload. Omitted when absent. |
data is an open object tagged with kind. Match on (code, data.kind) rather than parsing
primary. Additions are strictly additive, so ignore unknown kind values rather than failing on
them. Codes not listed below omit data entirely, so the JSON key is absent rather than null.
| Code | data payload |
|---|---|
W_WALKWAY_BLOCKED |
{ "kind": "walkway_blocked", "skipped": <u64> }. Cells along the fallback L-shaped path that overlapped an existing structure and were dropped. |
E_DUPLICATE_SELECTOR |
{ "kind": "duplicate_selector", "rebound": ["frame"] }. The binding keys this row takes over from an earlier one, without the trailing =. Never empty. |
E_UNKNOWN_ID |
{ "kind": "unknown_id", "id", "registry", "origin", "token"?, "suggestion"?, "aliases"? }. See below. |
E_INCOMPATIBLE_MATERIAL |
{ "kind": "incompatible_material", "id", "required", "slot"?, "token"? }. The bound material, the family the geometry needs, and where the binding came from. |
E_INCOMPLETE_PLACE |
{ "kind": "incomplete_place", "missing": ["id", "use", "theme"] }. The keys the row does not declare. Never empty. |
E_INVALID_REQUIRES |
{ "kind": "invalid_requires", "reason", "found" }. reason is one of not_a_version_requirement, unknown_edition_scope, unsupported_operator, empty_version, component_not_a_number, component_too_large, prerelease_not_a_tag, trailing_tokens. found is empty when the failure names no fragment. |
W_INVALID_CAIRN_VERSION |
{ "kind": "invalid_cairn_version", "reason", "found" }. reason is one of component_count, component_not_a_number, year_not_four_digits, month_out_of_range, patch_too_large, trailing_tokens. found is the component the reason is about, and is empty when the failure names no fragment — component_count, and component_not_a_number on a value whose component is itself empty (2026.). |
W_FUTURE_CAIRN_VERSION |
{ "kind": "future_cairn_version", "declared", "compiler" }. Both verbatim: declared is the string in the file, compiler is the build that reported it. |
W_TRUTH_TABLE_PARTIAL |
{ "kind": "truth_table_partial", "inputs": 2, "covered": 1, "missing": ["01","10","11"] }. See below. |
The three @intended_targets codes |
{ "kind": "intended_targets", "edition", "targets": ["1.20.4"], "floor"? }. The versions this finding is about, in source order, and the edition that weighed them. floor is the first one that refuses them, as written; absent for W_INTENDED_TARGET_UNSUPPORTED, which is not about a floor. |
E_UNKNOWN_ID.origin says who chose the ID, because the repair differs:
origin |
Meaning | Where the fix goes |
|---|---|---|
authored |
The source names the ID. | The author’s line. |
catalog |
The registry pack maps a token onto it. | The pack’s mapping. |
builtin |
The pack declares no row for a member default, so the compiler’s own ID was used. | The pack, which has to grow the row. |
token accompanies catalog and builtin and is absent for authored. suggestion is absent
when no declared ID is within the typo threshold. A rename is normally past it, but the two fields
are filled by independent rules and a renamed ID near its replacement carries both.
aliases is the other half, and the two are different claims about the same ID. A suggestion is a
guess from a string distance; an alias is the registry pack’s aliases component stating that two
names are one block, so a quick-fix may apply an alias unasked and should not apply a suggestion.
It holds every ID the target declares for that block — the closed set, never a pick from it — in
the pack’s own order, and is absent when the pack names none. The rendered note prints the first
few and counts the rest; the payload is where the whole set lives. See
Versioning and Editions §10.4.
E_INCOMPATIBLE_MATERIAL follows the same idea: slot is the mat_slot= name the member read and
is absent when it carries no binding, and a dotted token (roof.dark_wood) means the pack’s
mapping is what to correct rather than the source line. required is named rather than implied so
that adding a second family later is a new value here, not a new code.
W_TRUTH_TABLE_PARTIAL.missing is a sample rather than the set: twenty inputs have a million
combinations. Take the count from 2^inputs - covered, never from missing.len(). inputs is
carried instead of that total because the grammar puts no ceiling on the input list and no integer
holds 2^130.
11.3 Error vs warning
Section titled “11.3 Error vs warning”- Errors are things that, left alone, produce unintended results: concept absence, unknown IDs, out-of-domain states. Silent substitution and implicit dropping are forbidden.
- Warnings are semantic drift across versions and editions, the non-guarantee of redstone behaviour, and the partial-build degradations the block-array pass reports. In those the compiler rather than the source is the incomplete side.
The E_ / W_ prefix is not the severity, in either direction. Most W_ codes mark a
partial-build degradation, but W_UNUSED_DEF, W_TRUTH_TABLE_PARTIAL and the two @cairn codes
are warnings without being one; and two E_-prefixed codes are decided by the rule above rather
than by their name:
E_UNKNOWN_SLOT_TARGETis an error, because a slot bound to a non-material value lowers every member referencing it to air.E_THEME_SELECTOR_UNMATCHEDis a warning, because a rule that matches nothing overrides nothing.
E_PARSE is one code for every shape a parse fails in — a stray character, an odd indent, an
integer past i64, a keyword where an item was expected — with the message saying which. A code per
shape would make every future one a line of this table, for a distinction nothing branches on; what
that costs is that a consumer reads the message to tell them apart. It is also the only code no
check pass raises: parsing precedes them all, and a source that does not parse reaches none of
them, so it is the one finding a build reports alone.
E_MISSING_MATERIAL is an error by the first rule above rather than the second: the member is
dropped from the build and nothing is put in its place, which is implicit dropping and not a
partial-build degradation. The incomplete side is the source, which named no material, and not
the compiler. The drop does not stop at the member either — a dropped walls lowers the wall
height the volume is derived from, so the structure shrinks and every door and window that
was to be cut into it is deferred as well.
The two @cairn codes are warnings by the same rule read the other way. The header is
provenance: no pass branches on it, no palette entry comes from it, and the lockfile’s
cairn_version records the compiler rather than the file. @cairn banana builds byte-for-byte
what @cairn 2026.06 builds, so nothing about the result is unintended. What is lost is the
header’s own job — being readable by a later compiler — which is worth a word and not worth
refusing the file over. @requires is an error on the same test because its floor is an input:
it sets cairn info’s compatible range and the bound cairn compile --target is held to, so a
floor that evaporates accepts a target it should not.
E_UNKNOWN_ARGUMENT is an error for the same reason E_UNKNOWN_KEYWORD is, one level down.
A key outside the keyword’s vocabulary names nothing, so no pass will read the value however the
compiler grows, and the member is built without whatever was being asked for. A misspelled argument
that has a default is the worst of them: the build succeeds, at the default, and says nothing.
Each keyword’s vocabulary is closed, and a theme selector widens the one it names — writing
window[tags=...] in a theme makes tags= a key something reads on a window, and on nothing else.
The reverse direction is E_THEME_SELECTOR_UNMATCHED. A selector coins words; one edit away from a
word the keyword already has is a typo written twice rather than a coinage, and is refused with the
suggestion. Widening admits the word and nothing more: the match hands the member the row’s
bindings, which are reserved (Materials and Themes), so a key the
keyword defines and no pass reads is still an unreached key when a selector matches on it.
A member’s own [key=value] answers to that same vocabulary. window[clas=outer] is the same
defect as window clas=outer — a word the author expects something to read that nothing does, with
the class lost either way — so it earns the same code and the same suggestion. What the selector
means is a separate question and is not settled here: a member’s selector is carried through
verbatim and later passes decide whether one binds a fresh id or references an existing member.
This check does not need that answer, because the word is one something reads or it is not,
whichever the answer turns out to be. Note that a key in a member’s own bracket does not make the
member carry the attribute, so a theme row selecting on it matches nothing.
A struct / def header line answers to a closed vocabulary of its own, the one Components,
Editing, and Multi-building states: size=, which lowering
reads, and class=, which no pass reads yet. No theme selector widens it, since a selector names
a member keyword. A key outside the two is the same defect one line up from a member’s, and is
refused with the suggestion: struct s siz=7x7 is offered size. The missing-size warning the
same typo causes cannot name it, and is raised only where block-array lowering runs, which
cairn check does only with both --edition and --target; without this check a plain
cairn check would say nothing about the line. Where both are raised, the error is printed before
the warning.
W_IGNORED_ARGUMENT is a warning, and covers three things. An unreadable value: a key=
in the vocabulary whose value the pass cannot read is dropped and a default put in its place. An
unreached key: a key= this specification defines that no pass reads yet — window shape= /
anchor=, roof footprint= / bounds= and a header’s class= are those keys today — is carried
into the IR and never consulted. Every key=value on the right of a theme selector row whose
keyword the compiler knows is one too, reported on the binding whatever its key or value, since no
pass lowers a selector’s bindings yet (Materials and Themes). And a key
routed past: one the keyword reads only under some ways of writing a sibling argument, on a
member that writes it another way. The boundary is the keyword: a spec-defined key on a keyword the
compiler knows is reported this way, while a spec-defined keyword it does not know is
E_UNKNOWN_KEYWORD and its arguments are not judged at all. All three make the build differ from
the source. The rule forbids silent substitution, and all three are announced. For the unreached
key the gap is the compiler’s rather than the source’s, which is why it is not a refusal. Whether
autofix is offered is up to the implementation.
An unreadable value is reported whether or not its member is then built. The value is wrong
wherever the member ends up, so it is a repair of its own, and holding the finding back until a
refusal on the same line is repaired only costs the author another compile. The note says which
happened: what the default did to a member that is built, or that the member is not built either
way and the refusal beside it is why. A member dropped before its arguments are read — a roof
under a raised level — is the exception today: its values are not read, so they are not reported
beside the drop. Most values are read only during block-array lowering, so a cairn check with no
--edition / --target, and the language server, report an unreadable value only for the keys the
check passes read themselves (§11.1).
The routed-past shape is the vocabulary’s second axis: closed per keyword and per the way the
argument that selects the lowering rule is written. roof slope_to= is read by the kind=shed
rule and by none of the others, so
roof kind=gable slope_to=frontbuilds a roof that ignores the direction it was pointed in — the same silent drop a key outside the
vocabulary makes, one argument down. It stays a warning rather than becoming the refusal
E_UNKNOWN_ARGUMENT is, because the repair is not decided: the key names something real on a
keyword that reads it, and either the kind= is the argument that was meant or the slope_to= is
left over. The message names both sites and picks neither.
An arm of that axis can also be the selector’s absence, where that is itself a rule rather than a
mistake. place gap= is the case: a row with no at= is placed relative to another and reads the
distance, and at=origin is anchored absolutely and does not, so at=origin gap=5 is the same
silent drop (§9.3.2).
Where the selector names no rule at all — a value the dispatch does not know, or, on an axis
with no absent arm, nothing written — no finding is raised here: that member lowers to nothing and
W_DEFERRED_MEMBER is the whole repair, so a second finding about the argument that rule would not
have read bills one repair twice. That deferral is raised during block-array lowering, so a cairn check with no --edition / --target reports neither (§11.1); the case
that is always reported is the one where the member builds. A key another key makes inert without
selecting a rule is a different shape again and is not reported today: window step= at repeat=1
is a condition on a count rather than on a rule.
E_MISPLACED_BINDING is the fourth field of a member line asked the same question the other three
are: is this word read by anything. A -> value tail is read by exactly one thing — the sensor set
of §14.2 — so a tail on a member that is not a sensor is carried into
the IR and dropped, and the signal it names is emitted by nothing. It is an error on the same
test E_UNKNOWN_ARGUMENT meets: the build differs from the source, and no edit to the value
repairs it.
Only the host is asked here, because only the host can be asked without the Logic IR. Whether the
tail’s value names a signal, and whether that signal is driven twice or by nobody, are questions the
redstone pipeline answers (§14.2) — and the host is asked first, so a
tail on a member that cannot emit earns this code and not also the value’s. A member whose keyword
is not in the known-keyword table is left to E_UNKNOWN_KEYWORD, which is why lever -> sig.a —
a sensor this specification lists and the surface has not reached — is not told its host is wrong.
11.4 Constraint catalog
Section titled “11.4 Constraint catalog”In-game constraints are cataloged and managed per version: gravity blocks, attachment conditions, fluid flow, and disallowed attachment combinations (Versioning and Editions). “A frame cannot hang on glass” lives there.