Standard Library

ZayneScript registers built-in modules under the core: import prefix (names are case-sensitive: core:date, core:array, core:regex, core:string, …). They load on demand — import only what you need. The repo also includes lib/http.zs, a thin wrapper re-exporting the HTTP client and common constants for use via lib:http.

core:array — Array class

Import with:

import { Array } from "core:array";

Array methods are available directly on every array literal via the prototype. You do not need to explicitly import core:array to use the built-in methods — they are always present. Import it only when you need the Array class itself (e.g. to add static helpers at runtime).


push

array.push(value)

Appends value to the end of the array. Returns null.

const arr = [1, 2];
arr.push(3);
println(arr); // [1, 2, 3]

pop

array.pop() → value

Removes and returns the last element. Throws an error if the array is empty.

const last = arr.pop();
println(last); // 3
println(arr);  // [1, 2]

length

array.length() → int

Returns the number of elements in the array.

println([10, 20, 30].length()); // 3

each

array.each(callback(value, index)) → array

Calls callback(value, index) for every element and returns a new array containing each return value. Analogous to Array.prototype.map in JavaScript.

const nums    = [1, 2, 3, 4, 5];
const doubled = nums.each(fn(v, i) { return v * 2; });
println(doubled); // [2, 4, 6, 8, 10]

// println prints each argument: element, then index
nums.each(println);
// 1 0
// 2 1
// 3 2 ...

keep

array.keep(callback(value, index)) → array

Returns a new array containing only the elements for which callback returns a truthy value. Analogous to Array.prototype.filter.

const evens = nums.keep(fn(v, i) { return v % 2 == 0; });
println(evens); // [2, 4]

Adding static helpers at runtime

Because Array is a first-class class value you can attach arbitrary static functions to it:

import { Array } from "core:array";

Array.sum = fn(arr) {
    local total = 0;
    arr.each(fn(i, e) { total += e; });
    return total;
};

println(Array.sum([1, 2, 3, 4, 5])); // 15

core:crypto — detailed API reference

Import with:

import { Crypto } from "core:crypto";

Native implementation lives in src/core/crypto.c. The module mirrors a CryptoJS-style aggregate API and returns WordArray objects for all binary outputs.

Overview

Accepted input types

Most crypto functions accept message/key inputs as: string, byte Array (0..255), WordArray, object with bytes field, or null (treated as empty input). Output is usually WordArray.

AES sample (real-world flow)

Typical secure-message flow: derive key/IV from passphrase + salt, encrypt with AES-CBC, transmit as Base64, then decode/decrypt.

import { println } from "core:io";
import { Crypto } from "core:crypto";

const passphrase = "correct horse battery staple";
const salt = "NaCl-1234"; // demo only; use per-message random salt in real apps
const msg = "confidential payload";

// 1) Derive 16-byte key + 16-byte IV
const kivHex = Crypto.EvpKDF(passphrase, salt, 1000, 16, 16).toString(null);
const key = Crypto.enc.Hex.parse(kivHex.substring(0, 32));
const iv  = Crypto.enc.Hex.parse(kivHex.substring(32, 64));

// 2) Encrypt
const ciphertextWa = Crypto.AesCbcEncrypt(key, iv, msg);

// 3) Transport-friendly encoding
const transport = ciphertextWa.toString(Crypto.enc.Base64);
println(transport);

// 4) Decode + decrypt (receiver side)
const decodedWa = Crypto.enc.Base64.parse(transport);
const plaintext = Crypto.AesCbcDecrypt(key, iv, decodedWa).toString(Crypto.enc.Utf8);
println(plaintext); // confidential payload

Cipher helpers

Crypto.AesEcbEncrypt(key, plaintext) / Crypto.AesEcbDecrypt(key, ciphertext)

AES-128 ECB with PKCS#7 padding. Key must be 16 bytes.

Crypto.AesCbcEncrypt(key, iv, plaintext) / Crypto.AesCbcDecrypt(key, iv, ciphertext)

AES-128 CBC with PKCS#7 padding. Key and IV must both be 16 bytes.

Crypto.RC4(key, data, dropN) → WordArray

RC4 stream cipher. Use same key and dropN to decrypt.

const key = Crypto.enc.Utf8.parse("0123456789abcdef");
const iv = Crypto.enc.Hex.parse("000102030405060708090a0b0c0d0e0f");
const pt = "hello-aes-cbc";

const ct = Crypto.AesCbcEncrypt(key, iv, pt);
const dec = Crypto.AesCbcDecrypt(key, iv, ct).toString(Crypto.enc.Utf8);
println(dec); // hello-aes-cbc

const rc4Ct = Crypto.RC4("secret-key", "hello", 768);
const rc4Pt = Crypto.RC4("secret-key", rc4Ct, 768).toString(Crypto.enc.Utf8);
println(rc4Pt); // hello

Common errors and constraints

Complete example

import { println } from "core:io";
import { Crypto } from "core:crypto";

const msg = "payload";

// digest + hmac
println(Crypto.SHA256(msg).toString(null));
println(Crypto.HmacSHA256(msg, "k").toString(null));

// derive key+iv then encrypt
const kiv = Crypto.EvpKDF("password", "12345678", 1000, 16, 16).toString(null);
const key = Crypto.enc.Hex.parse(kiv.substring(0, 32));
const iv  = Crypto.enc.Hex.parse(kiv.substring(32, 64));
const c = Crypto.AesCbcEncrypt(key, iv, msg);
println(Crypto.AesCbcDecrypt(key, iv, c).toString(Crypto.enc.Utf8));

Encoders

Crypto.enc.Hex.stringify(wa) / Crypto.enc.Hex.parse(hex)

Hex encode/decode. Invalid hex parse returns error.

Crypto.enc.Utf8.stringify(wa) / Crypto.enc.Utf8.parse(str)

UTF-8 conversion. Invalid UTF-8 decode returns error.

Crypto.enc.Base64.stringify(wa) / Crypto.enc.Base64.parse(str)

RFC 4648 Base64 with padding.

Crypto.enc.Base64Url.stringify(wa) / Crypto.enc.Base64Url.parse(str)

RFC 4648 base64url (URL-safe alphabet).

const hello = Crypto.enc.Utf8.parse("Hello");
println(Crypto.enc.Hex.stringify(hello));        // 48656c6c6f
println(Crypto.enc.Base64.stringify(hello));     // SGVsbG8=
println(Crypto.enc.Base64Url.stringify(hello));  // SGVsbG8
println(Crypto.enc.Utf8.stringify(hello));       // Hello

Hash functions

All hash functions take one argument and return WordArray.

Crypto.MD5(msg), Crypto.SHA1(msg), Crypto.SHA224(msg), Crypto.SHA256(msg), Crypto.SHA384(msg), Crypto.SHA512(msg), Crypto.SHA3_224(msg), Crypto.SHA3_256(msg), Crypto.SHA3_384(msg), Crypto.SHA3_512(msg), Crypto.RIPEMD160(msg)
println(Crypto.SHA512("abc").toString(null));
println(Crypto.SHA3_256("").toString(null));
println(Crypto.RIPEMD160("").toString(null));

HMAC functions

All HMAC functions take (message, key) and return WordArray.

