Types
Scrii is dynamically typed: values carry their type at runtime, and any variable can hold any type.
| Type | Example | Notes |
|---|---|---|
int | 42, -7, 0 | 32-bit signed integer |
long | 3000000000L | 64-bit signed integer (L/l suffix) |
long | 0xFF, 0b1010, 0o17 | Prefixed forms are also long |
float | 3.14, 1e5F | 32-bit float (decimal or F/f) |
double | 3.14D | 64-bit float (D/d suffix) |
string | "hello" | Double-quoted text |
char | 'a' | Single character (stored signed, identical on all platforms) |
boolean | true, false | Boolean |
nil | nil | Null value |
array | [1, 2, 3] | Ordered, heterogeneous list |
object | {name: "Bob"} | Key-value pairs |
function | fn(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
| Syntax | What it does | Example |
|---|---|---|
obj.name | Dot — 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 members | s:upper() · arr:sort() |
obj["key"] | Bracket — property access by string key | obj["version"] |
arr[0] | Array indexing | arr[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).