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:
constis something you declare — your own value, locked by choice.restrictedis imposed on you — the engine’s builtins (len,keys,
items, …), thestdlibrary, and anything the host injects as
restricted. You getREASSIGN_RESTRICTED/MODIFY_RESTRICTEDif 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 barex = 100and the name
must already exist. (Alias targets work too:var o.k = &innerbinds the
nested slot.) var/constlive 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/elseor a standalone
{}) are local to that block — including a C-styleforheader: 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 —
fndefines a function;returnexits early. - Branching —
if/elif/else(C-styleelse ifis written
elif). - Loops —
while, C-stylefor,foreachover an iterable;eachis
the implicit loop variable insideforeach(iterable) { ... }. - Switch —
switch/defaultmulti-way dispatch, withbreakand
continueto control flow. - Modules —
importloads 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.