Crypto.HmacMD5(msg, key), Crypto.HmacSHA1(msg, key), Crypto.HmacSHA256(msg, key), Crypto.HmacSHA384(msg, key), Crypto.HmacSHA512(msg, key), Crypto.HmacRIPEMD160(msg, key)
const mac = Crypto.HmacSHA256(
  "The quick brown fox jumps over the lazy dog",
  "key"
);
println(mac.toString(null));

Key derivation functions

Crypto.PBKDF2(password, salt, iterations, dkLen, prf) → WordArray

iterations must be positive. dkLen range is 1..65536. prf values: 0 = HMAC-SHA256, 1 = HMAC-SHA1, 2 = HMAC-MD5.

Crypto.EvpKDF(password, salt, iterations, keyLen, ivLen) → WordArray

OpenSSL-style EVP_BytesToKey MD5 chain. Returns one WordArray containing key || iv. keyLen/ivLen must be within implementation limits.

// PBKDF2 example
const dk = Crypto.PBKDF2("password", "salt", 10000, 32, 0);
println(dk.toString(null));

// EvpKDF example: first 16 bytes key, next 16 bytes iv
const kiv = Crypto.EvpKDF("password", "12345678", 1000, 16, 16);
const key = Crypto.enc.Hex.parse(kiv.toString(null).substring(0, 32));
const iv  = Crypto.enc.Hex.parse(kiv.toString(null).substring(32, 64));

Use-case scenarios

Validation

./dist/zscript.exe --run tests/test_crypto.zs

WordArray and conversion

wordArray.toString(encoder?) → string

Without encoder (or with null) returns lowercase hex. Encoder options are Crypto.enc.Hex, Crypto.enc.Utf8, Crypto.enc.Base64, and Crypto.enc.Base64Url.

const wa = Crypto.SHA256("abc");
println(wa.toString(null));                  // hex
println(wa.toString(Crypto.enc.Base64));     // base64

core:date — Date and Time

Import with:

import { Date } from "core:date";
// Or wildcard:
import "core:date";

Constructor

new Date()
new Date(timestamp: number)
new Date(dateString: string)
new Date(year, month, [day, hours, minutes, seconds, ms])

Creates a new Date instance.
No argument — current date and time.
Number — Unix timestamp in milliseconds.
String — parsed date. Supported formats: YYYY-MM-DD, YYYY-MM-DD HH:mm:ss, YYYY-MM-DDTHH:mm:ss, YYYY/MM/DD HH:mm:ss.
Components — year (full), month (0-based Jan=0), day (default 1), hours, minutes, seconds, milliseconds.

const now = new Date();
println(now.toString()); // e.g. Fri Apr 04 2026 14:30:00

const d1 = new Date("2023-12-25");
println(d1.getFullYear()); // 2023
println(d1.getMonth());    // 11  (0-based)
println(d1.getDate());     // 25

const d2 = new Date(2024, 0, 1, 12, 30, 45);
// year=2024, month=January(0), day=1, 12:30:45

const d3 = new Date(1705449600000); // from timestamp ms

Instance Getters

Method Returns Notes
getFullYear() int Full 4-digit year (e.g. 2026)
getMonth() int Month 0–11 (0 = January)
getDate() int Day of month 1–31
getDay() int Day of week 0–6 (0 = Sunday)
getHours() int Hours 0–23
getMinutes() int Minutes 0–59
getSeconds() int Seconds 0–59
const d = new Date("2026-04-04T14:30:00");
println(d.getFullYear()); // 2026
println(d.getMonth());    // 3   (April = index 3)
println(d.getDate());     // 4
println(d.getDay());      // 6   (Saturday)
println(d.getHours());    // 14
println(d.getMinutes());  // 30
println(d.getSeconds());  // 0

getTime

date.getTime() → num

Returns the internal Unix timestamp in milliseconds.

println(new Date().getTime()); // e.g. 1743778200000

toString

date.toString() → string

Returns a human-readable string representation of the date, e.g. "Sat Apr 04 2026 14:30:00".

println(new Date().toString());

core:io — Input / Output

Import with:

import { print, println, scan, parseNum, format,
         clearScreen, setColor, decompile, File } from "core:io";

print

print(...args)

Writes each argument (space-separated) to stdout without a trailing newline. Accepts any number of arguments of any type.

print("x =", 42); // x = 42  (no newline)

println

println(...args)

Like print but appends a newline after all arguments. Calling with no arguments prints a blank line.

println("Hello", "World"); // Hello World
println();                 // (blank line)

scan

scan(prompt?: string) → string

Reads one line from stdin and returns it as a string (newline stripped). If prompt is provided it is printed first without a trailing newline. Returns null on EOF.

const name = scan("Enter your name: ");
println("Hello,", name);

parseNum

parseNum(str: string) → int | num

Parses str into a number. Returns an integer when the parsed value has no fractional part, otherwise a float.

const n = parseNum(scan("Enter a number: "));
println(n * 2);

format

format(template: string, ...args) → string

Returns a new string by replacing each exact {} token in template with the next argument in order. Only empty-brace placeholders are recognized.

const msg = format("Hello, {}! You are {} years old.", "Alice", 30);
println(msg); // Hello, Alice! You are 30 years old.
// Fewer args than placeholders: remaining {} are kept
println(format("{} + {} = {}", 1, 2)); // 1 + 2 = {}

// Extra args are ignored
println(format("x={}", 42, 99)); // x=42

// Specifiers like {0}, %s, etc. are not supported
println(format("name={0}, value=%s, raw={}", "Alice")); // name={0}, value=%s, raw=Alice
Note: format only replaces literal {} pairs. It does not implement positional or typed format specifiers.

clearScreen

clearScreen()

Clears the terminal screen using ANSI escape codes.

clearScreen();

setColor

setColor(fg?: int, bg?: int)

Sets the terminal foreground (and optionally background) colour using ANSI colour codes. Calling with no arguments resets to default colours. Typical foreground codes: 31 red, 32 green, 33 yellow, 34 blue, 0 reset.

setColor(32);          // green text
println("Success!");
setColor();            // reset

decompile

decompile(fn: function) → string

Decompiles a compiled user function back into a human-readable source representation. Useful for debugging or inspecting generated bytecode.

fn add(a, b) { return a + b; }
println(decompile(add));

File

Text-oriented file I/O similar to Python. Construct with new File(path, mode?) (the class constructor is init). path and mode are strings; mode defaults to "r" and follows the same conventions as C fopen (for example "w", "a", "r+", "rb").

import { File } from "core:io";

const out = new File("notes.txt", "w");
out.writelines(["first line\n", "second\n"]);
out.close();

const inp = new File("notes.txt", "r");
println(inp.read()); // full remainder of file
inp.close();

Reading text

read() with no argument (or a negative size) reads from the current position until EOF. read(0) returns an empty string without advancing. read(n) with n > 0 reads at most that many bytes and returns a string (still text mode).

import { File } from "core:io";

const f = new File("data.txt", "r");
const whole = f.read();   // string: rest of file
f.close();

const g = new File("data.txt", "r");
const head = g.read(4);   // first four bytes as text
const line = g.readline(); // next line, including "\n" if present
g.close();

const h = new File("data.txt", "r");
const lines = h.readlines(); // Array of strings, one per line
h.close();

