Language reference · implementation guide

OpenScript

Syntax and features currently implemented by the parser, compiler, and bytecode runtime. Examples use the language's own syntax; feature notes describe implementation behavior rather than aspirations.

Quick start

Build from the repository root with ./build.sh on macOS/Linux or build.bat on Windows. With no arguments, either script writes the executable to the repository root as openscript or openscript.exe. The optional --all builds compressed releases under dist/compressed/ for supported Windows, Linux, macOS, BSD, and WebAssembly targets (js/wasm and wasip1/wasm). Each archive has its own .sha256 checksum file, and the JS/WASM package includes Go's Node.js runtime files.

Scripts use the .os extension. The executable accepts a script path with -f or -file. The built-in I/O module provides println.

import { println } from "core:io";

const greeting = "Hello, OpenScript!";
println(greeting);
./openscript -f hello.os
# Windows: .\openscript.exe -f .\hello.os

Statements generally end in semicolons. Blocks use braces. Whitespace and comments separate tokens but are otherwise insignificant.

Lexical rules

Identifiers

Identifiers may use Unicode letters, letter numbers, combining marks, decimal digits after the first character, connector punctuation, and underscore. Keywords are reserved unless a construct explicitly accepts a keyword name, such as a member accessed with ->.

Comments and whitespace

// Line comment

/* Block comment
   may span lines */

Unicode whitespace and common Unicode line separators are recognized. Line breaks are not statement terminators; semicolons are used explicitly.

Strings and numbers

Strings may use single or double quotes and cannot contain an unescaped line break. Recognized escapes include \a, \b, \f, \n, \r, \t, \v, \0, escaped quotes and backslash, \xNN, \uNNNN, \UNNNNNNNN, and braced Unicode \u{...}.

Numeric literals include decimal integers and fractions, exponent notation, and hexadecimal (0x), octal (0o), and binary (0b) prefixes. Underscores are accepted in digit runs.

Values and literals

The language includes number, BigInt, string, boolean, null, array, object, function, class, and promise values. BigInt literals use an n suffix and support exact integer arithmetic. Boolean and null literals are true, false, and null.

var enabled = true;
var absent = null;
var ratio = 12.5e-2;
var large = 12345678901234567890n;
var mask = 0xff;

Arrays use square brackets. Objects use braces and string or identifier keys:

var colors = ["red", "green", "blue"];
var person = { name: "Ada", "role": "engineer" };

Expressions

Expressions support literals, identifiers, grouping, function and class expressions, calls, property/index access, unary and binary operators, assignments, ternaries, await, and switch expressions.

Operator precedence

From highest to lowest, according to the parser's expression chain:

Level Operators / forms Associativity
Postfix Call (), member ->, index [], postfix ++ -- Left to right
Unary + - ! ~ prefix ++ -- await Right to left
Multiplicative * / % Left to right
Additive + - Left to right
Shift << >> Left to right
Relational < <= > >= Left to right
Equality == != Left to right
Bitwise AND / XOR / OR & then ^ then | Left to right
Logical AND / OR && then || Left to right
Assignment =, compound assignment operators Right to left
Ternary condition ? thenExpr : elseExpr Right-associative branches

Compound assignments are *=, /=, %=, +=, -=, <<=, >>=, &=, ^=, and |=.

Logical operators short-circuit. == and != use runtime value equality; switch matching uses the runtime's case-equality operation.

var score = 8;
var message = score >= 6 && score < 10
    ? "pass"
    : "retry";

score += 1;
score++;
var next = score--;

Access and calls

person->name = "Grace";
person["name"];
colors[0] = "amber";
printValue(person->name);

Property access uses ->; bracket access accepts an expression as a key/index. Calls use parenthesized comma-separated arguments.

Variables and patterns

Declarations

var globalCount = 0;
let localName = "Ada";
const limit = 100;
var first = 1, second = 2;

var is restricted to global/module scopes. let is not allowed at global scope. const is supported at both. Compiler checks also reject duplicate names in the same scope and assignment to constants.

Array destructuring

const [red, green, blue] = colors;

Object destructuring

const { name, role: jobTitle } = person;

Object patterns may bind a property under its original name or rename it with property: localName. Destructuring is available with var, let, and const, subject to the same scope rules as ordinary declarations.

