Identifiers & references

How `#id` names a construct, and the two ways to reach what it names.

##id in the head

Any construct may carry an identifier, written with # in its head before the implicit value: curve#flight, canvas.graph#projectile, @data#islands, @preset#accent. Every construct accepts one, including directives with no use for it.

#One namespace, document-wide

Every #id in a document shares a single namespace, and identifiers resolve in both directions: a reference may point at something declared later. Two constructs claiming the same id is a collision, reported as an error — @preset#apple and curve#apple cannot coexist.

#Bindings

A binding is a reference written bare — #flight, or with a member, #islands.area. It is never resolved to a value at compile time: the pointer survives into the renderer, and whatever consumes it decides what to do with it.

continue: #earlier-environment
cue.trace{ point(x: 0; y: 0) }(path: #flight)
table(data: #islands; columns: island, area)
fit(of: #points; model: linear)

Only where the attribute's declared type is reference, though. Everywhere else those characters are just text: label: is rich text, so text(label: slope = #f.slope) renders the literal words rather than a number. Each reference page names the type of every attribute it takes.

Because nothing is resolved early, a binding is live: it reflects animated attributes, reader-set parameters and derived values as they change.

#Substitutions

{{ }} is the other way to reach a value, and a different mechanism: an expression the compiler evaluates, which is why it works identically in prose, attribute values and code bodies. A group holding one bare name reads as that name's value — a constant, a reference, an attribute — which is what makes it read like a substitution even though the mechanism underneath is arithmetic that happens to have nothing to do.

{{z-expected}}          an @let constant
{{#survey.area}}        a dataset column, as a comma-joined list
{{#flight.colour}}      a plain, unanimated attribute
{{body}}, {{detail}}    reserved — see below

{{body}} and {{detail}} are reserved across the whole constant namespace. Inside an @define template they splice the content of the use's own {...} and [...] sections, so neither name may be declared as a template parameter or an @let constant.

@define:finding{
    note{
        {{body}}
    }
}(
    body: required
)

[attribute]@define `finding`: `body` on node `finding` is a content policy — write `!none()`, `!all()`, `!literal()`, `!children(…)` or `!inline(…)`

A section is declared as a policy, never as an ordinary parameter — and what it is declared as decides where it may be used. Written on a line of its own, {{body}} splices constructs and needs no declaration at all; used inside a line of text it needs one that makes it a value, body: !inline(content: all). So the template above needs no attribute group, and the use supplies the body by writing one — finding{Richness scales with area.}.

A micronode head followed directly by a substitution takes it as the whole body, so !bold{{name}} is !bold{ {{name}} } — without the spaces, which the spaced form leaks into the text it emits. At a run of three or more braces the innermost two open the substitution and the earlier ones are literal, which is how a body containing a reference is written:

!bold{{name}}      the constant, as the body
!bold{ {{name}} }  the same, with a space either side of it
!m{{{k}}}          a body of "{" + the constant + "}"
!bold{{a} b}       an ordinary body — the interior is not a key

Micronodes only. A block head reads {{…}} groups inside it, so quote#q{{cap}} is genuinely ambiguous and compiles as paragraph text. A group abutting an implicit value is never part of that value either — an implicit value is a simple token — so !format:comma{{price}}(places: 2) reads the group as the body.

{{#fit.slope}} is no longer a compile error — reading a value that varies at render time makes the expression bound rather than refused: the compiler cannot finish it, so the tree travels to the renderer and is evaluated there, every frame. The same is true of an animated or reader-manipulated attribute. Derived attributes covers what a bound expression means for a construct that publishes one; a handful of positions the compiler itself has to read still refuse a bound value outright — an @if pair, an id, a @data cell — covered on Expressions.

Every name still has to resolve, though. {{2k}} with no k declared is undefined constant `k`, reported at the line that wrote it, so a mistyped constant name is caught rather than quietly compiling. The one exception is an equation-typed position, where an unresolved name is a free name for the renderer to supply — see Expressions.

#Member access

The . sigil reaches into whatever the identifier names: a dataset's column (#islands.area), a construct's attribute (#flight.colour), or a derived value (#fit.slope). It is the same operation as the namespace access in canvas.graph; the language draws no distinction between the two.

  • Column references must be fully qualified. There is no bare-column shorthand, because attribute values are unquoted and label: island must stay the literal string.
  • Existence is proven at compile time even though values are not — a reference to a construct or member that does not exist is an error, so the renderer never meets a dangling pointer.