Writing text

In text mode, write accepts any value: non-strings are turned into UTF-8 text before writing. writelines takes an array and writes each element in order with no extra separators between elements.

import { File } from "core:io";

const w = new File("out.txt", "w");
w.write("Hello\n");
w.writelines(["line2\n", "line3\n"]);
w.flush();
w.close();

Binary mode and Blob

Open with a fopen-style mode that includes "b" (for example "rb", "wb", "ab"). Then read() / read(n) return a Blob instead of a string. readline and readlines are not available on binary files. Use import { Blob } from "core:blob" to build byte payloads.

import { File } from "core:io";
import { Blob } from "core:blob";

const bin = new File("app.bin", "wb");
bin.write(new Blob([0x48, 0x69])); // raw bytes, not "Hi" as text
bin.close();

const r = new File("app.bin", "rb");
const b = r.read(); // Blob
r.close();

// Optional: reopen as text to decode the same bytes as UTF-8
const t = new File("app.bin", "r");
println(t.read()); // "Hi"
t.close();

In binary mode, write and writelines only accept string or Blob (each array element must be one of those in writelines).

import { File } from "core:io";
import { Blob } from "core:blob";

const w = new File("parts.bin", "wb");
w.writelines([new Blob([0x01, 0x02]), new Blob([0x03])]);
w.close();

Position: tell and seek

Use a read/write mode such as "r+" when you need both read and write on the same handle. seek(offset, whence) uses whence 0 = start of file, 1 = current position, 2 = end; omit whence to seek from the start.

import { File } from "core:io";

const f = new File("notes.txt", "r+");
println(f.tell());   // 0
f.seek(6, 0);        // six bytes from start
println(f.read(1));  // one character at that offset
f.seek(0, 2);        // end of file
println(f.tell());   // file length in bytes
f.close();

Mode helpers

After new File(...), readable() and writable() reflect whether the mode allows reading or writing (for example "w" is writable but not readable; "r+" is both). isClosed() becomes true after close(). If fopen fails (missing path, permission denied, and so on), the constructor raises an error.

import { File } from "core:io";

const w = new File("tmp.txt", "w");
println(w.writable(), w.readable()); // true, false
w.close();
println(w.isClosed()); // true
Constructor / methods Description
Constructor: init(path, mode?) (invoked by new File(path, mode?)) Opens the file. On failure raises an error (for example fopen: … from the host C library).
read(size?) Without size, or with a negative size, reads until EOF. With size === 0, returns an empty string. With size > 0, reads up to that many bytes. In text mode returns a string; in binary mode ("b" in mode) returns a Blob.
readline() Next line including the newline when present; text modes only (not "rb").
readlines() Returns an Array of lines; text modes only.
write(value) A Blob is always written as its raw byte payload (fwrite). Any other value is converted to a UTF-8 string first. In binary mode ("b" in mode), only string or Blob may be passed. Requires a writable mode.
writelines(lines) Writes each array element in order with no extra separators (Python-style). A Blob element writes its raw payload; other values use string conversion. In binary mode, every element must be a string or Blob. Requires a writable mode.
flush() / close() fflush / fclose. After close(), other operations report a closed file.
tell() Current byte offset as a number.
seek(offset, whence?) whence: 0 = start of file, 1 = current position, 2 = end (same as C SEEK_SET / SEEK_CUR / SEEK_END). Defaults to 0.
readable() / writable() / isClosed() Boolean helpers derived from the open mode and handle state.

Regression coverage lives in tests/test_fileio.zs.

core:math — Mathematics

Import with:

// Named imports
import { sqrt, pow, sin, cos, pi, e } from "core:math";

// Or wildcard — module bound as `math`
import "core:math";
println(math.sqrt(16)); // 4

Single-argument functions

Each accepts one numeric argument and returns a num:

Function Description
abs(x) Absolute value
acos(x) Arc cosine (radians)
asin(x) Arc sine (radians)
atan(x) Arc tangent (radians)
ceil(x) Round up to nearest integer
cos(x) Cosine (radians)
cosh(x) Hyperbolic cosine
exp(x) ex
floor(x) Round down to nearest integer
log(x) Natural logarithm
log10(x) Base-10 logarithm
round(x) Round to nearest integer
sin(x) Sine (radians)
sinh(x) Hyperbolic sine
sqrt(x) Square root
tan(x) Tangent (radians)
tanh(x) Hyperbolic tangent

Two-argument functions

Function Description
atan2(y, x) Arc tangent of y/x (full quadrant)
hypot(x, y) √(x² + y²)
max(a, b) Larger of two values
min(a, b) Smaller of two values
pow(base, exp) baseexp

Constants

Name Value
pi 3.14159265358979323846
e 2.71828182845904523536
import { sin, cos, sqrt, pow, pi, e } from "core:math";

println(sqrt(2));       // 1.4142135623730951
println(pow(2, 10));    // 1024
println(sin(pi / 2));   // 1
println(e);             // 2.718281828459045

core:mongoose — HTTP Server

Import with:

import { Server, request } from "core:mongoose";

core:mongoose wraps the Mongoose embedded networking library to give ZayneScript an Express.js-style HTTP server and a small async HTTP client. Create a Server, register routes, and call listen() — the event loop runs until close() is called. Outbound requests use request() and return a Promise.


Overview

Import symbols for both the server and client:

import { Server, request } from "core:mongoose";

request — HTTP client

request(url: string [, options: object]) → Promise

Performs a non-blocking HTTP request using the interpreter's shared Mongoose manager. Resolves with an object { status, statusText, headers, body } — on failure the promise is rejected with an error string.

Optional options fields:

If the response has Content-Type: application/json, body is parsed into objects/arrays/values; otherwise it remains a string.

import { println } from "core:io";
import { request } from "core:mongoose";

request("http://127.0.0.1:8080/api/ping", {
    method: "GET"
}).then(fn(res) {
    println(res.status, res.body);
}).error(fn(e) {
    println("failed:", e);
});

For named constants and a re-export of request, see lib:http below. For parsing args() into flags and positionals, see lib:argparser.

Server class

constructor

new Server()

Creates a new HTTP server instance. No arguments required.

import { Server } from "core:mongoose";
const app = new Server();

Routing

Route handlers receive (req, res) and are matched in registration order. The path is matched with Mongoose's glob patterns — use * as a wildcard segment.

app.get(path, handler)
app.post(path, handler)
app.put(path, handler)
app.delete(path, handler)
app.patch(path, handler)
app.all(path, handler)

Matches any HTTP method.

app.get("/hello", fn(req, res) {
    res.send("Hello World!");
});

app.post("/users", fn(req, res) {
    res.status(201).json({ created: true });
});

Wildcard captures

Wildcards in the path are captured and exposed on req.params as numeric keys ("0", "1", …).

app.get("/users/*", fn(req, res) {
    const id = req.params["0"];  // e.g. "42" for /users/42
    res.json({ id: id });
});

Multiple wildcards

If your route contains more than one wildcard, each is captured in order:

app.get("/users/*/posts/*", fn(req, res) {
      const userId = req.params["0"];  // e.g. "42" for /users/42/posts/99
      const postId = req.params["1"];  // e.g. "99" for /users/42/posts/99
      res.json({ user: userId, post: postId });
  });