Functions

Function expressions use fn. Named function declarations use the top-level declaration form with ::.

var add::fn(left, right) {
    return left + right;
};

var increment = fn(value) {
    return value + 1;
};

var anonymous = fn() {
    return null;
};

Functions support positional parameters, return statements, closures over outer variables, and variadic parameters marked with .... A variadic parameter collects remaining arguments into an array.

var collect::fn(prefix, values...) {
    println(prefix);
    println(values);
};

collect("items", 2, 3, 4);

A function without a returned value yields null. A return may omit its expression, which also returns null.

Async functions and await

var fetchValue = fn() async {
    return 42;
};

var result = await fetchValue();

Calling an async function produces a promise. await is accepted only inside an async function and suspends until the awaited promise settles.

Arrays and objects

Array literals accept expressions and spread elements. Object literals accept key/value entries and object spread.

var more = [0, ...colors, "violet"];
var profile = {
    name: "Ada",
    ...person,
    active: true
};

Arrays support numeric indexing and assignment. Objects support property access with -> or bracket notation, and property assignment. Missing object properties read as null; array bounds violations are runtime errors.

more[0] = 10;
profile->active = false;
profile["name"];

Arrays expose ->len(), ->map(callback), ->filter(callback), and ->find(callback). Each callback receives the element, its zero-based index, and the source array. map returns transformed values, filter retains values whose callback result is truthy, and find returns the first matching value or null.

Objects expose ->map(callback), ->filter(callback), and ->find(callback). Their callbacks receive the value, key, and source object; map and filter preserve keys, while find returns the first matching value or null.

Strings expose ->len(), ->contains(text), ->startsWith(text), ->endsWith(text), ->indexOf(text), ->toLower(), ->toUpper(), ->trim(), ->split(separator), ->replace(search, value), and ->replaceAll(search, value). len and indexOf use UTF-8 byte counts and offsets; use core:unicode for code-point operations. Case conversion also has toLowerCase and toUpperCase aliases.

var values = [3, 8, 11];
var doubled = values->map(fn(value, index) {
    return value * 2 + index;
});
var firstLarge = values->find(fn(value) {
    return value > 5;
});

Classes

Classes may define a constructor and methods. When omitted, an empty constructor is supplied. A class can extend one named base class. Instances are created by calling the class name.

var Animal::class {
    constructor: fn(name) {
        this->name = name;
    },
    speak: fn() {
        return "...";
    }
};

var Dog::class(Animal) {
    constructor: fn(name) {
        super(name);
        this->kind = "dog";
    },
    speak: fn() {
        return "woof";
    }
};

var pet = Dog("Milo");
pet->speak();

this refers to the instance inside a constructor or method. super(...) invokes the base constructor. Methods are inherited through the base-class chain; instance fields are assigned through this->field.

Control flow

Conditional statements

if (ready) {
    start();
} else {
    wait();
}

The body may be a braced block or a single block-level statement.

log expression; is a dedicated statement that writes the expression through the runtime's standard output path.

log "diagnostic value";

Loops

loop supports a while-style condition and a three-part for-style form. The for-style initializer may be a declaration or expression; its condition and update are separated by semicolons.

var index = 0;
loop (index < 3) {
    println(index);
    index++;
}

loop (let i = 0; i < 3; i++) {
    println(i);
}

loop (;;) {
    break;
}

An omitted for-style condition is treated as an unconditional loop. break; exits the nearest loop or switch. continue; inside a loop starts its next iteration. Inside a switch case, continue advances to the next case body; a nested loop takes precedence.

Switch

Statement switch

A statement switch compares its selector to each label, supports multiple comma-separated values per case, and permits one optional default in any position. Cases execute in source order after dispatch: they do not automatically break. Empty cases and ordinary case completion fall through. Use break; to exit the switch.

switch (value) {
    case 0:
    case 1, 2, 3:
        println("small");
        break;
    case 4:
        println("four");
        continue; // continue with the next case body
    default:
        println("other");
}
Control-flow meaning: break exits the switch. continue skips the rest of the current case and jumps to the following case body; from the final case it exits the switch. A loop nested inside a case handles its own break/continue first.

Switch expression

