Tables and arrays
A table is an associative container: a set of slots, each with a key and a value. The key can be any value. An array is a sequence of values indexed from 0.
Table literals
{ key = value, ... } builds a table. A trailing comma after the last field is allowed, so a field added at the end changes only one line in a diff. A key does not have to be a plain identifier: write a quoted string before a : for a key that is not a valid name, or [expr] = value for a key computed at construction time. Braces nest, so a literal can hold another literal as a value.
// a table maps names to values
let squad = { name = "alpha", strength = 4 }
println("squad.name =", squad.name)
// an array holds values in order, reached by position from 0
let spawnPoints = [10, 20, 30]
println("spawnPoints[1] =", spawnPoints[1], "count =", spawnPoints.len())squad.name = alpha
spawnPoints[1] = 20 count = 3// trailing commas are allowed after the last field
let loadout = {
weaponName = "ak74",
ammoBelt = 90,
}
// a key can be a string literal or a computed [expr], not just an identifier
let zone = "north"
let spawnPoints = {
"spawn point count": 4,
[$"spawn_{zone}"] = { x = 120, y = 40 },
}
println("loadout.weaponName =", loadout.weaponName)
println("spawn point count =", spawnPoints["spawn point count"])
println("spawnPoints.spawn_north.x =", spawnPoints.spawn_north.x)
// the comma between array elements is optional
let squadIds = [101, 102 103, 104,]
println("squadIds.len() =", squadIds.len())loadout.weaponName = ak74
spawn point count = 4
spawnPoints.spawn_north.x = 120
squadIds.len() = 4Array literals
[value, ...] builds an array. The comma between elements is optional: [1 2 3] and [1, 2, 3] are the same array. A missing comma next to a unary - or a call can merge two elements into one by accident.
Spread
...expr inside a table or an array literal copies the contents of expr into the literal that is being built. In a table literal the source can be a table, a class or an instance. In an array literal it must be an array. Any other type throws. A null source adds nothing, so an optional part can go into a literal with no branch around it. A class body takes no spread.
The copy is shallow. A slot holding a weakref arrives as the value it points at, the same value a reader of that slot gets. The copy is made in the order the literal is written: a key that comes after a spread replaces the same key the spread brought in, and a spread that comes after a key replaces that key. In an ordinary literal two identical keys written by hand stay a compile error, spread or not. A const initializer does not run that check, and there the later key wins.
The result is a new container, never the source. A spread of a frozen source gives a copy that can be written to, unless the module sets #allow-auto-freeze, which freezes every literal, including this one. Values stay frozen if they came from a frozen source, since the immutable flag follows the reference.
let a = [1, 2]
let b = [0, ...a, 3]
println(b.len())
println(b[0], b[1], b[2], b[3])
let t = { x = 1 }
println({ ...t }.x)
// a key written after the spread wins
println({ ...t, x = 99 }.x)
// the copy is new, so writing to it leaves the source alone
let copy = { ...t }
copy.x = 5
println(t.x, copy.x)
// a frozen source gives a copy that can be written to
let locked = freeze({ y = 1 })
let unlocked = { ...locked }
unlocked.y = 2
println(unlocked.y)
// a null source adds nothing
let none = null
println([...a, ...none].len())4
0 1 2 3
1
99
1 5
2
2A const initializer accepts a spread too, and folds it at compile time. The source must then be a constant of the matching type (a constant table for a table literal, a constant array for an array literal) or a constant null, which adds nothing here too.
Reading and writing a slot
.name and ["expr"] read or write an existing slot the same way on a table, an array (with an integer index), or a class instance. Writing with a plain = requires the slot to exist already. An assignment to a name that is not there yet throws; it does not create the slot. This is by design. A typo in a field name becomes an error instead of a new unrelated slot in the table. <-, the newslot operator, adds a new slot; see Operators and expressions for its full behavior, including why it rejects a bare local variable.
let squadRoster = { alpha = 4, bravo = 9 }
// reading and writing an existing slot works like any other variable
squadRoster.alpha += 1
println("squadRoster.alpha =", squadRoster.alpha)
// a bare name in a table literal is shorthand for name = name
let missionId = "raid07"
let briefing = { missionId, squadRoster }
println("briefing.missionId =", briefing.missionId)
// an array is written the same way, by index rather than by name
let vehiclePark = ["t34", "kv1"]
vehiclePark[0] = "is2"
println("vehiclePark[0] =", vehiclePark[0])
// <- is the only way to add a slot that does not exist yet; see the
// newslot operator on the Operators and expressions page for the full story
squadRoster.charlie <- 2
println("squadRoster.charlie =", squadRoster.charlie)squadRoster.alpha = 5
briefing.missionId = raid07
vehiclePark[0] = is2
squadRoster.charlie = 2<- does not work on an array, even at an index that already holds a value: arr[0] <- 99 throws indexing array with integer every time. Grow an array with append, insert or resize, and write an existing element with plain [i] = value.
Iteration order
foreach over a table (and keys, values, topairs) walks the slots in an order that depends on the table's internal layout, not on insertion order. That order is reseeded for every run, so the same script can print two different orders on two runs of the same build with no code change. Sort the keys first when the order has to be stable; see Control flow for the full foreach pattern. An array keeps the order its elements were given, since it is indexed by position and not by a hashed key.
delete is forbidden
The delete operator is forbidden by default, so delete t.key fails to compile with Usage of 'delete' operator is forbidden. Use 'o.$rawdelete("key")' instead. Call rawdelete, which removes the slot and returns the value that was in it. See Operators and expressions for a runnable example of both sides of that error, and for #allow-delete-operator, which lifts the restriction.
Methods
Both types have a full set of methods reached with .: a table's rawget, rawset, rawin, clear, clone, map, filter and others on the table page, and an array's append, remove, sort, slice and others on the array page. This page covers only the literal syntax and the slot semantics the methods build on.
Edge cases
- A key can be any value, including
null.{[null] = 1}is a legal, one-slot table, and iteration over it works like over any other slot. Only the shorthand below is limited to identifiers. - A bare name inside a table literal is shorthand for
name = name, reading the value from a variable already in scope:{ squadStrength }is{ squadStrength = squadStrength }. This shorthand and the quoted-string key form are both table-only. Neither parses in a class body. clone tmakes a shallow copy: top-level slots are copied, but a table or array nested inside is shared with the original.cloneis a keyword, sot.clone()is a parse error; writeclone t(seeclonefor the full rule, including what it does to a frozen table).freezereturns an immutable reference to a table or array; see Bindings and constants for how the immutable flag follows the reference, not the object.