Core language · State & bindings
Declarations & assignments.
Nift v4.1 introduced explicit lexical declarations and assignment. Use := to create a binding and = to update an existing mutable binding; the type inferred at declaration remains stable.
:= declares; = assigns.They are deliberately different operations. Assignment never creates a missing name, which catches typos and makes the lifetime of state visible in the source.
Declare and assign
$[count := 0]
$[count = count + 1]
$[price := 19.95]
$[title := "Nift"]
$[published := true] A top-level declaration or assignment inside $[...] changes state and renders no text. Reading the binding later renders its value normally:
$[visits := 1]
$[visits = visits + 1]
<p>Visits: $[visits]</p> Stable inferred types
The declaration establishes the binding's type. Reassignment must preserve it.
$[count := 0]
$[count = 12]
$[count = "twelve"] Numeric literal typing is lexical. An integer spelling (0, 8) infers int; a fractional or exponent spelling (0.0, 8.0, 8.5, 1e3) infers double, even when the value is integral. The inferred type then stays stable.
$[n := 0]
$[n = 8]
$[f := 0.0]
$[f = 8.5]
$[f = 3] The .0 spelling makes 0.0 a floating-point literal, so f is double: reassigning 8.5 is valid while reassigning the integer 3 is an error.
An arithmetic expression is double when any operand is double, so a double accumulator remains double even when it lands on an integral value:
$[total := 0.0]
$[total = total + 8.5]
$[total = total + 9.0] Arrays, objects and struct instances likewise retain their declared kind. Struct fields have the same stable inferred-type rule.
Lexical scope
Nested constructs run in child scopes. They can read and update an existing outer mutable binding, while a declaration made inside the child disappears when that child exits.
$[visible := 0]
@if(show) {
$[visible = visible + 1]
$[temporary := "inside only"]
}
$[visible]
const and immut
$[const site_name := "Nift"]
$[immut config := {"features":["docs","builds"]}] const prevents rebinding. immut establishes a recursively read-only binding/view contract. An immut view does not make separately reachable mutable storage globally immutable.
Arrays (lists)
Nift calls list-like values arrays, following JSON terminology. JSON arrays existed as data values before v4.1; v4.1 made array and object literals directly declarable in the expression language.
$[names := ["Ada", "Grace", "Linus"]]
$[matrix := [[1, 2], [3, 4]]]
$[users := [{"name":"Ada"}, {"name":"Grace"}]] Use zero-based indexing to read an item and @for to iterate:
<p>First: $[names[0]]</p>
<ul>
@for(name : names) {
<li>$[name]</li>
}
</ul> Nift does not currently expose in-place list methods such as push, pop, append or remove. Collection operations are value-producing transforms: filter, map, sort, slice, reverse and distinct return new arrays rather than mutating their source. If you need a different literal array in mutable state, assign another array of the same type; for data transformation, use the collection operations page.
$[names := ["Ada", "Grace"]]
$[names = ["Ada", "Grace", "Linus"]] This distinction is intentional: lexical bindings can be mutable while Nift's collection transforms remain predictable and non-mutating.
Objects
$[site := {"title":"Example", "draft":false}]
$[sites := [{"title":"One"}, {"title":"Two"}]]
$[site.title]
$[sites[1].title] Objects and arrays can be nested freely. Ordinary JSON object/array values are not structs: use structs when you need fixed-shape state with methods, privacy and reference semantics.
Multiline declarations
@:=(config){
{
"title": "Example",
"navigation": ["Home", "Docs", "About"]
}
} @:=(name){...} is the multiline spelling of $[name := expression]. The outer braces frame one expression; they are not part of the value.
Chained declaration and assignment
:= and = are right-associative, so related state can be initialized or updated in one expression when that improves clarity.
$[a := b := 0]
$[a = b = 5] Prefer separate lines when the chain would make scope or intent harder to read.