Switch expressions produce one value and require a default alternative. Their alternatives use commas and =>; each result is an expression.

var label = switch (status) {
    200, 201 => "success",
    404 => "missing",
    default => "other"
};

Unlike statement switches, switch expressions do not have case bodies with statements or fall-through.

Exceptions

throw expression; throws any value, including strings, numbers, arrays, objects, and Error instances. An optional catch(name) binds the original thrown value to a local name. Runtime-generated errors are caught as Error objects with message and line properties.

try {
    throw "configuration is missing";
} catch (err) {
    println(err);
}

A try without a catch is accepted and executes its body normally. There is no finally syntax in the current parser.

Modules and imports

Import a module for its side effects, or import named exports. Named imports may be renamed with exportName: localName.

import "./setup.os";
import { println } from "core:io";
import { isEven: checkEven } from "lib:number";

Built-in module imports require named imports. Relative module paths resolve from the importing file. Library imports use lib:name and resolve under the executable's lib directory; path traversal outside that directory is rejected. Circular imports are detected and reported.

Top-level declarations are made available as module exports. Use the import form supported by the target module; built-ins validate requested export names.

Core modules

Core modules are imported by name and expose only named exports.

API reference

Choose an API from its module submenu to read its behavior and a runnable usage example.

Bundled libraries

Library modules are imported with the lib:name form and loaded from the executable's lib directory. The shipped modules provide these primary interfaces:

Module Primary API Behavior
lib:argparse ArgumentParser(config?), ArgumentError(message) Creates a command-line parser. Configure it with prog, description, allow_abbrev, and add_help; register options and positionals with add_argument(definition). Parser methods include parse_args(args?), parse_known_args(args?), format_help(), and print_help(). Argument definitions support defaults, choices, type conversion, required options, and nargs forms ?, *, +, or a fixed count. Parsing errors are returned as objects with an error field; known-argument parsing returns a namespace and unknown list.
lib:number isEven(value), isOdd(value), isPositive(value), isNegative(value), isZero(value), isInteger(value) Numeric predicates for parity, sign, zero, and integral values. Zero is neither positive nor negative; integer checks reject values with a fractional part.
lib:crypto CryptoJS-style hashes, HMAC, WordArray, encoders, algorithm factories, PBKDF2, and EvpKDF. Bundled facade over the native cryptographic primitives; legacy hashes are documented with their interoperability limitations.
lib:tui Terminal colors, status messages, prompts, confirmation, and table output. ANSI-based terminal helpers and simple text-table rendering.

Runtime features

  • Built-ins: core:io, core:file, core:math, core:number, core:unicode, core:sys, core:path, core:json, core:regex, core:error, core:http, and core:socket and core:crypto provide I/O, filesystem, numeric, Unicode, host-system, path, JSON, regular-expression, error, HTTP, loopback TCP, and cryptographic operations.
  • Built-in value methods: arrays provide indexed access, mutation, and len/map/filter/find; objects provide map/filter/find; strings provide length, search, case-conversion, trimming, splitting, and replacement methods.
  • Promises: async calls return promises. Promise values expose ->then(fn) and ->catch(fn) chaining.
  • Closures: nested functions can capture and mutate variables from enclosing functions.
  • Errors: parser/compiler errors include source positions; runtime errors can be handled with try/catch.
  • Line mapping: generated bytecode retains source line information for runtime diagnostics.

Implementation notes

This page documents syntax and behavior visible in the parser/compiler implementation, not a complete formal language specification. A few boundaries are especially useful to know:

  • There are no parser forms for for, while, do, finally, or labels; looping uses loop.
  • Named fn and class declarations use top-level name::fn / name::class syntax. Anonymous function and class expressions are also supported.
  • Class declarations and expressions may omit the constructor; the runtime supplies an empty one.
  • Switch statement matching is performed at dispatch before bodies run. Once a body begins, C-style fall-through applies unless break or the case-specific continue changes control flow.
  • Switch expressions require a default and are parsed as comma-separated alternatives.
  • The compiler validates declaration placement, duplicate local names, constant assignment, await context, and break/continue context.
Version note: the exact supported syntax is the implementation in main/parser.go and main/compiler.go at the time this reference was generated. Runtime built-in modules may expose additional APIs beyond syntax covered here.