forma-dsl docs
Home Editor GitHub

Errors#

Every error the language raises, grouped by the stage that raises it. Knowing the stage narrows the cause: a lexer error is a typo, an evaluator error is a mistake about meaning.

All of them carry a source position where one is known, appended to the message and available as error.loc:

try {
  await render(source, { context });
} catch (error) {
  error.loc?.line;     // undefined when the error has no position
}

Most are a FormaError. A few come from the kernel or from render itself and are plain Errors, so test error.loc rather than the class.


Lexer#

Raised while turning characters into tokens. Always a typo.

Message Cause
unexpected character "…" A character that cannot begin a token - a smart quote, a stray backtick, a non-ASCII dash
unterminated string A " with no closing ", or a literal newline inside one
unterminated block comment A /* with no */. Block comments do not nest
unknown escape \q Only \n \t \r \" \\ \$ exist
unterminated ${ } in string A splice with no closing brace

Parser#

Raised while turning tokens into a tree. Always a grammar mistake.

Message Cause
expected a block, found "…" Something at the top level that is not a block
unknown top-level block "shape" Only param, local, component and model may appear at the top level
expected an attribute or block, found "…" Inside a body, an entry that does not start with a name
expected {, found "…" A param with no = and no body - write param s { }. Also a component or model with no body
expected =, found "…" A local with no value
expected ident, found "…" A keyword where a name was required
"for" cannot be used as a block type One of the eleven keywords used as a block name
a param block holds attributes, not nested blocks Usually a missing =: param h { default 20 }
a label cannot contain ${ } component and model names must be static. part labels may be expressions
expected = or : in an object An object literal entry with no separator
unexpected "…" A token that cannot begin an expression
trailing input in ${ } A splice holding more than one expression

expected {, found "box"#

The most common parser error, and the least obvious. A component param needs = or a body, even an empty one:

component "c" {
  param s          // ✗ the parser is looking for `=` or `{`
  box { size = [s, s, s] }
}

component "c" {
  param s { }      // ✓ declared, and required at the call site
  box { size = [s, s, s] }
}

Declarations#

Raised when the parsed document is indexed.

Message Cause
param "a" is declared twice Two top-level params share a name
component "c" is declared twice Two components share a name
model "m" is declared twice Two models share a name
component "box" shadows a builtin block Components and builtins share one namespace

Locals are the exception: a duplicate local is accepted and silently replaces the first.


Names and expressions#

Raised while evaluating.

Message Cause
unknown name "x" Nothing in scope binds it. See Scope
no attribute "x" A missing key on an object - including var.x for something that is not a param
cannot read "x" of nothing A property read on null
cannot index this value Indexing something that is not a list, string or object
cannot negate a non-number Unary - on a string, list or bool
cannot apply + to a list and a number Operands the operator does not accept. See the operator table
cannot compare a number with a string < <= > >= require both sides to be the same type
cannot combine vectors of length 2 and 3 Componentwise arithmetic on lists of different lengths
division by zero · modulo by zero Errors rather than Infinity or NaN, so a degenerate value cannot reach the kernel
unknown function "clamp" No such builtin, and there are no user-defined functions
len takes 1 argument(s), got 2 Wrong argument count for a fixed-arity function
pow: functions take positional arguments Named arguments are not supported
range: step cannot be 0 A zero step would never terminate
range: more than 100000 values A guard against a live editor allocating until the tab dies

unknown name "outer-inner"#

Hyphens are part of an identifier, so outer-inner is one name. Put spaces around the operator: outer - inner. See Identifiers.

unknown name "var"#

A param default referred to var, which is assembled only after every param has resolved. Use the earlier param's bare name instead - see the root scope.


Params#

Message Cause
param "h" has no default and no value was supplied A required param with no value. Pass one, or give it a default
model "m": param "s" needs a default Model params cannot be set from outside, so a default is the only way they get a value

Blocks#

Raised while building geometry.

Message Cause
unknown block "bloop" - the blocks are … Not a builtin and not a declared component. The message lists every block there is, this document's own components first
"box" does not take a label Only part takes a meaningful label
"box" is a shape and cannot contain other blocks A shape is a leaf. Usually a missing } swallowed the next block
"translate" needs at least one shape inside it An operation with nothing to act on
box: unknown attribute "colour" - box reads … Every block rejects attributes it did not read, so a typo fails loudly, and names what it would have read
attribute "size" is set twice The same attribute written twice in one block
box: "size" must be a number or an array of 3 numbers Wrong shape of value
circle: "radius" must be a finite number A NaN or Infinity reached the block
regular_polygon: "sides" must be at least 3
polygon: "points" must be an array of at least 3 points
circle: give either "radius" or "diameter", not both They mean the same thing
cylinder: give "radius"/"diameter", or both "bottom_radius" and "top_radius" One end radius on its own is ambiguous
offset: "join" must be one of "Square", "Round", "Miter" An enum attribute with an unrecognised value

box: unknown attribute "colour" - box reads "center", "size"#

Worth dwelling on, because it is the error that saves the most time. A misspelled attribute would otherwise be silent, and the model would render subtly wrong with nothing to point at. The one exception is a param block, which is metadata and passes anything through.

Both this and unknown block carry the alternatives, because the reader is often a program with no copy of this manual: an error that says only what is wrong leaves it guessing at spellings. A block that branches on which of two spellings it was given - radius against diameter - reads only one of them, so the list is what that call would accept rather than always the whole set.


Dimensionality#

Message Cause
"union" cannot mix 2D and 3D shapes Children of different dimensionality in one block
"extrude" takes 2D geometry, got 3D A block that fixes what it takes got the other thing
part "body" is 2D - extrude or revolve it before rendering A cross-section reached the renderer
cannot combine 2D and 3D geometry in a union The same mistake, from the node API rather than a document

See Dimensionality for the three rules, and 2D ↔ 3D for the way across.


Components#

Message Cause
c: "s" has no default and was not given A required component param the call site did not pass
c: unknown param "t" Components have no pass-through: every value must be declared
component "c" produced no geometry An empty body, a loop over an empty list, or an if that took no branch
a component cannot declare parts Scene structure belongs in the model
nesting deeper than 64 blocks - is a component using itself? Direct or mutual recursion. Rewrite as a for loop

Parts and scenes#

Message Cause
a part cannot contain another part Parts are a flat list
a part needs at least one shape inside it Put the if around the part, not inside it
model "m" produced no geometry Nothing was produced. Often a for over an empty range
the document declares no model Add a model block
no model named "x" render(source, { model }) named one that does not exist

A part nested inside an operation raises nothing - it is silently dropped. If a part is missing from a render, check it is written directly in the model.


Control flow#

Message Cause
for: expected a list to iterate The sequence is not a list. Strings are not iterable here

align#

Message Cause
align needs a kernel context - render the model rather than building it An Evaluator built with no context. render always supplies one
align needs at least one shape inside it Nothing to measure
align: "x" must be "min", "center", "max" or a number The spelling is "center"

Runtime#

Raised by the JavaScript API rather than by a document.

Message Cause
EvaluationContext has been disposed A solid or a render used a context after dispose()
EvaluationContext used before create() resolved The constructor was called directly. Use EvaluationContext.create()
bad dimensionality 4 new GeometryNode with something other than 2 or 3
expected 3 components, got 2 Vector.of asked for a size the value does not have
cannot read an angle from string Angle.of takes an Angle or a number

Silent behaviours#

Not errors, and each is deliberate - but each has surprised someone. A parameter reaching the end of its range should not break a model, so these degenerate quietly:

What Result
extrude { height = 0 } Empty geometry
revolve { angle = 0 } Empty geometry
A difference that removes everything Empty geometry
A negative offset larger than the shape Empty geometry
range running backwards for its step An empty list
A label on align or a component call Ignored
A supplied param the document does not declare Ignored
min, max, step, options on a param Advisory; never enforced
A duplicate local Replaces the first, everywhere
A part inside an operation Dropped from the scene
An index past the end of a list Nothing, which fails later where it is used

If a model renders but is not what you meant, this table is the place to start.