An expression referenced a name that was never declared as `param`/`bool`/`choice` state (or isn't a frame variable like `x`/`t`). Typos get a "did you mean" suggestion.
bad
param t = 0
curve f = t2 { color: primary }good
param t = 0
curve f = t { color: primary }Controls (`slider`, `toggle`, `stepper`, `picker`, `button`), timeline `step`s and `label`s refuse a property they do not read, with a "did you mean" for typos. These used to be dropped silently, so a misspelt `label:` or a feature that does not exist (`show:` on a slider) compiled and simply did nothing.
bad
param a = 1 { range: [0, 5] }
slider a { lable: "speed" }good
param a = 1 { range: [0, 5] }
slider a { label: "speed" }Older versions silently turned `None` into `Infinity`, producing baffling off-screen geometry. It's a parse error now — omit the prop instead, or bind it to a real boolean.
bad
curve f = x^2 { show: None }good
bool showF = true
curve f = x^2 { show: showF }The message names the problem and the column in the SMILES string, and the caret in the source points at it. Common causes: a branch `(` that is never closed, a ring digit opened and never closed, an atom with more bonds than its valence allows, or an element outside C N O S P F Cl Br I written without square brackets.
bad
scene plane {
x: [-4, 4]
y: [-3, 3]
aspect: equal
molecule m = "CC(C"
}good
scene plane {
x: [-4, 4]
y: [-3, 3]
aspect: equal
molecule m = "CC(C)O"
}A molecule has two kinds of drawing. `full`, `condensed` and `skeletal` are flat and need a `scene plane` with `aspect: equal`. `ball` is the 3D ball-and-stick model and needs a `scene space3` (where it is the default, so `view` can be left out). Mixing them is an error that names the scene it needs. In space3 `at` takes three numbers.
bad
scene space3 {
x: [-2, 2]
y: [-2, 2]
z: [-2, 2]
molecule m = "C" { view: skeletal }
}good
scene space3 {
x: [-2, 2]
y: [-2, 2]
z: [-2, 2]
molecule m = "C" { view: ball }
}The species is an element symbol and an optional charge, `"Fe"`, `"Fe3+"`, `"O2-"`, for the elements H to Kr. An unknown symbol, an element past Kr, a charge of zero, or a positive charge that takes more electrons than the atom has is an error that points at the column in the string.
bad
scene plane {
x: [-4, 4]
y: [-3, 3]
aspect: equal
orbitals m = "H2+"
}good
scene plane {
x: [-4, 4]
y: [-3, 3]
aspect: equal
orbitals m = "Li+"
}`boxes` writes one subshell as `name:` and one character per box: `0` empty, `1` an up arrow, `2` a pair, `d` a down arrow. An s subshell has 1 box, a p subshell 3 and a d subshell 5, so `3d:2200` is a box short. Every subshell in `boxes` must also be in `shells` when `shells` is given, and `boxes` cannot be combined with `fill` or `remove`.
bad
scene plane {
x: [-4, 4]
y: [-3, 3]
aspect: equal
orbitals m = "Fe" { boxes: "4s:2 3d:2200" }
}good
scene plane {
x: [-4, 4]
y: [-3, 3]
aspect: equal
orbitals m = "Fe" { boxes: "4s:2 3d:22000" }
}A `wrong` line in a `configuration` exercise is a pattern a student might really give, written like `boxes:` on `orbitals` (`3d:11100`, one character per box, `0` empty, `1` up, `2` a pair, `d` down). It is an error when the pattern is the right answer (the compiler works that out from the species), when it is written twice, when its box count does not fit the subshell, or when it names a subshell that is not drawn.
bad
lesson "L" {
slide "Fe3+" {
id: "fe3"
configuration {
ask "Fill the boxes."
species: "Fe3+"
wrong "4s:0 3d:11111" "Not quite."
}
}
}good
lesson "L" {
slide "Fe3+" {
id: "fe3"
configuration {
ask "Fill the boxes."
species: "Fe3+"
wrong "4s:2 3d:11100" "The 4s electrons leave first."
}
}
}A reaction keeps every atom, so each element (hydrogens included, the ones a bracket atom carries too) and the total charge must be the same on both sides of `>>`. The message lists the elements that differ and their counts. Usually a product or a reactant is missing, or a bracket atom has the wrong hydrogen count (`[CH2:1]` has two, `[C:1]` none).
bad
scene plane {
x: [-6, 6]
y: [-3, 3]
aspect: equal
param p = 0 { range: [0, 1] }
reaction r = "[CH2:1]=[CH2:2].BrBr>>Br[CH2:1][CH2:2]" { by: p }
}good
scene plane {
x: [-6, 6]
y: [-3, 3]
aspect: equal
param p = 0 { range: [0, 1] }
reaction r = "[CH2:1]=[CH2:2].BrBr>>Br[CH2:1][CH2:2]Br" { by: p }
}A number after the colon in a bracket atom says which atom it becomes, so a map is used once on the reactants and once on the products, on atoms of the same element. A map on one side only, a map used twice on a side, and a mapped atom that changes element are all errors. A mapped hydrogen needs a mapped atom to leave or join (`[H:4][Br:5]`). An atom with no map is fine: it fades out among the reactants and in among the products.
bad
scene plane {
x: [-6, 6]
y: [-3, 3]
aspect: equal
param p = 0 { range: [0, 1] }
reaction r = "[CH3:1][CH3:2]>>[CH3:1][CH3]" { by: p }
}good
scene plane {
x: [-6, 6]
y: [-3, 3]
aspect: equal
param p = 0 { range: [0, 1] }
reaction r = "[CH3:1][CH3:2]>>[CH3:1][CH3:2]" { by: p }
}Prism is state-centric, not object-centric — there's no `point.x`. Give each coordinate its own state variable instead.
bad
param p = 0
curve f = p.x
good
param px = 0
curve f = px
`x: [min, max]` is required on every scene (and `y:` too, for `plane`). Without it the renderer has no coordinate system to map to pixels.
bad
scene plane {
grid
axes
}good
scene plane {
x: [-5, 5]
y: [-5, 5]
grid
axes
}A slide is prose? + scene? + exercise? + goal* — exactly zero or one exercise. Two answerable things on one slide is ambiguous for the Check button. Split into two slides instead.
bad
lesson "L" {
slide "s" {
id: "s"
quiz {
ask "pick one"
* "a"
- "b"
}
numeric {
ask "and a number?"
answer: 1
}
}
}good
lesson "L" {
slide "s1" {
id: "s1"
quiz {
ask "pick one"
* "a"
- "b"
}
}
slide "s2" {
id: "s2"
numeric {
ask "and a number?"
answer: 1
}
}
}Exactly one option must be marked `*` — the rest use `-`.
bad
quiz {
ask "pick one"
- "a"
- "b"
}good
quiz {
ask "pick one"
- "a"
* "b"
}Every `slider`/`toggle`/`stepper`/`picker`/`button` writes to a `param`/`bool`/`choice` declared earlier in the same scene — declare the state first.
bad
slider t { label: "t" }good
param t = 0 { range: [-3, 3] }
slider t { label: "t" }A `hotspot` exercise needs exactly one `target rect (...)` or `target circle (...)`.
bad
hotspot {
ask "tap it"
}good
hotspot {
ask "tap it"
target circle (0, 0) { r: 1 }
}Every `row:` cell is either a plain given number or a `blank(<answer>)` — at least one cell across the whole table must be a blank, or there's nothing for the learner to fill in.
bad
table {
ask "fill it in"
row: 1, 2
}good
table {
ask "fill it in"
row: 1, blank(2)
}Each `pair` line is `"left" -> "right"` — both the left and right text are required.
bad
match {
ask "match them"
pair "a"
pair "b" -> "2"
}good
match {
ask "match them"
pair "a" -> "1"
pair "b" -> "2"
}An `onwrong:` target has to name a slide in the same lesson that is marked `hidden: true`. Without `hidden`, the scaffold would also appear on the main path, so every learner would see the remediation whether they needed it or not. Unknown ids get a "did you mean" suggestion.
bad
lesson "L" {
slide "Question" {
id: "question"
numeric {
ask "2 + 2?"
answer: 4
onwrong: "help"
}
}
slide "Help" {
id: "help"
> count on your fingers
}
}good
lesson "L" {
slide "Question" {
id: "question"
numeric {
ask "2 + 2?"
answer: 4
onwrong: "help" retry
}
}
slide "Help" {
id: "help"
hidden: true
> count on your fingers
}
}`expect:` states the derivation of a numeric answer so the compiler can double-check it. If the two don't agree within the exercise's tolerance, the build fails and names both values. That's the point: it catches a mistyped answer before a learner ever sees it. Fix whichever one is actually wrong.
bad
numeric {
ask "What is 3/8 as a decimal?"
answer: 0.385
tolerance: 0.0001
expect: 3/8
}good
numeric {
ask "What is 3/8 as a decimal?"
answer: 0.375
tolerance: 0.0001
expect: 3/8
}Slide ids are derived from the title by keeping `[a-z0-9]` and nothing else, so an Arabic (or any non-latin) title leaves nothing to build an id from. The compiler would fall back to a positional `slide-2`, which silently changes the moment you reorder slides, and slide ids are a durable key: saved notes, recorded attempts and every `onwrong:` target point at them. A title mixing Arabic with latin math is worse, because it keeps only the scraps, so two different slides can both end up as `f-x`. Write the id yourself.
bad
lesson "النهايات" {
slide "المشتقة تقيس معدل التغير" {
> نص
}
}good
lesson "النهايات" {
slide "المشتقة تقيس معدل التغير" {
id: "derivative-intro"
> نص
}
}A slide id is a database key. The answer a student gave, every recorded attempt and the spaced review card for that question are all stored under it. An id derived from the title changes the moment someone rewords the title, and the student silently loses their history with that question. So any slide that carries an exercise has to name its id. Slides with no exercise can still leave it out.
bad
lesson "Adding" {
slide "Two and two" {
numeric {
ask "2 + 2?"
answer: 4
}
}
}good
lesson "Adding" {
slide "Two and two" {
id: "add-2-2"
numeric {
ask "2 + 2?"
answer: 4
}
}
}A `kind: "bank"` lesson lets the student jump between questions from a grid, so there is no single path for a detour to leave and rejoin. A wrong answer in a bank shows the worked solution instead. Put the fix in the `!` and end it with a link to the lesson that teaches the idea.
bad
lesson "Practice" {
kind: "bank"
slide "Q1" {
id: "q1"
cat: "Round 1"
numeric {
ask "2 + 2?"
skill: "add"
answer: 4
onwrong: "help"
}
}
slide "Help" {
id: "help"
hidden: true
> count on your fingers
}
}good
lesson "Practice" {
kind: "bank"
slide "Q1" {
id: "q1"
cat: "Round 1"
numeric {
ask "2 + 2?"
skill: "add"
answer: 4
! "Two and two make four. [Adding, from the start](lesson:add-1)"
}
}
}`reading:` checks a question against the circuit solver, so the slide needs a `scene circuit` to read from. On any other slide there is nothing to solve.
bad
lesson "L" {
slide "Q" {
id: "q"
quiz {
ask "How much current flows?"
reading: I_L
* "2 A" { value: 2 }
- "3 A" { value: 3 }
}
}
}good
lesson "L" {
slide "Q" {
id: "q"
scene circuit {
x: [-1, 9]
y: [-1, 5]
battery B = (0, 0) -> (0, 4) { emf: 12 }
lamp L = (0, 4) -> (0, 0) { ohm: 6 }
}
quiz {
ask "How much current flows?"
reading: I_L
* "2 A" { value: 2 }
- "3 A" { value: 3 }
}
}
}The compiler works the IUPAC name out from the `molecule` SMILES, the way `expect:` checks a number, and refuses an `answer:` that folds to something else. The usual cause is the wrong SMILES (a branch in the wrong place) or a name written with the wrong numbering. Delete the `answer:` line to use the worked-out name, or list a different spelling the book allows in `accept:`.
bad
name {
ask "Name this."
molecule: "CC(C)CC"
answer: "pentane"
}good
name {
ask "Name this."
molecule: "CC(C)CC"
accept: ["isopentane"]
}The naming code covers chains, rings, halogens, alcohols, acids, esters, ketones and the like in C, H, O, N and the halogens. For a structure outside that (sulfur here) it returns nothing, and the exercise has no answer to check against until you write one.
bad
name {
ask "Name this."
molecule: "CS"
}good
name {
ask "Name this."
molecule: "CS"
answer: "methanethiol"
}Names are compared after folding capitals, spaces, hyphens and commas, so `2-Methylbutane` and `2 methyl butane` are the same name. A `wrong` line that folds to the right answer (or to an `accept:`) would mark a correct student wrong.
bad
name {
ask "Name this."
molecule: "CC(C)CC"
wrong "2 methyl butane" "Close."
}good
name {
ask "Name this."
molecule: "CC(C)CC"
wrong "3-methylbutane" "Number from the end that gives the smaller number."
}In a `scene magnet` a wire with one point is a cross-section of a long wire going through the page, and its field turns clockwise or counter-clockwise depending on whether the current runs into the page or out of it. Say which with `flow: in` or `flow: out` (or the name of a `choice` with those options). A wire with two points lies in the page and runs from the first end to the second, so it takes no `flow:`.
bad
scene magnet {
x: [-6, 6]
y: [-4, 4]
wire W = (0, 0) { I: 3 }
}good
scene magnet {
x: [-6, 6]
y: [-4, 4]
wire W = (0, 0) { I: 3, flow: out }
}A probe `P` gives `B_P`, `Bx_P`, `By_P`, `Bz_P`, and `B_W_P` for the field wire `W` makes at it. A reading for a probe or wire that does not exist (a typo, or a probe never drawn) is an error with the nearest name offered. A reading cannot also be used to place a wire, a field or a probe: it is worked out from them, so use a `param`.
bad
scene magnet {
x: [-6, 6]
y: [-4, 4]
wire W = (0, 0) { I: 3, flow: out }
probe P = (3, 0) { arrow }
label at (0, 3) = "B = ${B_Q}"
}good
scene magnet {
x: [-6, 6]
y: [-4, 4]
wire W = (0, 0) { I: 3, flow: out }
probe P = (3, 0) { arrow }
label at (0, 3) = "B = ${sci(B_P, 2)} T"
}Two cross-sections drawn at the same point would hide each other, and the field of the pair would be one wire with the currents added. Give them different places, or use one wire with the total current. A wire that a `param` moves is allowed to cross another one: the field there is capped, never infinite.
bad
scene magnet {
x: [-6, 6]
y: [-4, 4]
wire A = (0, 0) { I: 3, flow: out }
wire B = (0, 0) { I: 2, flow: in }
}good
scene magnet {
x: [-6, 6]
y: [-4, 4]
wire A = (-2, 0) { I: 3, flow: out }
wire B = (2, 0) { I: 2, flow: in }
}