Query parameters

The req.query property contains the raw query string (e.g. "name=Alice&age=30" for /users?name=Alice&age=30). To access query parameters as an object, you can parse it manually:

// Example: GET /search?term=cat&page=2
  app.get("/search", fn(req, res) {
      // Parse query string into an object
      const params = {};
      req.query.split("&").each(fn(pair) {
          const kv = pair.split("=");
          if (kv.length == 2) {
              params[kv[0]] = kv[1];
          }
      });
      res.json(params); // { term: "cat", page: "2" }
  });
  // For more robust parsing (decoding, missing values), see core:string helpers.
          
Note: req.params contains wildcard route captures (from * in the path), while req.query is the raw query string after ? in the URL. They are separate.

Request object

Available inside every route handler as the first argument.

Property Type Description
req.method string HTTP verb in upper-case — "GET", "POST", etc.
req.path string Request URI path (without query string).
req.url string Same as req.path (full URI).
req.query string Raw query string, e.g. "name=Alice&age=30".
req.body string Raw request body as a string.
req.headers object All request headers as a plain object with lower-cased keys.
req.params object Wildcard captures from the route path, keyed "0""3".

Response object

Available inside every route handler as the second argument.

res.send(body)

res.send(body: string)

Sends a plain-text response with the current status code (default 200) and any headers set via res.setHeader().

res.json(value)

res.json(value)

Serialises value to a string and sends it with Content-Type: application/json.

res.status(code)

res.status(code: int) → res

Sets the response status code. Returns res for chaining: res.status(404).send("Not found").

res.redirect(url)

res.redirect(url: string)

Sends a 302 Found redirect to url.

res.setHeader(name, value)

res.setHeader(name: string, value: string) → res

Appends a response header. Can be chained before send() or json().

Middleware

app.use(fn(req, res))

Registers a function that runs before every route handler. Useful for logging, auth checks, or attaching properties to req. Up to 32 middleware functions are supported.

import { println } from "core:io";

app.use(fn(req, res) {
    println(req.method, req.path);
});

Common Patterns

Simple REST-like API

import { Server } from "core:mongoose";
import { println } from "core:io";

const app = new Server();

app.get("/", fn(req, res) {
    res.send("Welcome!");
});

app.get("/greet/*", fn(req, res) {
    const name = req.params["0"];
    res.json({ message: "Hello, " + name + "!" });
});

app.post("/echo", fn(req, res) {
    res.setHeader("X-Echo", "true").send(req.body);
});

app.listen(8080, fn(msg) { println(msg); });

Request logging middleware

import { Server } from "core:mongoose";
import { println } from "core:io";

const app = new Server();

app.use(fn(req, res) {
    println("[" + req.method + "]", req.path);
});

app.get("/ping", fn(req, res) {
    res.send("pong");
});

app.listen(3000, fn(msg) { println(msg); });

JSON API with status codes

import { Server }  from "core:mongoose";
import { Database } from "core:sqlite";

const app = new Server();
const db  = new Database("app.sqlite");
db.exec("CREATE TABLE IF NOT EXISTS notes (id INTEGER PRIMARY KEY, text TEXT)");

app.get("/notes", fn(req, res) {
    const rows = db.prepare("SELECT * FROM notes").all();
    res.json(rows);
});

app.post("/notes", fn(req, res) {
    const info = db.prepare("INSERT INTO notes (text) VALUES (?)").run(req.body);
    res.status(201).json({ id: info.lastInsertRowid });
});

app.listen(4000, fn(msg) { println(msg); });

core:mysql — MySQL / MariaDB Database

Import with:

import { Database } from "core:mysql";

Overview

core:mysql provides a lightweight synchronous SQL API for MySQL-compatible servers (MySQL and MariaDB). It dynamically loads libmariadb / libmysqlclient at runtime and exposes one class: Database.

Database

Constructor

new Database(options?) or new Database(host, user, password, database, port?)

Opens a new connection. Preferred usage is an options object. Default port is 3306.

const db = new Database({
    host: "127.0.0.1",
    user: "root",
    password: "secret",
    database: "zscript_test",
    port: 3306
});

// Positional form is also supported:
const db2 = new Database("127.0.0.1", "root", "secret", "zscript_test", 3306);
Note: if the client library cannot be loaded or authentication fails, the constructor raises a runtime error.

query

db.query(sql: string) → array<object>

Executes a query and returns rows. Each row is currently keyed by column index strings ("0", "1", ...), matching the current runtime implementation.

const rows = db.query("SELECT id, name FROM users ORDER BY id LIMIT 3");
rows.each(fn(row, i) {
    println("id=", row["0"], "name=", row["1"]);
});

exec

db.exec(sql: string) → object

Executes statements such as INSERT, UPDATE, DELETE, CREATE, and DROP. Returns an object with: affectedRows and insertId.

db.exec("CREATE TABLE IF NOT EXISTS users (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(64))");

const ins = db.exec("INSERT INTO users (name) VALUES ('alice')");
println("affected:", ins.affectedRows, "insertId:", ins.insertId);

const upd = db.exec("UPDATE users SET name = 'alice_updated' WHERE id = " + ins.insertId);
println("updated:", upd.affectedRows);

const del = db.exec("DELETE FROM users WHERE id = " + ins.insertId);
println("deleted:", del.affectedRows);

close

db.close()

Closes the active database handle. It is safe to call during cleanup before process exit.

db.close();

Common Patterns

CLI-driven connection options

import { Database } from "core:mysql";
import { args } from "core:os";

const argv = args();
if (argv.length() < 4) {
    raise "Usage:     [port]";
}

const db = new Database({
    host: argv[0],
    user: argv[1],
    password: argv[2],
    database: argv[3],
    port: argv.length() > 4 ? argv[4] : 3306
});

Ephemeral test table lifecycle (CRUD)

import { Database } from "core:mysql";
import { getPid } from "core:os";
import { format } from "core:io";

const db = new Database({ host: "127.0.0.1", user: "root", password: "secret", database: "zscript_test" });
const suffix = format("{}", getPid());
const table = "zscript_mysql_crud_" + suffix;

db.exec("CREATE TABLE IF NOT EXISTS " + table + " (id INT AUTO_INCREMENT PRIMARY KEY, name VARCHAR(128) NOT NULL)");
const ins = db.exec("INSERT INTO " + table + " (name) VALUES ('alice_" + suffix + "')");

const rows = db.query("SELECT id, name FROM " + table + " WHERE id = " + ins.insertId);
assert rows.length() == 1;
assert rows[0]["1"] != null;

db.exec("DROP TABLE IF EXISTS " + table);
db.close();

Safe cleanup with try/catch

import { Database } from "core:mysql";
import { println } from "core:io";

const db = new Database({
    host: "127.0.0.1",
    user: "root",
    password: "secret",
    database: "zscript_test"
});

try {
    db.exec("CREATE TABLE IF NOT EXISTS logs (id INT AUTO_INCREMENT PRIMARY KEY, msg TEXT)");
    db.exec("INSERT INTO logs (msg) VALUES ('hello')");
    const rows = db.query("SELECT id, msg FROM logs ORDER BY id DESC LIMIT 1");
    println(rows[0]["0"], rows[0]["1"]);
} catch (e) {
    println("MySQL error:", e);
}

