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"}]] In v4.3 array elements are full expressions, evaluated from left to right exactly once:
$[x := 10]
$[i := 0]
$[computed := [x, x + 1, i++, i++, "42".to_int()]] 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 4.3 arrays are mutable list values. They support push, pop, insert, remove, clear, first, last, indexOf, contains, size and empty.
$[names := ["Ada", "Grace"]]
$[names.push("Linus")]
$[names.insert(1, "Margaret")]
$[names.contains("Grace")]
$[names.indexOf("Linus")]
$[last := names.pop()] Array search uses structural equality, so nested data values can be found by content. Mutating methods require a mutable binding; const/immut state remains protected. Higher-order transforms such as map, filter and reduce are documented on the collection operations page.
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.
Reference semantics (locations)
:= assigns a root binding. Assigning one aggregate binding to another, or extracting a nested aggregate, creates a location reference: the target names a location, not a frozen snapshot of the value that happened to be there.
$[a := [[1], [2]]]
$[b := a]
$[c := a[0]] A nested reference identifies a location as root binding + normalized path. It does not permanently identify the aggregate that happened to occupy that location when the reference was created.
Structural mutation changes what a retained reference resolves to, so the reference always reflects the live location:
$[a := [[1], [2]]]
$[b := a[0]]
$[a.remove(0)]
$[b] $[a := [[1], [2]]]
$[b := a[0]]
$[a.insert(0, [9])]
$[b[0]] $[a := [[1]]]
$[b := a[0]]
$[a[0] = [99]]
$[b[0]] Rebinding the root, composing paths, and writes through a reference all follow the same rule. A reference into a location that no longer exists fails safely with reference target no longer exists; if a value later occupies that location again, the reference resumes resolving:
$[a := [[1]]]
$[b := a[0]]
$[a = [[7]]]
$[b[0]]
$[a.clear()]
$[b]
$[a.push([3])]
$[b[0]] Independent values with copy()
When you want an independent recursive copy rather than a location, use copy():
$[a := [[1], [2]]]
$[b := copy(a)]
$[b[0][0] = 99]
$[a[0][0]] Identity with same()
same(x, y) reports whether two aggregate references name the same location (same root binding identity and same normalized path). It is not structural equality (==) and not value equality:
$[a := [[1], [2]]]
$[b := a[0]]
$[c := a[0]]
$[same(b, c)]
$[same(b, a[1])]
$[d := copy(a[0])]
$[same(b, d)]
$[a[0] == a[0]] Structs and collections are identity-bearing handles, so same() on them compares handle identity while == compares handle value equality.
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.