Skip to main content

Types

Scrii is dynamically typed: values carry their type at runtime, and any variable can hold any type.

TypeExampleNotes
int42, -7, 032-bit signed integer
long3000000000L64-bit signed integer (L/l suffix)
long0xFF, 0b1010, 0o17Prefixed forms are also long
float3.14, 1e5F32-bit float (decimal or F/f)
double3.14D64-bit float (D/d suffix)
string"hello"Double-quoted text
char'a'Single character (stored signed, identical on all platforms)
booleantrue, falseBoolean
nilnilNull value
array[1, 2, 3]Ordered, heterogeneous list
object{name: "Bob"}Key-value pairs
functionfn(x) { x }Callable closure

Use type(value) to get the type name as a string.

print(type(42)) // int
print(type(0xFF)) // long
print(type(3.14)) // float
print(type(3.14D)) // double
print(type("hi")) // string
print(type('a')) // char
print(type([1, 2])) // array
print(type({a: 1})) // object

Arrays

var a = [1, 2, 3]
var mixed = [1, "two", true, nil]

a[0] = 10 // assign by index
print(a[0])
print(len(a)) // builtin: number of elements

// Destructuring
var [first, second] = [10, 20]
print(first, second) // 10 20

Arrays have value semantics on assignment; use ref if you need aliasing. insert and erase are dispatch pipes, so they work on the receiver directly: a:insert(x) appends and a:erase(idx) removes. (len, push, has, keys are global builtins — not pipes — so call them as len(a), insert/has, etc.)

Objects

var obj = {name: "Scrii", version: 26}

obj.name // property access (dot)
obj["version"] // bracket access
has(obj, "name") // builtin (different type result) — true/false

// Object iteration
for ([k, v] : obj) {
print(k, v) // destructured: key string, value
}
for (pair : obj) {
print(pair.key, pair.value) // single variable: {key, value} pair
}
foreach(obj) {
print(each.key, each.value) // implicit 'each'
}

Member access

SyntaxWhat it doesExample
obj.nameDot — read a property, or call a function stored in the object (receiver is not passed)obj.name · obj.hello("Scrii")
obj:method()Pipe (colon) — self operator: receiver:method(args) means receiver = method(receiver, args); return type must match receiver; resolves via the pipe table / globals, not object memberss:upper() · arr:sort()
obj["key"]Bracket — property access by string keyobj["version"]
arr[0]Array indexingarr[0]

Dot calls a function stored in the object; the receiver is not passed. The pipe does pass the receiver as the first argument and auto-assigns the result back when used as a statement:

var obj = {count: 0}
obj:greet = fn(n) { return "Hello " + n } // colon only valid as call, not assignment

// Dot: member function
var m = {hello: fn(who) { return "hi " + who }}
print(m.hello("Scrii")) // correct
// print(m:hello("Scrii")) // error — hello is a member, use dot

// Pipe: self operator on values
var s = "hello"
s:upper() // s becomes "HELLO"
print(s) // HELLO
print("hello":upper()) // error — literals cannot be receivers

var arr = [3, 1, 2]
arr:sort() // arr becomes [1, 2, 3]
arr:sort():reverse() // chained

var obj2 = {tree: "hat", wall: "tree"}
obj2:erase("tree") // obj2 = {wall: "tree"}

if (has(obj2, "wall")) {
print(keys(obj2)) // builtin (different type) — not a pipe
}

Member access must start on the same line as the value (obj\n.name is not a continuation).

Nested objects and arrays

Arrays and objects nest freely — an object value can hold arrays, an array element can be an object, and so on, to any depth.

Building nested structures

Write a nested literal directly, or build it up from an empty root:

var user = {
name: "Ann",
roles: ["admin", "editor"], // array inside an object
profile: {age: 30, theme: "dark"}, // object inside an object
}

print(user.roles[0]) // admin
print(user.profile.theme) // dark

Build programmatically with bracket assignment — empty objects/arrays spring into being as you walk down:

var cfg = {}
cfg["servers"] = {}
cfg["servers"]["primary"] = {host: "10.0.0.1", port: 8080}
print(cfg.servers.primary.host) // 10.0.0.1