db.close();

core:object — Object utilities

Import with:

import { Object } from "core:object";

Exposes the Object class used for plain maps. Static helpers mirror common JavaScript-style utilities. The constructor new Object() produces a new empty object instance.


Object.keys

Object.keys(obj: object) → array

Returns an array of the object's enumerable string keys.

const o = { a: 1, b: 2 };
println(Object.keys(o)); // [a, b]

Object.values

Object.values(obj: object) → array

Returns an array of the object's values in key iteration order.

Object.freeze

Object.freeze(obj: object) → object

Returns a new object with a shallow copy of obj's map, marked frozen in the runtime (mutations on the copy are rejected). Use for immutable snapshots or constant lookup tables (see lib/http.zs for StatusCode / Method objects).

core:os — Operating System Utilities

Import with:

import { getCwd, getPid, getUser, getType, system, args } from "core:os";

getCwd

getCwd() → string

Returns the current working directory as a string.

println(getCwd()); // /home/alice/projects

getPid

getPid() → int

Returns the process ID of the running interpreter.

println(getPid()); // 12345

getUser

getUser() → string

Returns the current OS username. Falls back to the USER environment variable on headless Linux environments.

println(getUser()); // alice

getType

getType() → string

Returns a string identifying the host platform: "win32", "mac", "linux", or "unknown".

if (getType() == "linux") {
    println("Running on Linux");
}

system

system(cmd: string) → int

Executes cmd in the system shell (equivalent to C system()). Returns the exit status code.

Warning: Never pass untrusted user input directly to system(). Validate and sanitise all inputs first to avoid command injection.
const code = system("ls -la");
println("exit:", code);

args

args() → array of string

When the interpreter is started with zscript --run path/to/script.zs …, returns the tokens passed after the script path as an array of strings. The host joins argv[3…] with single spaces and the runtime splits on spaces, so each entry is one shell word after the script name (no embedded spaces inside a token unless the shell already split differently).

Use these tokens as a conventional argv for flags and positionals. For structured parsing (long options, short clusters, --, defaults), import lib:argparser.

import { args } from "core:os";
import { println } from "core:io";

// zscript --run ./app.zs input.txt --verbose --out=out.txt
println(args()); // e.g. [input.txt, --verbose, --out=out.txt]

core:path — File paths

Import with:

import {
    normalize, basename, dirname,
    absolutePath, absolutePathFromBase, join,
    exists, isDirectory, isFile, isAbsolute, separator
} from "core:path";

Overview

core:path helps you work with file and directory paths in a way that is consistent on both POSIX and Windows: both / and \ are accepted as separators in input, and results use the host's native separator. Functions that return a boolean (exists, isDirectory, isFile, isAbsolute) use the interpreter's canonical true and false values, not new boolean allocations.

normalize

normalize(p: string) → string

Returns a normalized path: redundant separators are removed, . and .. components are resolved, and separators are converted to the native form.

println(normalize("foo//bar/../baz")); // foo/baz (POSIX) or foo\baz (Windows)

basename

basename(p: string) → string

Returns the last path segment (file name). If the name contains a . and the dot is not the first character, the suffix after the last dot is stripped (extension removed).

println(basename("/home/user/readme.txt")); // readme

dirname

dirname(p: string) → string

Returns the directory portion of p, or "." when there is no directory separator.

println(dirname("/tmp/data.json")); // /tmp

absolutePath

absolutePath(p: string) → string

If p is already absolute, returns a normalized absolute path. Otherwise resolves it against the current working directory. Returns an error if the path cannot be resolved (for example when the working directory is unavailable).

import { getCwd } from "core:os";
import { absolutePath, join } from "core:path";

println(absolutePath(join(getCwd(), "README.md")));

absolutePathFromBase

absolutePathFromBase(base: string, p: string) → string

If p is absolute, returns it normalized. Otherwise combines p with base (intended to be an absolute directory) and normalizes. Returns an error if resolution fails.

join

join(a: string, b: string) → string

Joins two segments with the native separator and normalizes the result. Treats empty segments like "." after normalization.

println(join("src", "main.zs")); // src/main.zs or src\main.zs

exists

exists(p: string) → bool

true if p refers to an existing file or directory.

isDirectory

isDirectory(p: string) → bool

true if p exists and is a directory.

isFile

isFile(p: string) → bool

true if p exists and is a regular file (not a directory or special node).

isAbsolute

isAbsolute(p: string) → bool

true if p is absolute for the host OS (POSIX root path, or Windows drive letter / UNC).

separator

separator() → string

Returns a one-character string: backslash on Windows and slash on POSIX and macOS (for example / on Linux).

println(separator()); // "/" on Linux and macOS

core:promise — Promises

Import with:

import { Promise } from "core:promise";

Promises are created implicitly by calling any async function. You rarely construct them manually. The two instance methods below form the chaining API.


then

promise.then(callback(value)) → Promise

Registers a callback to run when the promise resolves. The callback receives the resolved value as its sole argument. The return value of the callback becomes the resolved value of the new promise returned by then, enabling chaining.

fn getData() async { return 42; }

getData()
    .then(fn(v) {
        println("value:", v); // value: 42
        return v * 2;
    })
    .then(fn(v) {
        println("doubled:", v); // doubled: 84
    });
Note: The then callback must accept exactly 1 argument (or use a vararg signature). A callback with the wrong arity will produce a runtime error.

error

promise.error(callback(err)) → Promise

Registers a callback to run if the promise (or any preceding promise in the chain) is rejected. The callback receives the error value. Returns a new promise for further chaining.

fn mayFail() async {
    return somethingRisky();
}

mayFail()
    .then(fn(v) { println("Got:", v); })
    .error(fn(e) { println("Error:", e); });

Full async / Promise example

import { println } from "core:io";

fn step1() async { return "hello"; }
fn step2(s) async { return s + " world"; }

fn pipeline() async {
    const s = await step1();
    return await step2(s);
}

pipeline()
    .then(fn(result) {
        println(result); // hello world
    })
    .error(fn(e) {
        println("Failed:", e);
    });

core:regex — Python-style Regular Expressions

Import with:

import {
    compile, search, match, fullmatch, findall,
    flags, I, M, S, U
} from "core:regex";

core:regex is powered by the embedded libregexp engine and exposes a Python-like surface: module-level helpers plus compiled pattern objects.

Match-returning calls use this shape: null when no match, otherwise an array where index 0 is the full match and subsequent indexes are capture groups.


Module API

FunctionDescription
compile(pattern, flags = 0) Compiles and returns a regex object (Pattern / RegExp).
search(patternOrCompiled, text, flags = 0) Finds first match anywhere in text.
match(patternOrCompiled, text, flags = 0) Requires match at start of text.
fullmatch(patternOrCompiled, text, flags = 0) Requires the whole string to match.
findall(patternOrCompiled, text, flags = 0) Returns all matches. Shape mirrors Python: full-match strings (no groups), one string per match (one group), or arrays of groups (multiple groups).
import { search, match, fullmatch, findall } from "core:regex";

const s = search("cat", "xxcatyy");
println(s[0]); // cat

println(match("cat", "xxcatyy"));  // null
println(match("cat", "catyy")[0]); // cat

