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.
  • \uXXXX decodes exactly four hex digits (e.g. \u0041 is A); values
    above 0xFF are rejected (non-BMP/surrogates).
  • An unknown escape such as \q is an error rather than a silent strip.
  • escape_string and parseString are inverse: every string that
    escape_string emits 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
restricted

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).