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.
// C-style line comment
/* block
comment */
Variables and constants
Scrii has four declaration keywords that differ in mutability and scope/aliasing:
| Keyword | Mutability | Scope / behavior |
|---|---|---|
var | mutable | top level = global; inside a block/function = local to it |
const | immutable | same scoping as var, but cannot be reassigned or its members changed |
local | mutable | an explicit block-local that shadows an outer binding without touching it |
ref | n/a (alias) | shares storage with another slot — writes through the alias reach the original |
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 // 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: REASSIGN_RESTRICTED
std.file.read("a.txt") // reading module members is fine
std.file = 0 // error: REASSIGN_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 = [...] declares a variable, but items is already a builtin, so
using it afterwards fails. Rename the variable.
The host side is covered in Embedding — restricted and const values.
local — explicit block-local
local is var's intent-revealing sibling: it always declares a new local
binding that shadows an outer name and never modifies or reuses the outer
slot. Inside a block both behave the same; local is the clearest way to say
"isolate me from the outer scope":
var counter = 0
if (enabled) {
local counter = 5 // shadows; the outer counter is untouched
counter += 1 // 6 (this local)
}
print(counter) // 0 (outer unchanged)
Reach for local when you are shadowing a name deliberately — especially at
the top level where it keeps a local binding that won't clobber a global.
ref — shared storage (an alias)
ref does not copy the value — it points at the same storage as another
slot, so reads and writes through the alias reach the original:
var x = 42
ref alias = x // alias and x share the same storage
alias = 99 // writes through to x
print(x) // 99
var user = {count: 0}
ref u = user
u.count = 5 // member write reaches user
print(user.count) // 5
ref requires an lvalue on the right (ref a = value, not a literal).
Because function arguments are passed by value (a deep copy), ref is how
you share or mutate state that lives outside the current function.
ref and pipes
ref 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]
ref 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/local/ref) must precede the first assignment to a new name. Reassignment uses a barex = 100and the name must already exist. var/constat the top level are global; inside a function/block they are scoped to it.localat the top level keeps the binding as a top-level local instead of a global.- Variables declared inside a block (
if/while/for/elseor a standalone{}) are local to that block. refrequires an lvalue on the right, cannot destructure, and shares storage until therefslot is rebound.- Assignment copies values deeply; see Nested objects and arrays and Value semantics in Types.
Destructuring declarations unpack an array in one step:
var [a, b] = [1, 2] // a is 1, b is 2
var [head] = [9, 8] // extra elements are ignored
const [x, y] = [10, 20] // immutable bindings
local [u, v] = [3, 4] // block-local destructuring
// var [a, b] = 5 // error: value must be an array
// var [a, b] = [1] // error: not enough elements
Literals
// integers
42
-7
0
// prefixed integers — always produce a long
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)
3000000000L
2000000000l
// floats (decimal point or F/f suffix)
3.14
1.0f
3.14F
// doubles (D/d suffix)
3.14D
1.0d
// scientific notation (decimal only)
1.5e10
2.0E-3
5E+2
1e5D
// strings and chars
"hello world"
'c'
'"raw\\nstring"' // raw string literal: \n stays literal
// booleans and nil
true
false
nil
// arrays
[]
[1, 2, 3]
[1, "two", true]
// objects
{}
{name: "John", age: 30}
{"key with spaces": 1}
{nested: {inner: true}}
String and character escapes
Regular double-quoted strings ("...") decode escape sequences:
\n \t \r \\ \" \' \b \f \0 \a \v \/ \uXXXX
print("line1\nline2") // line1 <newline> line2
print("tab\there") // tab
print("quote: \" end") // an embedded double quote
print("\u0041") // "A"
\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 thatescape_stringemits re-parses to the same value.
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. If you want a
line break at that point, write the \n escape explicitly inside one of the
literals:
var two = "first line\n"
"second line"
print(two)
// first line
// second line
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'
See Descriptors for the -> describe operator.
Keywords
Bindings: var const local ref
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,local,ref(covered in Variables above). - Functions —
fndefines a function;returnexits early. - Branching —
if/elif/else(C-styleelse ifis writtenelif). - Loops —
while, C-stylefor,foreachover an iterable;eachis the implicit loop variable insideforeach(iterable) { ... }. - Switch —
switch/defaultmulti-way dispatch, withbreakandcontinueto control flow. - Modules —
importloads another script (see Builtins —import). - Literals —
true,false,nil.
Everything else is an identifier. There is no do, goto, try, or
class.
Hello world
// Run with: scrii_repl hello.scr
print("Hello, world!")
Next: Types for the full value model, or Builtins — import for multi-file programs.