println(fullmatch("cat", "cat")[0]); // cat
println(fullmatch("cat", "cat!"));   // null

println(findall("a.", "abac")); // [ab, ac]

Compiled Pattern API

pattern.search(text) → array | null
pattern.match(text) → array | null
pattern.fullmatch(text) → array | null
pattern.findall(text) → array

Compiled objects also keep compatibility aliases: exec(text) (same as search) and test(text) (boolean).

import { compile, I } from "core:regex";

const rx = compile("(ab)+", I);
println(rx.search("..ABab..")[0]); // ABab
println(rx.match("zzAB"));         // null
println(rx.fullmatch("ABab")[0]);  // ABab

Flags

Use numeric bitmasks from flags or short aliases: I, M, S, U.

NameMeaning
flags.IGNORECASE / ICase-insensitive matching
flags.MULTILINE / MMultiline anchors
flags.DOTALL / S. matches newlines
flags.UNICODE / UUnicode behavior
import { compile, I } from "core:regex";
const rx = compile("hello", I);
println(rx.search("HeLLo")); // [HeLLo]

core:sqlite — SQLite3 Database

Import with:

import { Database } from "core:sqlite";
// Or import both exported names:
import { Database, Statement } from "core:sqlite";

Overview

core:sqlite wraps the bundled SQLite3 amalgamation and exposes two classes:

Every new Database(...) call opens an independent connection. Multiple databases can be open simultaneously.

Note: Always call db.close() when you are finished. The garbage collector does not automatically close SQLite handles — that is intentional so you have full control over transaction lifetime.

Database

Constructor

new Database(path?: string)

Opens (or creates) an SQLite database at path.
Pass ":memory:" or omit the argument for a private in-memory database that is discarded when closed.

const db  = new Database("app.sqlite");   // file-based
const mem = new Database(":memory:");     // in-memory
const def = new Database();               // also in-memory

exec

db.exec(sql: string)

Executes one or more semicolon-separated SQL statements with no parameter binding. Ideal for DDL (CREATE, DROP, ALTER), PRAGMAs, and multi-statement migration scripts. Returns null on success; an error on failure.

db.exec(`
    CREATE TABLE IF NOT EXISTS users (
        id   INTEGER PRIMARY KEY AUTOINCREMENT,
        name TEXT    NOT NULL,
        age  INTEGER
    );
`);

// Transactions
db.exec("BEGIN");
// ... statements ...
db.exec("COMMIT");

prepare

db.prepare(sql: string) → Statement

Compiles sql into a reusable Statement object. All queries and DML should go through prepared statements: they are safer (parameter binding prevents SQL injection), faster when reused, and ergonomic with the run / get / all methods. Call stmt.finalize() when you are done with the statement.

// One-shot INSERT — chain prepare().run()
const info = db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 30);
println(info.changes);         // 1
println(info.lastInsertRowid); // 1

// One-shot SELECT — chain prepare().all()
const rows = db.prepare("SELECT * FROM users WHERE age >= ?").all(18);
println(rows.length());

// Reusable statement
const stmt = db.prepare("SELECT * FROM users WHERE age >= ?");
const adults = stmt.all(18);
const first  = stmt.get(18);
stmt.finalize();

close

db.close()

Closes the database connection. Before closing, any prepared statements that are still open (including one-shot statements created via inline chains like db.prepare(sql).run(x)) are automatically finalized via sqlite3_next_stmt, ensuring the connection frees all of its resources immediately. Calling close() on an already-closed database is a no-op. Do not use any Statement objects after the database has been closed.

db.close();

Statement

A Statement is obtained via db.prepare(sql). Unlike the raw SQLite C API, you never call step / reset manually — the run / get / all methods handle the full lifecycle internally (modelled after better-sqlite3).

run

stmt.run([...params]) → { changes: int, lastInsertRowid: num }

Executes the statement (intended for DML: INSERT, UPDATE, DELETE) and returns an info object. Parameters are bound for this call only, unless bind() was already used to lock them permanently.

const r1 = db.prepare("INSERT INTO users (name, age) VALUES (?, ?)").run("Alice", 30);
println(r1.changes);         // 1
println(r1.lastInsertRowid); // 1

const r2 = db.prepare("UPDATE users SET age = ? WHERE name = ?").run(31, "Alice");
println(r2.changes); // 1

// Named parameters
const r3 = db.prepare("INSERT INTO users (name, age) VALUES (@name, @age)")
              .run({ name: "Bob", age: 25 });
println(r3.lastInsertRowid); // 2

get

stmt.get([...params]) → object | null

Executes the query and returns the first matching row as an object whose keys are the column names, or null if no rows are found. Column types are mapped as shown in the type table below.

const user = db.prepare("SELECT * FROM users WHERE id = ?").get(1);
println(user.name); // Alice
println(user.age);  // 31

// Named parameter
const byName = db.prepare("SELECT * FROM users WHERE name = @name")
                  .get({ name: "Bob" });
println(byName); // { id: 2, name: "Bob", age: 25 }

// Returns null when nothing matches
const missing = db.prepare("SELECT * FROM users WHERE id = ?").get(999);
println(missing); // null

all

stmt.all([...params]) → array

Executes the query and collects every matching row into an array. Each element is a row object (same shape as get()). Returns an empty array if no rows match.

const all = db.prepare("SELECT * FROM users").all();
all.each(fn(row, i) { println(row.id, row.name); });

// With a filter
const seniors = db.prepare("SELECT * FROM users WHERE age >= ?").all(30);
println(seniors.length());

// Reusable: call all() multiple times with different params
const stmt = db.prepare("SELECT * FROM users WHERE age >= ?");
println(stmt.all(18).length());
println(stmt.all(30).length());
stmt.finalize();

Type mapping

SQLite type ZayneScript type
INTEGER int
REAL num
TEXT string
NULL null
BLOB null

bind

stmt.bind([...params]) → this

Permanently binds parameters to the statement for its entire lifetime. Bindings survive internal resets so the statement can be executed many times without rebinding. After calling bind() you must not pass parameters to run(), get(), or all(). Supports the same positional and named-object conventions as those methods. Returns this for chaining: db.prepare(sql).bind(x).get().

// Positional permanent bind
const topUser = db.prepare("SELECT * FROM users WHERE id = ?").bind(1);
println(topUser.get()); // { id:1, name:"Alice", age:31 }
println(topUser.get()); // same result – statement is reused
topUser.finalize();

// Named permanent bind
const byName = db.prepare("SELECT * FROM users WHERE name = @name")
                  .bind({ name: "Bob" });
println(byName.get()); // { id:2, ... }
byName.finalize();

pluck

stmt.pluck([bool]) → this

Toggles pluck mode. When on (the default when called with no argument or true), get() and all() return the value of the first column only instead of the full row object. Pass false to turn pluck back off. Chainable.

const stmt = db.prepare("SELECT id FROM users");

// Returns array of id values instead of array of objects
const ids = stmt.pluck().all();
ids.each(fn(id, i) { println(id); }); // 1  2  3 ...

stmt.pluck(false); // back to full row objects
const rows = stmt.all();

stmt.finalize();

columns

stmt.columns() → array of { name, column, table, database, type }

