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
Literals
Numbers
# integers -> int (32-bit signed)
42
-7
0
# prefixed integers — always produce a long, whatever the size
0xFF # hex 255
0b1010 # binary 10
0o17 # octal 15
-0x10 # negative hex is parsed as a unary minus on the literal
# longs — L/l suffix (64-bit signed)
3000000000L
2000000000l
# floats — decimal point or F/f suffix (32-bit)
3.14
1.0f
3.14F
# doubles — D/d suffix (64-bit)
3.14D
1.0d
# scientific notation (decimal mantissa or none; float unless suffixed)
1.5e10 # float
2.0E-3 # float
1e5 # float (100000 — the exponent counts without a decimal point)
5E+2 # float (500)
1e5D # double (suffix picks the type)
A name immediately after a number is an error — there is no implicit
multiplication. Write the operator explicitly:
var y = 9
var x = 99 * y # correct: 891
# var x = 99y # error: unexpected 'y' after number '99'
Strings and chars
"hello world" # string
'c' # char: exactly one character
'"raw\nstring"' # raw string literal: \n stays literal (see below)
String and character escapes
Inside "..." strings (and 'c' char literals), a backslash starts an
escape sequence that decodes to a single character:
| Escape | Produces | Notes |
|---|---|---|
\n |
newline (0x0A) | |
\t |
tab (0x09) | |
\r |
carriage return (0x0D) | |
\" |
" |
the only way to embed a double quote |
\\ |
\ |
|
\/ |
/ |
JSON-style; useful when embedding paths/URLs |
\b |
backspace (0x08) | |
\f |
form feed (0x0C) | |
\0 |
NUL byte (0x00) | preserved inside strings — len("a\0b") is 3 |
\a |
bell (0x07) | |
\v |
vertical tab (0x0B) | |
\uXXXX |
the byte 0xXX |
exactly 4 hex digits; values above 0xFF are rejected |
print("line1\nline2") # line1 <newline> line2
print("tab\there") # tab
print("quote: \" end") # an embedded double quote
print("\u0041") # "A"
Notes:
- A
'needs no escaping inside double quotes — just write"don't".
(\'is not a valid escape and throws.) - Char literals (
'c') decode only\n \t \r \\ \' \0 \a \b \f \v—
notably not\uXXXX,\"or\/. Anything else silently yields
the character after the backslash ('\u0041'is'u', not'A'), so for
anything beyond the basic set, write a double-quoted string instead. \uXXXXdecodes exactly four hex digits (e.g.\u0041isA); values
above0xFFare rejected (non-BMP/surrogates).- An unknown escape such as
\qis an error rather than a silent strip. escape_stringandparseStringare inverse: every string that
escape_stringemits re-parses to the same value.- Raw strings (
'"..."', below) decode nothing — use those when backslashes
should stay literal.
Raw strings — escapes do not apply
To take text literally (including backslashes), wrap it in a raw string
literal using '" … "' (a single quote followed by a double quote on each
end). Inside a raw string, backslash escapes like \n are not decoded —
they print verbatim:
print("this\nline") # regular string: \n is a newline
print('"this\nline"') # raw string: \n prints as "\n"
this
line
"this\nline"
The two quote forms in short:
| Syntax | Kind | Backslash escapes |
|---|---|---|
"..." |
Regular string | Decoded (\n → newline) |
'"..."' |
Raw string literal | Literal (\n prints \n) |
Adjacent string literals are concatenated:
var paragraph = "This is a long string that "
"spans multiple lines."
print(paragraph)
# This is a long string that spans multiple lines.
The joins are exact — no whitespace or newline is inserted between the
literals, and escape sequences do not apply across the join. Source
layout is ignored: indentation and line breaks between the literals vanish
(they are outside any literal), so only an explicit \n inside a literal
produces a line break:
var two = "first line\n"
"second line"
print(two)
# first line
# second line
Booleans, nil, arrays, objects
true
false
nil
[]
[1, 2, 3]
[1, "two", true] # arrays are heterogeneous
{}
{name: "John", age: 30}
{"key with spaces": 1}
{nested: {inner: true}}
Arrays and objects are covered in depth below — Arrays,
Objects, and Nesting.
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 come in two forms (see Builtins):
the bare builtin is pure (b = insert(a, x) returns a new array, a
unchanged) while the pipe form writes back (a:insert(x) appends in
place). There is no push builtin — appending is insert/a:insert(x).
len, has, and keys are globals only, so call them as len(a).
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["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 colon is something else entirely — a pipe, which passes the receiver
as the first argument (see Pipes). Calling a member with a colon
is an error, and the interpreter tells you so:
# 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
For pipes (receiver:name(args)), see Pipes — including the
conversion pipes (flt:int()) that change the receiver’s type.
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.n[0] = 99 # element write stays inside the copy
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
Re-seating a ref with &
A ref normally writes through to whatever it points at — but assigning
an address (built with the unary & operator) moves the alias itself to
different storage:
var a = 4
ref b = a
var c = 9
b = &c # b now aliases c, not a
print(b) # 9
b = 100 # writes through to c
print(c, a) # 100 4
& takes the address of any writable location — a variable or a member
path — and produces an alias value. var x = &c declares x as an alias
of c in one step. Assigning an alias into an existing plain variable
converts it (four = &nine makes four an alias) — the & sigil is the
explicit opt-in, so this is intended rather than silent; later plain-looking
writes then reach through to the shared storage.
Erasing a name tombstones its shared storage: every alias of an erased
binding reads nil afterwards, instead of orphaned data. This holds whether
the original or an alias is erased, and for member keys as well as
top-level names:
var a = 4
ref b = a
erase("a")
print(b) # nil — b dangles visibly, not silently
When a write through an alias hits a const (or restricted) barrier, the
error names the alias you used and points at the real owner:
var a = 4
const b = 100
ref c = b
c = a # REASSIGN_CONSTANT: Cannot reassign constant: c
# (c is an alias of constant storage) — b holds the const
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
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, identically on every platform (there is no \x
escape — write non-ASCII bytes with "\u00ff"-style double-quoted strings).