Scope and names#
Every name in a document resolves through a chain of scopes. This page says exactly what is in each one, and in what order they are built - which is what determines whether a declaration may refer to another.
The root scope#
Built once, before any geometry, in this order:
- Constants -
piande. - Type names -
number,string,bool,vector,list,angle, each bound to itself as a string, sotype = numberin a param block resolves with no quoting. - Params, in source order. Each is bound under its bare name as it is resolved, so a later param's default may refer to an earlier one.
var- an object holding every resolved param.- Locals, in source order. Each is bound as it is resolved, so a later local may refer to an earlier one.
The order has two consequences worth knowing:
param a = 3
param b = a * 2 // ✓ an earlier param, by its bare name
param c = var.a * 2 // ✗ unknown name "var" - var does not exist yet
local d = var.a + 1 // ✓ locals are built after var
local e = f // ✗ unknown name "f" - a later local
local f = 2
var. versus the bare name#
Every param is reachable two ways: bare, and under var.
param wall = 2
model "m" {
box { size = [wall, var.wall, 1] } // the same value, twice
}
They differ when something shadows the bare name. var holds params only - never
locals, never component params, never loop variables - so var.NAME is the unambiguous way
to reach an input:
param a = 1
local a = 2 // shadows the bare name
model "m" {
box { size = [a, var.a, 1] } // [2, 1, 1]
}
Reading a local through var fails, because it is not there: no attribute "a".
Prefer var. in models for anything that came from a param. It is the convention the
editor's examples follow, and it makes an input visually distinct from a local at the point
of use.
Body scopes#
Every block body - a model, a component, a builtin block, a for iteration, an if
branch - evaluates in a fresh scope nested inside its parent.
Locals declared in a body are visible to the whole body, not only to what follows them, because every local in a body is resolved before any of its blocks are built:
model "m" {
box { size = [s, 1, 1] } // ✓ s is already bound
local s = 4
}
That is the opposite of the top-level rule, where locals are strictly ordered. Within one body, ordering between locals still applies - a body local may not refer to a later body local.
Component isolation#
A component body gets a fresh scope rooted at the document
root, not at the call site. A name that happens to exist where the component is used does
not leak into it.
local g = 7
component "c" {
param s = 1
box { size = [s, g, 1] } // ✓ its own param, and a document local
}
model "m" {
local q = 5
c { } // c cannot see q
}
Inside a component body, these are in scope:
- its own
params, bound from the attributes at the call site - everything in the root scope - constants, type names, document params (bare and via
var.) and document locals - its own body locals
Nothing from the caller. Values travel in through params and no other way.
Model params#
A model may declare its own param blocks. They behave like locals with a declaration
syntax, not like document params:
- Each must have a default -
model "m": param "s" needs a default. - They are not settable from outside.
render(source, { params: { s: 9 } })sets document params only; a model param keeps its default. - They are bound in the model's scope under the bare name, and are not added to
var.
model "bracket" {
param thickness { default = 4 } // a fixed constant for this model
box { size = [20, 20, thickness] }
}
For an input the caller can change, declare the param at the top level instead.
Loop variables#
for binds one or two names in a child scope, live for one iteration:
for size in sizes { ... } // size is the value
for i, size in sizes { ... } // i is the index, size the value
The names shadow anything of the same name in an enclosing scope, and are gone after the loop.
Shadowing summary#
Innermost wins. From outermost to innermost:
constants and type names
└─ params (bare) · var · locals
└─ model scope: model params, model body locals
└─ block body locals
└─ for-loop variables
A component body cuts the chain: it starts again from the root scope regardless of where
it was called.
What cannot be shadowed#
- Builtin block names. Declaring
component "box"is an error:component "box" shadows a builtin block. Components and builtins share one namespace so a call site reads the same either way. - Builtin function names. There are no user-defined functions, so nothing can shadow them.
- Duplicate declarations of the same kind are errors:
param "a" is declared twice,component "c" is declared twice,model "m" is declared twice. Locals are the exception: a secondlocal ais accepted and silently replaces the first everywhere, including in code written above it. Nothing warns about it, so treat it as a mistake to avoid rather than a feature.