Returns an array of column metadata objects for the result set, matching the better-sqlite3 shape:

Field Description
name Column alias or name (always populated)
column Source column name (null for expressions)
table Source table name (null for expressions)
database Source database name (null for expressions)
type Declared type string, e.g. "TEXT" (null for expressions)
Note: column, table, and database are only populated when the bundled SQLite amalgamation is compiled with SQLITE_ENABLE_COLUMN_METADATA.
const stmt = db.prepare("SELECT id, name, age FROM users");
const cols = stmt.columns();
cols.each(fn(c, i) {
    println(c.name, c.type); // id INTEGER, name TEXT, age INTEGER
});
stmt.finalize();

finalize

stmt.finalize()

Destroys the prepared statement and frees its resources. After this call the statement must not be used. Calling finalize() on an already-finalized statement is a safe no-op. Note: db.close() automatically finalizes any not-yet-finalized statements, so explicit finalize() calls are only strictly necessary if you want to release the statement's resources before closing the database.

stmt.finalize();

Common Patterns

In-memory scratch database

import { Database } from "core:sqlite";
import { println } from "core:io";

const db = new Database(":memory:");
db.exec("CREATE TABLE kv (key TEXT PRIMARY KEY, value TEXT)");

db.prepare("INSERT INTO kv VALUES (?, ?)").run("theme", "dark");

const row = db.prepare("SELECT value FROM kv WHERE key = ?").get("theme");
println(row.value); // dark
db.close();

Bulk insert wrapped in a transaction

import { Database } from "core:sqlite";

const db    = new Database("data.sqlite");
const names = ["Alice", "Bob", "Carol", "Dave", "Eve"];

db.exec("CREATE TABLE IF NOT EXISTS people (id INTEGER PRIMARY KEY, name TEXT)");
db.exec("BEGIN");

const stmt = db.prepare("INSERT INTO people (name) VALUES (?)");
names.each(fn(name, i) {
    stmt.run(name); // run() resets and re-executes automatically
});
stmt.finalize();

db.exec("COMMIT");
db.close();

Named parameters

// SQLite supports @name, :name, and $name placeholders.
// Pass a plain object to run / get / all to bind by name.
db.exec("CREATE TABLE IF NOT EXISTS products (id INTEGER PRIMARY KEY, sku TEXT, price NUM)");
db.prepare("INSERT INTO products (sku, price) VALUES (@sku, @price)")
  .run({ sku: "WIDGET-42", price: 9.99 });

const p = db.prepare("SELECT * FROM products WHERE sku = @sku")
             .get({ sku: "WIDGET-42" });
println(p.price); // 9.99

Pluck — collect a single column

const stmt = db.prepare("SELECT name FROM users ORDER BY name");
const names = stmt.pluck().all();
names.each(fn(n, i) { println(n); }); // Alice  Bob  Carol ...
stmt.finalize();

Multiple independent connections

const users  = new Database("users.sqlite");
const orders = new Database("orders.sqlite");
const cache  = new Database(":memory:");

// All three connections are fully independent.
users.close();
orders.close();
cache.close();

Error handling

import { Database } from "core:sqlite";
import { println } from "core:io";

const db = new Database(":memory:");
db.exec("CREATE TABLE t (id INTEGER PRIMARY KEY, val TEXT UNIQUE)");

try {
    db.prepare("INSERT INTO t VALUES (1, 'hello')").run();
    db.prepare("INSERT INTO t VALUES (1, 'hello')").run(); // duplicate PK
} catch (e) {
    println("Caught:", e);
}
db.close();

core:string — text helpers

Import with:

import {
    toUpper, toLower, capitalize,
    isIdentifier, isAlpha, isDigit, isAlnum,
    split, ord, chr, bytes, codepoints,
    strip, join, repeat, reverse,
    contains, startsWith, startswith, endsWith, endswith,
    replace, byteLength, encode, decode
} from "core:string";

core:string complements core:utf8: higher-level helpers on script strings (Unicode scalar / rune strings at runtime). Case mapping and classification use utf8proc. Normalization and case folding stay in core:utf8 (nfc, casefold, …).


Overview

Quick example

import { println } from "core:io";
import {
    toUpper, toLower, capitalize,
    isIdentifier, isAlpha, isDigit, isAlnum,
    split, join, strip, replace,
    ord, chr, byteLength, bytes, encode, decode
} from "core:string";

println(toUpper("ab"));                         // AB
println(toLower("AB"));                         // ab
println(capitalize("hello WORLD"));             // Hello world
println(isIdentifier("_x9"));                   // true
println(isAlpha("abc"));                        // true
println(isDigit("42"));                         // true
println(isAlnum("a1"));                        // true
println(join(split("a,b,c", ","), "-"));       // a-b-c
println(strip("  x  "));                        // x
println(replace("one one", "one", "two"));      // two two
println(ord("X"));                              // 88
println(ord(chr(65)));                          // 65
println(byteLength("hello"));                 // 5
println(split("  a  b  ").length());           // 2 (whitespace split)
println(bytes("AB").length());                // 2 (UTF-8 byte count)
println(decode(encode("hello")));             // hello (UTF-8 roundtrip)
println(encode(bytes("AB")));                 // QUI=

Functions (reference)

Native bindings are implemented in src/core/string.c and declared for the host from src/core/string.h (LoadCoreString). Errors use the usual Error value pattern (wrong arity or type).

toUpper(s) / toLower(s) / capitalize(s) → string

Per-codepoint case mapping (capitalize: title + lower rest).

isIdentifier(s) / isAlpha(s) / isDigit(s) / isAlnum(s) → bool

Whole-string predicates; empty string is false for the is* checks.

split(s) / split(s, delim) → array

One-arg whitespace split; two-arg delimiter split (delim non-empty UTF-8 substring).

ord(s) → int

Exactly one codepoint required.

chr(cp) → string

One Unicode scalar; invalid codepoint is an error.

bytes(s) / codepoints(s) → array

UTF-8 byte values vs UTF-32 code units as integers.

byteLength(s) → int

strlen of the UTF-8 encoding.

strip(s) → string

Trim leading/trailing Unicode whitespace (same categories as one-arg split).

join(parts, sep) → string

parts must be an array of strings.

repeat(s, n) → string

n non-negative integer.

reverse(s) → string

Reverses codepoint order (not full grapheme clusters).

contains(s, sub) / startsWith(s, p) / startswith(s, p) / endsWith(s, p) / endswith(s, p) → bool

UTF-8 substring / prefix / suffix tests; startswith/endswith are lowercase aliases.

replace(haystack, from, to) → string

Non-overlapping replacements; from must be non-empty.

encode(s) / encode(byteArray) → string

Standard Base64 (no line breaks). s is UTF-8 text; byteArray is an array of byte values 0..255.

decode(s) → string

Decodes Base64; skips spaces, tabs, CR, LF in s. Result must be well-formed UTF-8 or callers receive an error value (binary payloads are not returned as raw strings).

core:utf8 — Unicode and UTF-8

Import with:

import {
    nfc, nfd, nfkc, nfkd, casefold,
    toLower, toUpper, toTitle, len,
    charWidth, categoryString, validCodepoint,
    graphemeBreak, version
} from "core:utf8";

