Language Guide
A complete reference for the ZayneScript language: syntax, semantics, and examples derived from the test suite and interpreter source.
Overview
ZayneScript is a dynamically-typed, interpreted scripting language compiled to an internal bytecode format and evaluated by a C runtime. Its syntax is intentionally close to JavaScript / C with a few purposeful differences:
-
The
asynckeyword appears after the parameter list. -
Block-scoped variables use
localinstead oflet. -
for/whileloop initialisers use:=(short assign), not keyword declarations. -
Switch expressions use
=>and are written in value position.
Data Types
| Type | Examples | Notes |
|---|---|---|
null |
null |
Absence of a value |
bool |
true, false |
|
int |
0, -1, 42 |
Machine integer |
num |
3.14, 1e-9 |
64-bit double |
bigint |
100n, 2n**64n |
Arbitrary precision (libbf) |
str |
"hello", 'world' |
UTF-8 string |
array |
[1, 2, 3] |
Dynamic, heterogeneous |
object |
{ x: 1 } |
Key-value map |
function |
fn() {} |
First-class, closures |
class |
class Foo {} |
Class descriptor |
promise |
returned by async calls |
Async future value |
Variables
There are three declaration keywords with distinct scoping rules:
| Keyword | Scope | Mutable |
|---|---|---|
var |
Global (module level) | Yes |
const |
Global (module level) | No |
local |
Block / function | Yes |
Multiple declarations can be combined in one statement using commas:
var globalX = 10;
const PI = 3.14159;
const a = 1, b = 2, c = 3; // multiple consts
fn example() {
local x = 20;
local f = 6, g = 7; // multiple locals
if (true) {
local y = 30; // block-scoped; not visible outside this if
}
}
local variables declared inside
if, for, while, and other
blocks are restricted to that block's scope.
Operators
| Category | Operators |
|---|---|
| Arithmetic |
+ - * /
%
|
| Bitwise |
& | ^
<< >>
|
| Logical | && || ! |
| Comparison |
== != <
<= >
>=
|
| Augmented assignment |
+= -= *= /=
%= &= |=
^= <<=
>>=
|
| Increment / Decrement | ++ -- (prefix and postfix) |
| Spread | ... (in array and object literals) |
| Exponentiation (BigInt) | ** |
Ternary Expressions
Two syntactic forms are supported:
// Standard C-style ternary
local result = condition ? "yes" : "no";
// Postfix if/else form
local result = "yes" if (condition) else "no";
Spread Operator
Use ... to expand an array or object in place:
local a = [1, 2, 3];
local b = [...a, 4, 5]; // [1, 2, 3, 4, 5]
local obj1 = { x: 1 };
local obj2 = { ...obj1, y: 2 }; // { x: 1, y: 2 }
BigInt Literals
Append n to any numeric literal to create an
arbitrary-precision BigInt:
println(100n + 2n); // 102n
println(5n * 5n); // 25n
println(5n / 2n); // 2n (integer division)
println(1 << 2n); // 4
println(10.56n); // BigInt from float literal
println(2.2e2n); // BigInt from scientific notation
println(2n ** 64n); // 18446744073709551616n
Functions & Closures
Functions are declared with fn. They are first-class
values and can be assigned to variables, passed as arguments, and
returned from other functions.
fn greet(name) {
println("Hello", name);
}
// Anonymous function expression
const square = fn(x) { return x * x; };
// Higher-order function
fn apply(f, x) { return f(x); }
println(apply(square, 5)); // 25
Closures
Inner functions capture variables from their enclosing scope and can mutate them:
fn counter() {
local count = 0;
return fn() {
count += 1;
return count;
};
}
var c = counter();
println(c()); // 1
println(c()); // 2
println(c()); // 3
Multiple closures sharing state
fn makeCounters() {
local shared = 0;
local inc = fn() { shared += 1; };
local dec = fn() { shared -= 1; };
local get = fn() { return shared; };
return [inc, dec, get];
}
var [inc, dec, get] = makeCounters();
inc(); inc();
println(get()); // 2
dec();
println(get()); // 1
Async / Await
The async keyword is placed after the
parameter list. Calling an async function returns a
Promise immediately. Use await inside
another async function to suspend until the awaited promise resolves.
fn fetchData() async {
return "payload";
}
fn main() async {
const data = await fetchData();
println("Got:", data); // Got: payload
return 1;
}
println(main()); // <Promise> — call is non-blocking
Anonymous async functions
const task = fn() async {
return "done";
};
Await chains
fn topLevel() async { return "Hello"; }
fn callMe() async {
println(await topLevel()); // Hello
println(await topLevel()); // Hello
return 1;
}
println(callMe()); // <Promise>
Promises
Every async function returns a Promise. The
.then(callback) method chains a reaction to the resolved
value; its return value becomes the next promise in the chain.
.error(callback) handles rejections.
fn awaitable() async { return "Hola!"; }
const v = awaitable()
.then(fn(v) {
println("resolved with:", v); // resolved with: Hola!
return 42;
})
.then(fn(v) {
println("chained value:", v); // chained value: 42
return "done";
})
.then(println); // done
println(v); // <Promise>
Error handling with .error()
fn risky() async {
return someUndefinedOp();
}
risky()
.then(fn(v) { println("success:", v); })
.error(fn(e) { println("caught:", e); });
if / else
if (x > 0) {
println("positive");
} else {
println("non-positive");
}
Optional initialiser (:=)
The if condition supports an initialiser separated by
;. The declared variable is scoped to the if
block:
if (val := computeValue(); val > 0) {
println("got", val);
}
for Loop
The for initialiser uses :=; the loop
variable is scoped to the loop:
for (i := 0; i < 10; i++) {
println(i);
}
// Floating-point step
for (x := 0.0; x < 1.0; x += 0.1) {
println(x);
}
while Loop
// Standard while
while (condition) {
// ...
}
// With initialiser — variable scoped to loop
while (i := 0; i < 10) {
println(i);
i++;
}
// With initialiser + mutator (three-part form)
while (i := 0; i < 10; i++) {
println(i);
}
do-while Loop
var n = 0;
do {
println(n++);
} while (n < 5);
switch
Two forms are available: statement and expression.
Statement form
Uses case with : and block bodies.
Comma-separated values match multiple literals in one case:
switch (num) {
case 0, 1: {
println("matched 0 or 1");
}
case 2, 3: {
println("matched 2 or 3");
}
default: {
println("no match");
}
}
Expression form
Placed after the value being tested; uses => and
produces a value. No break needed:
const label = 100 switch {
case 10 => "Ten"
case 20 => "Twenty"
case 10, 30, 100 => "A Hundred"
default => "Unknown"
};
println(label); // A Hundred
try / catch
Both try and catch bodies must be block
statements:
try {
local x = riskyOperation();
println("success:", x);
} catch (e) {
println("Error:", e);
}
try/catch.
break & continue
break exits the nearest enclosing loop or switch.
continue skips the rest of the current loop iteration.
Both correctly unwind through nested try/catch blocks:
for (i := 0; i < 10; i++) {
if (i == 5) break;
if (i % 2 == 0) continue;
println(i); // 1 3
}
Classes
Classes support single inheritance. The superclass is listed in
parentheses after the class name. The constructor is named
init.
class Animal {
fn speak() {
println("...");
}
}
class Dog (Animal) {
fn init(name) {
this.name = name;
}
fn speak() {
println(this.name, "says Woof!");
}
}
const d = new Dog("Buddy");
d.speak(); // Buddy says Woof!
A subclass automatically inherits all methods from the parent.
Override a method by re-declaring it in the subclass. Access the
instance with this.
Static Members
Use static fn for static methods and
static name = value; for static properties:
class MathUtils {
static PI = 3.14159;
static fn square(x) {
return x * x;
}
}
println(MathUtils.PI); // 3.14159
println(MathUtils.square(4)); // 16
Arrays
Arrays are dynamic and heterogeneous. They use zero-based integer indexing:
local list = [1, 2, 3];
list.push(4); // append
list.pop(); // remove last → returns it
println(list.length()); // 3
println(list[0]); // 1
// Spread
local more = [...list, 10, 20];
Iteration with each
each(callback) calls
callback(index, element)
for every item and returns a new array of the return values:
const nums = [1, 2, 3, 4, 5];
const doubled = nums.each(fn(i, e) { return e * 2; });
println(doubled); // [2, 4, 6, 8, 10]
// println accepts varargs — index then value
nums.each(println);
// 0 1
// 1 2
// 2 3 ...
Filtering with keep
keep(callback) returns only the elements for which
callback returns truthy:
const odds = nums.keep(fn(i, e) { return e % 2 != 0; });
println(odds); // [1, 3, 5]
Objects
Objects are key-value maps. Access properties with dot notation or bracket notation:
local person = {
name: "Alice",
age: 30
};
println(person.name); // Alice
println(person["age"]); // 30
// Shorthand: variable name == key name
local x = 10;
local obj = { x }; // equivalent to { x: x }
// Spread
local extended = { ...person, city: "NYC" };
Imports
Three import forms are supported:
Named imports
import { println, scan, parseNum } from "core:io";
import { sqrt, pi, pow } from "core:math";
import { getUser, getCwd } from "core:os";
import { Array } from "core:Array";
import { Date } from "core:Date";
import { Promise } from "core:Promise";
Wildcard import (module bound to its name)
import "core:math"; // accessible as `math`
println(math.sqrt(16)); // 4
println(math.pi); // 3.141592...
Relative file imports
import "./utils"; // runs ./utils.zs
import { helper } from "./helpers/math"; // named import from file
.zs extension is appended
automatically. Each module is executed once and cached; circular
imports are detected and produce a runtime error.
User Libraries (lib:)
User-authored .zs files placed in the
lib/ directory are importable via the
lib: prefix. Subdirectories are supported using
/:
import { greet } from "lib:request"; // lib/request.zs
import "lib:nested/mod"; // lib/nested/mod.zs → bound as `mod`