// grow an array of objects with the :insert pipe
var rows = []
rows:insert({id: 1, score: 90})
rows:insert({id: 2, score: 75})
for (r : rows) { print(r.id, r.score) }

Accessing deep paths

Mix dot and bracket freely on one chain:

var data = {matrix: [[1, 2], [3, 4]], meta: {tags: ["a", "b"]}}
print(data.matrix[1][0]) // 3 (2D array indexing)
print(data["meta"]["tags"][1])// b
// arrays have no .length member — use len():
// print(data.matrix[0].length) // error: arrays support only index access
print(len(data.matrix[0])) // 2

Mutating nested members in place

Deep dot/bracket chains resolve to the live value, so you can write through them directly — the change is visible on the original:

var user = {profile: {theme: "dark"}, scores: [10, 20]}
user.profile.theme = "light" // object → object member
user.scores[0] = 99 // object → array → element
print(user.profile.theme) // light
print(user.scores[0]) // 99

Pipes reach nested containers through a dot prefix:

var cfg = {list: [10, 20, 30]}
cfg.list:erase(1) // cfg.list is now [10, 30]
cfg.list:insert(5) // cfg.list is now [10, 30, 5]

Copying nested values — deep copy

Assignment deep-copies the entire nested tree. Unlike many languages where b = a would keep sharing the inner objects, here b is a fully independent snapshot, both directions:

var a = {p: {t: "d"}, n: [1, 2]}
var b = a // deep copy

b.p.t = "x" // does NOT touch a.p.t
a.p.t = "CHANGED" // does NOT touch b.p.t
print(b.p.t) // d (still the snapshot)

b[0] = [99] // (arrays copy the same way)
print(a.n[0]) // 1

Sharing nested storage with ref

When you do want two names to see the same nested value, bind a ref — either the whole object or a path into it:

var a = {p: {t: "d"}}
ref whole = a // shares the whole object
whole.p.t = "z"
print(a.p.t) // z

var b = {p: {t: "d"}}
ref deep = b.p // shares just the nested member
deep.t = "z"
print(b.p.t) // z

Inspecting nested structures

The object introspection builtins work at any depth:

var u = {name: "A", meta: {roles: ["x"]}}

print(keys(u)) // ["meta", "name"] (object keys)
print(has(u, "meta")) // true
print(items(u)) // [{"key":"meta","value":{...}}, {"key":"name","value":"A"}]

// walk a nested object
for ([k, v] : items(u)) {
print(k, "->", v)
}

Nesting and std.json

std.json.stringify / parse round-trip arbitrarily nested values losslessly, which is the simplest way to save or clone a deep structure:

var u = {name: "A", nums: [1, 2, {x: 3}]}
var text = std.json.stringify(u)
var v = std.json.parse(text)
print(v.nums[2].x) // 3

:::note restricted names Some names are builtin globals you cannot reuse as variables — for example items, keys, has, len, insert, erase. var items = [...] looks fine but any use of items then fails with MODIFY_RESTRICTED. If you get that error, you are almost always colliding with a builtin name; rename your variable. :::

Value semantics

Arrays, objects, and functions have value semantics; assignment deep-copies the value rather than aliasing it (see Nested objects and arrays). Two reasons for ref:

var a = [1, 2]
var b = a // b is a (deep) copy; mutating b does not affect a
b:insert(3) // b is now [1, 2, 3], a is still [1, 2]

ref alias = a // shared storage — writes and pipes reach a
alias:insert(4) // a is now [1, 2, 3, 4]
print(a) // [1, 2, 3, 4]

A ref shares storage, so both direct writes and pipes on the alias land in the original (see Variables and constants for the full var / const / local / ref comparison).

Truthiness

In conditions, the falsy values are:

nil false 0 0.0 '\0' "" [] {}

Everything else is truthy: true, non-zero numbers, any non-nil char, any non-empty string/array/object, and any function.

Type conversions

var i = int("123") // string -> int
var l = long("9999999999") // string -> long
var fl = float("3.14") // string -> float
var d = double("2.718") // string -> double
var s = to_string(42) // any value -> string

Numeric conversions also accept numeric input without a string round-trip. char is always signed (\xFF is −1 on every platform).