String values in ZayneScript are stored internally as UTF-32 rune arrays (Unicode scalar values). The core:utf8 module wraps utf8proc for normalization, case folding, character metadata, and grapheme boundary checks. Functions that take or return string use the same runtime representation as the rest of the language.


Overview

Normalization

nfc(s) → string

Canonical composition (NFC). Equivalent to Unicode normalization form NFC.

nfd(s) → string

Canonical decomposition (NFD).

nfkc(s) → string

Compatibility composition (NFKC). Often used for matching and search.

nfkd(s) → string

Compatibility decomposition (NFKD).

import { nfc, len } from "core:utf8";
assert len(nfc("")) == 0;

Case mapping and folding

toLower(s) → string

Simple lowercase mapping per codepoint (not locale-aware title rules for whole strings).

toUpper(s) → string

Uppercase mapping per codepoint.

toTitle(s) → string

Titlecase mapping per codepoint.

casefold(s) → string

Full Unicode case fold, suitable for case-insensitive equality of strings.

import { toLower, casefold } from "core:utf8";
println(toLower("AbC")); // abc

Codepoint properties and graphemes

len(s) → int

Rune count of s (excluding any internal terminator).

charWidth(cp) → int

Display width for numeric codepoint cp (similar to wcwidth, with non-printable widths defined by utf8proc).

categoryString(cp) → string

Two-letter Unicode general category for integer codepoint cp (for example "Lu" for an uppercase letter).

validCodepoint(cp) → bool

Whether cp is a valid Unicode scalar value (in range, not a surrogate).

graphemeBreak(prev, curr) → bool

Whether a grapheme cluster boundary is allowed between codepoints prev and curr (UAX #29, legacy mode without full state machine).

import { categoryString, validCodepoint } from "core:utf8";
println(categoryString(65)); // Lu (Latin uppercase)
println(validCodepoint(65)); // true

version

version() → string

Returns the utf8proc library version string (for example "2.1.0").

import { version } from "core:utf8";
println(version());

lib:argparser — command-line argument parser

The repository includes lib/argparser.zs, copied next to the executable at build time (see the dist/ layout on the home page). It parses the string array returned by args() from core:os into positionals, boolean flags, and value flags.

How args() relates to the shell

Everything after the script path on the host command line becomes args(). Tokens are split on ASCII spaces only; use lib:argparser on that flat list the same way you would use a classic argv in C.

ArgParser class

Import symbols:

import { ArgParser, createArgParser, parseWithDefs } from "lib:argparser";

Supported syntax (high level):

import { args } from "core:os";
import { createArgParser } from "lib:argparser";
import { println } from "core:io";

const p = createArgParser();
p.option({ long: "output", short: "o", takesValue: true, help: "output file" });
p.option({ long: "verbose", short: "v", help: "more logging" });

const r = p.parse(args());
if (!r.ok) {
    println(r.errors);
} else {
    println("flags:", r.flags, "positionals:", r.positional);
}

parseWithDefs

parseWithDefs(defs, argv) builds a temporary parser, registers every object in the defs array with option, then returns parse(argv). Useful for small tools without keeping a parser instance.

import { parseWithDefs } from "lib:argparser";

const r = parseWithDefs([
    { long: "out", short: "o", takesValue: true },
    { long: "quiet", short: "q" }
], argv);

See tests/test_args.zs for a runnable example with args() and lib:argparser.

lib:http — bundled HTTP helpers

The repository includes lib/http.zs, copied next to the interpreter at build time. It imports request from core:mongoose and builds two frozen lookup tables with Object.freeze from core:object: StatusCode and Method.

import { request, Method, StatusCode } from "lib:http";

request("http://localhost:3000/missing", {
    method: Method.GET
}).then(fn(res) {
    if (res.status == StatusCode.NotFound) {
        println("not found");
    } else {
        println(res.status, res.body);
    }
});

StatusCode

A frozen object mapping symbolic names to their numeric HTTP status codes. Access any code with StatusCode.Ok, StatusCode.NotFound, etc.

NameCodeCategory
Continue1001xx Informational
SwitchingProtocols1011xx Informational
Processing1021xx Informational
EarlyHints1031xx Informational
Ok2002xx Success
Created2012xx Success
Accepted2022xx Success
NonAuthoritativeInformation2032xx Success
NoContent2042xx Success
ResetContent2052xx Success
PartialContent2062xx Success
MultipleChoices3003xx Redirection
MovedPermanently3013xx Redirection
Found3023xx Redirection
SeeOther3033xx Redirection
NotModified3043xx Redirection
TemporaryRedirect3073xx Redirection
PermanentRedirect3083xx Redirection
BadRequest4004xx Client Error
Unauthorized4014xx Client Error
PaymentRequired4024xx Client Error
Forbidden4034xx Client Error
NotFound4044xx Client Error
MethodNotAllowed4054xx Client Error
NotAcceptable4064xx Client Error
ProxyAuthRequired4074xx Client Error
RequestTimeout4084xx Client Error
Conflict4094xx Client Error
Gone4104xx Client Error
LengthRequired4114xx Client Error
PreconditionFailed4124xx Client Error
PayloadTooLarge4134xx Client Error
UriTooLong4144xx Client Error
UnsupportedMediaType4154xx Client Error
RangeNotSatisfiable4164xx Client Error
ExpectationFailed4174xx Client Error
ImATeapot4184xx Client Error
MisdirectedRequest4214xx Client Error
UnprocessableEntity4224xx Client Error
Locked4234xx Client Error
FailedDependency4244xx Client Error
TooEarly4254xx Client Error
UpgradeRequired4264xx Client Error
PreconditionRequired4284xx Client Error
TooManyRequests4294xx Client Error
RequestHeaderFieldsTooLarge4314xx Client Error
UnavailableForLegalReasons4514xx Client Error
InternalServerError5005xx Server Error
NotImplemented5015xx Server Error
BadGateway5025xx Server Error
ServiceUnavailable5035xx Server Error
GatewayTimeout5045xx Server Error
HttpVersionNotSupported5055xx Server Error
InsufficientStorage5075xx Server Error
LoopDetected5085xx Server Error
NetworkAuthRequired5115xx Server Error

Method

A frozen object mapping HTTP verb names to their string values. Use Method.GET, Method.POST, etc. to avoid typos in request code:

NameValue
Method.GET"GET"
Method.POST"POST"
Method.PUT"PUT"
Method.PATCH"PATCH"
Method.DELETE"DELETE"
Method.HEAD"HEAD"
Method.OPTIONS"OPTIONS"
Method.CONNECT"CONNECT"
Method.TRACE"TRACE"
import { request, Method, StatusCode } from "lib:http";
import { println } from "core:io";

// POST with JSON body using Method and StatusCode constants
request("http://api.example.com/users", {
    method: Method.POST,
    headers: { "Content-Type": "application/json" },
    body: { name: "Alice", age: 30 }
}).then(fn(res) {
    if (res.status == StatusCode.Created) {
        println("User created:", res.body);
    } else if (res.status == StatusCode.BadRequest) {
        println("Bad request:", res.body);
    } else {
        println("Unexpected status:", res.status);
    }
}).error(fn(e) {
    println("Request failed:", e);
});

See tests/test_request.zs for a minimal client example using lib:http.