Syntax Overview

Comments

Comments exist only in the source file for human readers. The tokenizer
discards them entirely — they never reach the parser or runtime, so they take
no memory, do not affect execution, and cannot be read back from the running
program.

# Line comment — the only comment syntax. Runs to the end of the line.
var x = 1   # trailing comments work too

There are no block comments. A leading #! line is just a comment, so
scripts are directly executable via a shebang:

#!/usr/bin/env scrii
print("runs as ./hello.scr")

Variables and constants

Scrii has two declaration keywords that differ in mutability; aliasing
(sharing storage between names) is an operation, not a keyword — you opt in
with the & (address-of) operator:

Keyword Mutability Scope / behavior
var mutable lives in the block where you wrote it — top level = global; inside a block/function = local to it
const immutable same scoping as var, but cannot be reassigned or its members changed

var — mutable, scoped

A mutable binding. Where it lives depends on where you declare it:

var x = 42               # top level  -> a GLOBAL, visible everywhere
var empty = nil          # declared without a value -> nil

fn report() {
  print(x)               # 42 (global is visible inside functions)
}

var inside = 0
if (true) {
  var inside = 9         # block-scoped: shadows the outer `inside`
  print(inside)          # 9
}
print(inside)            # 0 (the outer binding was not touched)

var inside a block or function is local to that scope — it shadows any
outer name of the same spelling and the outer value is restored when the scope
ends. At the top level, var creates a global that the whole script (and
functions called from it) can see.

const — immutable

Same scoping as var, but the value is deeply immutable: you can neither
reassign the name nor modify its members, at any depth:

const pi = 3.14
# pi = 3.0              # error: REASSIGN_CONSTANT

const user = {name: "Ann", roles: ["admin"]}
# user = {name: "Bob"}  # error: REASSIGN_CONSTANT
# user.name = "Bob"     # error: MODIFY_CONSTANT (members are locked too)

Use const for values you treat as fixed — config knobs, computed-once
constants — so a later accidental write fails loudly.

Restricted names

restricted is a read-only marker that the script cannot change, applied
from the outside rather than declared: builtins, the whole std object, and
any value a host or plugin registers are restricted. A restricted name can be
read and called, but never reassigned or modified (at any depth):

print(len("abc"))        # calling is fine
len = 5                  # error: MODIFY_RESTRICTED

std.file.read("a.txt")   # reading module members is fine
std.file = 0             # error: MODIFY_RESTRICTED

The difference from const is who decides:

  • const is something you declare — your own value, locked by choice.
  • restricted is imposed on you — the engine’s builtins (len, keys,
    items, …), the std library, and anything the host injects as
    restricted. You get REASSIGN_RESTRICTED / MODIFY_RESTRICTED if you try
    to write to one.

Colliding with a restricted name is the usual cause of MODIFY_RESTRICTED:
var items = [...] fails at the declaration itself (items is already a
builtin) — rename the variable.

The host side is covered in Embedding — restricted and const
values
.

& — shared storage (an alias)

var x = &y does not copy the value — it binds x to the same storage
as y, so reads and writes through the alias reach the original:

var x = 42
var alias = &x           # alias and x share the same storage
alias = 99               # writes through to x
print(x)                 # 99

var user = {count: 0}
var u = &user
u.count = 5              # member write reaches user
print(user.count)        # 5

& requires an lvalue (&x, not &42 — and the slot must already
exist). Because function arguments are passed by value (a deep copy),
& is how you share or mutate state that lives outside the current
function: pass &state and writes inside the callee reach the caller.

Rebinding — references as values

A plain = on an alias writes through — it never rebinds. r = y
stores y’s value into r’s target; it does not make r point at y:

var x = 1
var y = 2
var r = &x
r = y                    # writes 2 through into x (r still aliases x)
print(x)                 # 2

To point a name at different storage, assign a reference value built
with the & (address-of) operator. &x boxes x’s storage and yields a
reference to it — assigning that reference replaces the slot instead of
writing through, in both directions from then on:

var x = 1
var y = 2
var r = &x               # r aliases x (and x is linked into the shared box too)
r = &y                   # rebind: r now aliases y, x is untouched
r = 100                  # writes through to y
print(x)                 # 1
print(y)                 # 100

This works even when the slot starts out plain — assigning &target
converts it:

var four = 4
var nine = 9
four = &nine             # four is now an alias of nine...
print(four)              # 9
print(four == 9)         # true
four = 100               # ...so later writes reach through
print(nine)              # 100

The rule is uniform: a plain value on the right of = always writes
through; a reference (something built with &) on the right always
rebinds. & needs an lvalue (&(a + b) is an error).

Aliases and pipes

Aliases share storage, so a pipe called on the alias writes through
to the referenced value, just like a direct write:

var a = [1, 2]
var r = &a
r:insert(4)                # writes through the alias
print(a)                   # [1, 2, 4]

Because r and a share the same underlying array, further pipes keep
accumulating in the same storage:

r:insert(9)
print(a)                   # [1, 2, 4, 9]
print(r)                   # [1, 2, 4, 9]

Rules

  • A declaration keyword (var/const) must precede the first
    assignment to a new name. Reassignment uses a bare x = 100 and the name
    must already exist. (Alias targets work too: var o.k = &inner binds the
    nested slot.)
  • var/const live in the block where you wrote them — at the top level
    that block is the whole script, so top-level declarations are global;
    inside a function/block they are scoped to it.
  • Variables declared inside a block (if/while/for/else or a standalone
    {}) are local to that block — including a C-style for header: the
    header variable is scoped to the loop and closures in the body capture it
    per iteration (see Control Flow).
  • & requires an lvalue, and an alias shares storage until it is rebound
    with another & assignment. Aliasing cannot be combined with
    destructuring (var a, b = ... copies elements; array elements that are
    already references alias through).
  • Assignment copies values deeply; see
    Nested objects and arrays and
    Value semantics in Types.

Destructuring declarations unpack an array in one step — a comma-separated
name list (two or more names) before the =:

var a, b = [1, 2]        # a is 1, b is 2
const x, y = [10, 20]    # immutable bindings
# var a, b = 5          # error: value must be an array
# var a, b = [1]        # error: not enough elements

A single name binds the whole array. Extra array elements beyond the names
are ignored.

Keywords

Bindings:      var  const
Functions:     fn   return
Branching:     if   elif  else
Loops:         while  for  foreach  each
Switch:        switch  default  break  continue
Modules:       import
Literals:      true  false  nil
  • Bindings — var, const (covered in Variables above).
  • Functions — fn defines a function; return exits early.
  • Branching — if / elif / else (C-style else if is written
    elif).
  • Loops — while, C-style for, foreach over an iterable; each is
    the implicit loop variable inside foreach(iterable) { ... }.
  • Switch — switch / default multi-way dispatch, with break and
    continue to control flow.
  • Modules — import loads another script (see Builtins — import).
  • Literals — true, false, nil.

Everything else is an identifier. There is no do, goto, try, class,
local, or ref (local was removed in 0.5.0 — it was a synonym of var;
ref was removed in 0.6.0 — aliasing is spelled var x = &y, so ref is
now an ordinary identifier).

Next: Types for the full value model, or Builtins — import for multi-file programs.