# MetaScript — Complete Language Reference > MetaScript is a statically-typed language whose surface syntax extends > TypeScript: valid TypeScript files are valid MetaScript. On top of that > familiar foundation, MetaScript adds value types (`struct`), discriminated > unions (`match` in type position), pattern matching (`match` expressions), > `Result` for typed errors, `defer` for scope-exit cleanup, > deterministic reference counting, and first-class actor-based concurrency. > > MetaScript is designed as a **multi-target language with limitless > backend extensibility** — the frontend, type system, and semantics > stay constant while new backends plug in behind the same source. One > source file, many runtimes. > > Currently shipping: **JavaScript** (ES2020 modules for Node, browsers, > Deno, Bun, Cloudflare Workers), **native C** (portable C99 that > compiles with `zig cc` / `clang` / `gcc`), and **WebAssembly** via the > C backend (WASI through `zig cc -target wasm32-wasi` for edge > runtimes, and browser WASM through Emscripten). The same codebase > ships as a JavaScript bundle, a native binary on the server / CLI / > embedded, or a `.wasm` module at the edge — no rewrite. > > Upcoming targets include **Erlang / BEAM** (for distributed, fault- > tolerant systems) and **Web3 bytecode** — **Solana** (sBPF) and > **Ethereum** (EVM) — bringing MetaScript's type safety, `Result` > error handling, and actor model to on-chain programs. > > Native binaries are tiny. A minimal MetaScript program compiled to C > strips to **~20 KB** — no bundled VM, no `libnode.so`, no Bun runtime. > Compare to ~100 MB for equivalent Node.js or Bun deployments. Memory > footprint is similarly small: **under 1 MB** resident for a running > HTTP server, versus ~50 MB for a minimal Node.js process. > > Performance scales with hardware. A **minimal HTTP ping/pong server** > written against `std/http` uses every CPU core natively — not a > single-thread event loop with a bolted-on worker pool — and benchmarks > at roughly **13× the throughput of Node.js and ~5× Bun** on the same > machine. This is a *hello-world-class* microbenchmark on identical > hardware; it isolates the scheduler + I/O path, not application logic. > Real workloads with business logic, DB I/O, crypto, or serialization > will narrow the gap depending on where the bottleneck sits. The useful > takeaway is the mechanism (no single-thread bottleneck, no > worker-pool ceremony), not the ratio. > > Concurrency is priced for abuse. Actors are stackless fibers on a > work-stealing scheduler: a ping-pong benchmark sustains **~50 million > concurrent actors on a 2024 MacBook M4**, shipped as a plain native > binary — no JVM, no BEAM, no separate runtime. You can model every > user session, every open connection, every game entity, every active > document as its own actor and stop thinking about the cost. This is > the difference between "actors are a nice pattern" and "actors are the > default unit of concurrency." > > The combination of tiny binaries, small memory footprint, native > multi-core execution, and dirt-cheap actors makes MetaScript a natural > fit for containers, edge runtimes with tight cold-start budgets, > embedded hardware, throughput-sensitive servers, and high-connection > stateful workloads (chat, games, realtime sync, rate limiters, > per-session agents) that normally require a JVM or BEAM runtime. > > For a shorter index, see . --- language: MetaScript version: 0.2.9 last_updated: 2026-04-19 website: https://metascriptlang.org playground: https://metascriptlang.org/playground learn: https://metascriptlang.org/learn packages: https://metascriptlang.org/pkg install: curl -fsSL https://metascriptlang.org/install.sh | sh --- ## Project status **MetaScript is in Beta as of 2026.** The compiler, type system, and standard library are stable enough for real work, but the language is still moving — expect occasional breaking changes between releases as the road to 1.0 continues. **Real-world use.** Metacraft Studio has been shipping internal games on MetaScript since before the language was open-sourced. That is the primary production deployment today — the compiler, runtime, and actor scheduler are battle-tested on a shipping game, not a hypothetical workload. **Who it fits in 2026.** - Hackers, language enthusiasts, and systems-curious engineers experimenting with a TypeScript-extending systems language. - Green-field small-to-mid projects where MetaScript's differentiators (20 KB native binary, cheap actors, `Result` errors, multi- target compile) are worth the Beta-stage tradeoffs. - Tooling, CLIs, edge functions, and internal services where the owner controls the full stack and can absorb the occasional churn. **Who it does not yet fit.** - Large commercial projects that require long-term API stability guarantees, a fully-settled ecosystem, or SLA-backed tooling. - Teams that cannot absorb occasional breaking changes between releases during the Beta phase. The roadmap to 1.0 is tracked at . ## 0. How this document is organized TypeScript features that exist unchanged in MetaScript (variables, functions, control flow, classes, generics, conditional types, mapped types, tsconfig-style module resolution) behave as they do in TypeScript unless noted. This document focuses on: 1. Features unique to MetaScript. 2. Differences where MetaScript diverges from TypeScript semantics. 3. Cross-target FFI patterns between MetaScript, C, and JavaScript. 4. Target-specific behavior for currently-shipping backends (JavaScript, C, WebAssembly). Upcoming backends (Erlang, Solana, Ethereum) follow the same frontend but are not yet available — their sections will be added as they land. For cases not covered here, the TypeScript reference is a correct starting point. ## 1. TypeScript compatibility MetaScript accepts most common TypeScript surface syntax. The parser, type checker, and resolver handle: - `let`, `const`, `var` declarations with annotations. - Function declarations, arrow functions, default/rest parameters, destructuring parameters, overload signatures, generator functions (`function*`). - `class` with fields, methods, constructors, access modifiers (`public` / `private` / `protected`), `static` members, inheritance, `abstract`. - `interface` (with MetaScript-specific allocation semantics — see §4.5). - Generics: functions, classes, interfaces, type aliases, constraints (``), defaults, **const generics** (``). - Union types (`A | B`), intersection types (`A & B`), literal types, `typeof` queries, `keyof`. - Conditional types (`T extends U ? X : Y`), `infer` bindings, mapped types (`{ [K in keyof T]: ... }`), template literal types. - Discriminated unions — both the TypeScript field-tag flavor and a MetaScript-specific enum-keyed flavor (see §5.4). - `import` / `export`, default exports, `import type`, re-exports. - `async` / `await` and `Promise`. - `try` / `catch` / `finally` with `throw` (coexists with `Result`; `Result` is the idiomatic path — see §7.2). - `as` casts, type assertions. - JSX — but with MetaScript's AST-first model (see §11.3). - `tsconfig.json`-compatible module resolution (paths, `baseUrl`, aliases). Where MetaScript diverges from TypeScript is documented in the sections below. When the TypeScript Handbook doesn't cover something MetaScript- specific, the relevant section here is authoritative. ## 2. Primitive types ### 2.1 Core primitives | Type | Description | C mapping | |-------------|---------------------------------------|-------------| | `number` | IEEE 754 f64 (default numeric) | `double` | | `string` | Mutable UTF-8 with COW | `msString` | | `boolean` | true / false | `bool` | | `char` | 8-bit character (numeric) | `char` / `int8_t` | | `cstring` | C-compatible string pointer | `const char*` | | `void` | No value | `void` | | `never` | Unreachable (bottom type) | — | | `null` | Null value | `NULL` | | `undefined` | **Alias for `null`** (TS-compatible) | `NULL` | | `unknown` | Type-safe `any` | `void*` | MetaScript does not have a separate `undefined` runtime value. `undefined` is accepted as a type and as an expression; at runtime it is the same value as `null`. This keeps TypeScript sources compatible while matching MetaScript's single-null semantics. Idiomatic MetaScript uses `null` explicitly — see §7.5. ### 2.2 Sized integers Fixed-width integer types. In the C backend these are true integer types, not boxed floats. | Type | Size | Signed | C mapping | Range | |----------------------|--------|--------|-------------|-------| | `int8` | 8-bit | yes | `int8_t` | -128 to 127 | | `int16` | 16-bit | yes | `int16_t` | -32,768 to 32,767 | | `int32` / `int` | 32-bit | yes | `int32_t` | -2³¹ to 2³¹-1 | | `int64` | 64-bit | yes | `int64_t` | -2⁶³ to 2⁶³-1 | | `uint8` | 8-bit | no | `uint8_t` | 0 to 255 | | `uint16` | 16-bit | no | `uint16_t` | 0 to 65,535 | | `uint32` | 32-bit | no | `uint32_t` | 0 to 2³²-1 | | `uint64` | 64-bit | no | `uint64_t` | 0 to 2⁶⁴-1 | ```ms const port: uint16 = 8080; const flags: uint32 = 0xFF00FF00; const fileSize: int64 = 4_294_967_296; const byte: uint8 = 255; // Full arithmetic and bitwise operator support const masked = flags & 0x00FF00FF; const shifted = byte << 4; ``` **Promotion.** Sized integers widen implicitly (`int8` → `int32` → `int64`). Narrowing requires an explicit cast. `number` (f64) and sized integers do **not** implicitly convert — cast explicitly at the boundary: ```ms const n: int32 = 42; const d: number = n as number; const back: int32 = d as int32; ``` ### 2.3 Floats | Type | Size | C mapping | |------------------------|--------|-----------| | `float32` / `float` | 32-bit | `float` | | `float64` / `double` | 64-bit | `double` | `number` is an alias for `float64` / `double`. Use `float32` for interop with C APIs or GPU buffers that require single precision. ### 2.4 Byte arrays (`uint8[]`) and the zero-copy bridge `uint8[]` is a dynamic array of unsigned bytes — the standard type for binary data: file I/O, network buffers, cryptography, binary protocols. In the C backend, `uint8[]` shares an identical in-memory layout with `string`. Zero-copy conversion is provided in both directions: ```ms const buf: uint8[] = [0x48, 0x65, 0x6C, 0x6C, 0x6F]; const text = buf.asString(); // zero-copy → "Hello" (valid UTF-8) const data: uint8[] = readFile("config.json"); const json = data.asString(); // zero-copy for text parsing const firstByte: uint8 = buf[0]; // 0x48 const bytes: uint8[] = "hello".asBytes(); // reverse direction, zero-copy ``` `.asString()` and `.asBytes()` reinterpret the buffer header without copying — text and binary processing are interchangeable in hot paths. ### 2.5 Literal forms ```ms 42 // integer 3.14 // float 1_000_000 // underscore separator 0xFF // hex 0b1010 // binary 0o777 // octal 1e10 // exponent 1.5e-3 // float with exponent 123n // BigInt (n suffix, integers only) "hello" // double-quoted string 'world' // single-quoted string 'a' // char literal (single char, single quotes) "a" // string of length 1 (double quotes) "line\nnext" // escape: \n \t \r \\ \" \' "a".code // → 97 — compile-time fold of single-char literal "\n".code // → 10 (works with escapes) `hello ${name}` // template literal `${a} + ${b} = ${a + b}` // multiple substitutions `if` // backtick-escaped reserved word → identifier `my-var` // backtick-escaped invalid chars → identifier ``` ## 3. Operators ### 3.1 Arithmetic / assignment / comparison / logical / bitwise / update Standard TypeScript semantics apply to all of the following: | Category | Operators | |-------------|------------------------------------------| | Arithmetic | `+` `-` `*` `/` `%` `**` | | Assignment | `=` `+=` `-=` `*=` `/=` `%=` | | Comparison | `==` `===` `!=` `!==` `<` `<=` `>` `>=` | | Logical | `&&` `\|\|` `!` | | Bitwise | `&` `\|` `^` `~` `<<` `>>` `>>>` | | Update | `++` `--` | Both loose (`==`) and strict (`===`) equality exist; `===` is preferred except when TypeScript-compatible loose comparison is intended. ### 3.2 Special operators | Operator | Description | |------------|-------------| | `?:` | Ternary | | `??` | Nullish coalescing | | `?.` | Optional chaining | | `.` | Member access | | `..` | Exclusive range (exclusive end) | | `...` | Inclusive range / spread / rest | | `=>` | Arrow function | | `\|>` | Pipeline | | `sizeof` | Size of a type in bytes | **Pipeline (`|>`).** Low-precedence left-to-right composition: ```ms const result = input |> parse |> validate |> transform; // equivalent to: transform(validate(parse(input))) ``` ### 3.3 Punctuation `(` `)` `{` `}` `[` `]` `;` `:` `,` `@` (decorator prefix). ## 4. Declarations ### 4.1 Variables ```ms const x = 42; const x: number = 42; let y = "hello"; let y: string = "hello"; var z = true; // legacy function-scope ``` ### 4.2 Functions ```ms function add(a: number, b: number): number { return a + b; } // Arrow const mul = (a: number, b: number): number => a * b; const greet = (name: string): void => { console.log("hi " + name); }; // Async async function fetchJson(url: string): Promise { /* ... */ } // Generator function* range(n: number): Generator { /* ... */ } // Default + rest parameters function connect(host: string, port: number = 443, ...flags: string[]): void {} // Destructuring parameters function point({ x, y }: { x: number, y: number }): number { return x + y; } // Overload signatures — bodyless declarations followed by one implementation function parse(s: string): number; function parse(s: string, base: number): number; function parse(s: string, base?: number): number { return base === undefined ? parseInt(s, 10) : parseInt(s, base); } ``` ### 4.3 Extension methods Attach methods to any type using a `this`-typed first parameter. ```ms // Instance extension — adds a method to an existing type via `this` receiver function trim(this s: string): string { /* ... */ } "hello ".trim(); // → trim("hello ") // Generic instance extension function push(this arr: T[], elem: T): void { /* ... */ } names.push("alice"); // T inferred from receiver // Static extension — namespace method via `this typeof` function floor(this typeof Math, x: number): number { /* ... */ } Math.floor(3.7); // → floor(3.7); receiver stripped ``` Instance extensions prepend the receiver as the first argument at the call site. Static extensions strip the receiver entirely. Resolution happens at the call site; generated code is a direct function call — no v-table, no dispatch. ### 4.4 Classes ```ms class Point { x: number; y: number; constructor(x: number, y: number) { this.x = x; this.y = y; } distance(): number { return Math.sqrt(this.x * this.x + this.y * this.y); } } // Inheritance class Point3D extends Point { z: number; constructor(x: number, y: number, z: number) { super(x, y); this.z = z; } } // Access modifiers + static + readonly class Service { private key: string; protected data: number; public name: string; readonly id: number; static count: number; } ``` Classes are reference types — heap-allocated with DRC, same as interfaces (§4.5). Use `class` when you need methods with inheritance; use `struct` when you need a value type (§4.6). ### 4.5 Interfaces (reference types) Interfaces are **heap-allocated, reference-counted** — they are concrete runtime shapes, not merely structural types as in TypeScript. An object literal that satisfies an interface constructs a reference. ```ms interface User { id: string; name: string; joinedAt: number; email?: string; // optional } function createUser(id: string, name: string): User { return { id, name, joinedAt: Date.now() }; // heap-allocated } const u1 = createUser("u_1", "Alice"); const u2 = u1; // same reference; refcount incremented ``` - Fields are mutable unless declared `readonly`. - Interfaces may declare method signatures; implementations are attached via extension methods (§4.3). - Interfaces can extend one or more other interfaces. - In the C backend, interfaces emit as C structs, passed by pointer (`T*`), with refcounted deterministic destruction. ### 4.6 Structs (value types) `struct` declares a **value type** — stack-allocated by default, copied on assignment. Structs are the main semantic addition to TypeScript's type surface, and are an opt-in performance optimization for hot paths. ```ms struct Vec2 { x: float64; y: float64; } struct Color { r: uint8; g: uint8; b: uint8; a: uint8; } const p: Vec2 = { x: 1.0, y: 2.0 }; // stack-allocated const q = p; // byte-for-byte copy // Struct intersection with data-only interfaces interface IUser { name: string; age: number; } struct SuperUser = IUser & { role: string; }; ``` Structs are data-only — they cannot declare methods directly. For method- like functions, use extension methods (§4.3) with `this self: T` receivers. #### 4.6.1 Parameter passing — auto-ABI selection Struct parameters follow **TypeScript-compatible mutation semantics**: a mutation to a struct parameter propagates to the caller, exactly as mutation does on a plain object in TypeScript. Under the hood, the compiler picks the fastest C ABI per parameter based on size and mutation analysis: ```ms struct Vec2 { x: float64; y: float64; } struct BigData { name: string; items: number[100]; } // Not mutated → value (small) or const T* (big) — zero copy function length(v: Vec2): float64 { return Math.sqrt(v.x * v.x + v.y * v.y); } // Mutated → T* — mutation propagates to caller function reset(v: Vec2): void { v.x = 0; // caller's v.x becomes 0 v.y = 0; } // readonly → explicit copy; caller's value never affected function tryParse(readonly data: BigData): boolean { data.name = "test"; // mutates local copy only return validate(data); } ``` | Size | Mutated? | `readonly`? | Emitted C signature | |---------|----------|-------------|----------------------| | ≤ 24 B | no | no | `void f(Vec2 v)` — value in registers | | ≤ 24 B | yes | no | `void f(Vec2* v)` — pointer; mutation propagates | | ≤ 24 B | — | yes | `void f(Vec2 v)` — forced copy | | > 24 B | no | no | `void f(const BigData* v)` — zero-copy read | | > 24 B | yes | no | `void f(BigData* v)` — pointer; mutation propagates | | > 24 B | — | yes | Copy-on-entry — explicit caller isolation | The developer writes normal code; the compiler picks the fastest path. #### 4.6.2 Parameter modifiers | Modifier | Syntax | Purpose | |-------------|-----------------------------|---------| | *(default)* | `f(v: Struct)` | Auto-optimized: compiler picks best ABI | | `readonly` | `f(readonly v: Struct)` | Explicit copy — caller's value unaffected | | `move` | `f(move v: Struct)` | Ownership transfer — caller's value zeroed | | `out` | `f(out v: Struct)` | Output parameter — callee fills the value | #### 4.6.3 Struct vs interface vs class | Construct | Value / Ref | Methods | Allocation | Use case | |-------------|-------------|-------------------------------|--------------|----------| | `interface` | Reference | Signatures (impls via ext) | Heap (DRC) | Data + behavior, TS compatibility | | `struct` | Value | No | Stack | Hot paths, math types, small records, C interop | | `class` | Reference | Yes | Heap (DRC) | OOP, inheritance, polymorphism | **Workflow.** Start with `interface` (familiar TypeScript). Profile. Promote hot paths to `struct` for value semantics. Need methods on a value type? Use extension methods (§4.3). ### 4.7 Enums and enum literal types ```ms enum Color { Red, Green, Blue } enum Status { Active = 1, Inactive = 0, } // Enum literal types — a specific member as a type enum K { A, B, C } function handleA(k: K.A): void {} handleA(K.A); // OK // handleA(K.B); // error: got K.B, expected K.A const k: K.A = K.A; // literal type K.A const k2 = K.A; // widened to K (no annotation) ``` ### 4.8 Type aliases ```ms type ID = number; type StringOrNumber = string | number; type Callback = (data: string) => void; // Generic aliases type Box = { value: T }; type Pair = { first: A, second: B }; const b: Box = { value: 42 }; const p: Pair = { first: "hi", second: 1 }; // const bad: Box = { value: "wrong" }; // error ``` ### 4.9 Import / export ```ms import { Token, formatToken } from "./lexer/token"; import Parser from "./parser"; import * as utils from "./utils"; import type { Config } from "./config"; export function helper(): void {} export interface Config { debug: boolean; } export struct Vec2 { x: float64; y: float64; } export type ID = number; export default class App {} export { Token } from "./lexer/token"; // re-export export * from "./utils"; // re-export all ``` ## 5. Type system ### 5.1 Annotations ```ms const x: number = 42; function f(a: string, b: number): boolean { /* ... */ } const arr: number[] = [1, 2, 3]; const tuple: [string, number] = ["hello", 42]; const map: Map = new Map(); const set: Set = new Set(); ``` ### 5.2 Generics ```ms function identity(x: T): T { return x; } class Container { value: T; } interface Comparable { compareTo(other: T): number; } // Constraints function longest(a: T, b: T): T { /* ... */ } // Default type parameters type Result = { ok: true; value: T } | { ok: false; error: E }; // Const generics — compile-time values as type parameters class Matrix { getTotalElements(): int32 { return ROWS * COLS; } } ``` ### 5.3 Union and intersection types ```ms type StringOrNumber = string | number; type Shape = Circle & Drawable; // Intersection — combine multiple types type Extended = IUser & { role: string }; // Struct intersection — compose value types from data-only interfaces struct SuperUser = IUser & { role: string; }; ``` ### 5.4 Discriminated unions MetaScript supports two flavors. **Undiscriminated (TypeScript-style).** A union of object literals, matched by field presence. Use this when variant fields don't collide. ```ms type Shape = | { kind: "circle", r: float64 } | { kind: "square", side: float64 } | { kind: "rect", w: float64, h: float64 }; function area(s: Shape): float64 { return match (s) { { kind: "circle", r } => 3.14159 * r * r, { kind: "square", side } => side * side, { kind: "rect", w, h } => w * h, }; } ``` **Discriminated (MetaScript-specific).** Uses `match` in **type position**, keyed by an enum. Each variant binds its own field set. Construction is validated at compile time — you cannot omit the discriminant or mix fields from different variants. ```ms enum NodeKind { NumLit, StrLit, BinExpr } type NodeData = match (kind: NodeKind) { NodeKind.NumLit => { value: number }, NodeKind.StrLit => { value: string }, NodeKind.BinExpr => { op: string, left: Node, right: Node }, }; function makeNum(n: number): NodeData { return { kind: NodeKind.NumLit, value: n }; } function makeBin(op: string, l: Node, r: Node): NodeData { return { kind: NodeKind.BinExpr, op, left: l, right: r }; } // Compile errors: // { kind: NodeKind.BinExpr, value: 42 } // `value` not in BinExpr // { op: "+", left: l, right: r } // missing discriminant ``` Field access is narrowed per variant. Fields common to multiple variants (same name, same type) resolve unambiguously; variant-specific fields are narrowed by the discriminant. ```ms function getKind(d: NodeData): NodeKind { return d.kind; } // always available function getOp(d: NodeData): string { return match (d) { { kind: NodeKind.BinExpr, op } => op, _ => "", }; } ``` On the C target, discriminated unions compile to a tagged union whose tag is the enum type itself (not `int32_t`): ```c typedef struct NodeData { NodeKind _tag; union { struct { double value; } _v0; // NumLit struct { msString value; } _v1; // StrLit struct { msString op; Node left; Node right; } _v2; // BinExpr }; } NodeData; ``` **When to use which.** | Situation | Choice | |---------------------------------------------------|--------| | Variants have unique field names | Undiscriminated (TS-style) | | Variants share field names with different types | Discriminated (enum-keyed) | | You want compile-time construction validation | Discriminated | | You want the C tag to be a typed enum, not int | Discriminated | ### 5.5 Discriminant narrowing Narrowing applies inside `if` and `match` based on discriminant checks. ```ms type Shape = | { kind: "circle", radius: number } | { kind: "square", side: number }; function area(s: Shape): number { if (s.kind === "circle") { return s.radius * s.radius * 3; // narrowed to circle } return s.side * s.side; // narrowed to square } function describe(s: Shape): string { return match (s.kind) { "circle" => "r=" + s.radius.toString(), "square" => "s=" + s.side.toString(), }; } ``` ### 5.6 Function overload signatures Body-less overload declarations followed by a single implementation (TypeScript parity). ```ms function f(k: K.A): string; // overload 1 function f(k: K.B): string; // overload 2 function f(k: K): string { // implementation (must come last) return "result"; } ``` Rules: - Implementation must come last. - All signatures must have the same arity as the implementation. - Each signature's parameter types must be assignable to the implementation's corresponding parameters; its return type assignable to the impl return. **Known limitation.** Overload resolution scores arguments without per- candidate contextual re-checking, so object-literal arguments (`{ ... }`) cannot discriminate between candidates whose parameter types are named structs. Until per-candidate speculative checking lands, prefer a single wide signature with a union parameter type for APIs that take object literals. ### 5.7 Conditional types and `infer` ```ms // Basic: T extends U ? X : Y type IsNumber = T extends number ? "yes" : "no"; // Nested type Classify = T extends string ? "text" : T extends number ? "num" : "other"; // infer — extract type from a pattern (valid only inside extends clause) type Unwrap = T extends Array ? U : T; const x: Unwrap = 42; // U inferred as number const y: Unwrap = "hi"; // no match → T type Box = { value: T }; type Unbox = T extends Box ? U : never; const v: Unbox> = "hello"; // U inferred as string ``` ### 5.8 Mapped and utility types ```ms // Mapped type MyPartial = { [K in keyof T]?: T[K] }; type Nullable = { [K in keyof T]: T[K] | null }; // Utility (library-defined, matching TS semantics) Partial // all properties optional Required // all properties required Readonly // all properties readonly Record // object with keys K and values V Pick // subset of properties Omit // exclude properties ``` ### 5.9 Type assertions ```ms const data = expr as { value: number }; // narrowing const len = (x as string).length; const v = value as unknown as TargetType; // double cast ``` ## 6. Control flow ```ms // if / else if (c) { /* ... */ } if (c) { /* ... */ } else { /* ... */ } if (a) { /* ... */ } else if (b) { /* ... */ } else { /* ... */ } // while while (c) { /* ... */ } // C-style for for (let i = 0; i < n; i += 1) { /* ... */ } for (let i = 0; i < n; i++) { /* ... */ } // for-of for (const item of items) { /* ... */ } for (const [k, v] of map) { /* ... */ } // switch (keep for break-through fall-through; prefer match otherwise) switch (expr) { case value1: /* ... */; break; case value2: /* ... */; break; default: /* ... */; } // return / break / continue / throw return; return expr; break; continue; throw new Error("message"); // try / catch / finally try { riskyOperation(); } catch (e) { handleError(e); } finally { cleanup(); } ``` ## 7. MetaScript-unique semantics ### 7.1 `match` expressions Pattern matching with destructuring, or-patterns, guards, and exhaustiveness checking. `match` replaces most uses of `switch`. ```ms // Expression form — simple arms (implicit return) return match (escChar) { "n" => "\n", "t" => "\t", _ => escChar, }; // Expression form — block arms require explicit return return match (node.kind) { NodeKind.Identifier => getName(node), NodeKind.BinaryExpr => { const d = node.data as BinaryExprData; return d.left.toString() + d.op + d.right.toString(); }, _ => "unknown", }; // Statement form — side effects only match (ch) { "(".code => { advance(s); addToken(s, LParen); }, ")".code => { advance(s); addToken(s, RParen); }, _ => { /* nothing */ }, } // Destructuring match (result) { { ok: true, value: v } => process(v), { ok: false, error: e } => handleError(e), } // Or-patterns match (token.kind) { TokenKind.Plus | TokenKind.Minus => parseAddSub(), TokenKind.Star | TokenKind.Slash => parseMulDiv(), _ => defaultCase(), } // Guards — `when` condition; parentheses optional match (token.kind) { TokenKind.Ident when isKeyword(token.value) => handleKeyword(token), TokenKind.Ident when isBuiltin(token.value) => handleBuiltin(token), TokenKind.Ident => handleIdentifier(token), _ => handleOther(token), } // Binding guards match (score) { x when x >= 90 => "A", x when x >= 80 => "B", x when x >= 70 => "C", _ => "F", } // Wildcard guard — conditional default match (mode) { _ when strictMode => { unreachable; }, _ => handleFallback(), } ``` **Rules.** - *Arm form.* `pattern => expr` is an implicit return. `pattern => { stmt1; return expr; }` is a block arm that requires an explicit `return`. - *Wildcards.* `_` matches anything. - *Or-patterns.* `A | B | C => ...`. - *Guards.* `when (cond)` — parentheses are optional. Evaluated only when the pattern matches. First matching guard wins. - *Exhaustiveness.* The checker errors if a variant is missing, unless a wildcard arm (`_ => ...`) is present. - *Bare identifiers.* In patterns, a bare identifier is a **binding**, not a value comparison. Use a string / number literal or an enum member for value matching. - *Inside match arms,* prefer `for..of` or `while`. The C-style `for` loop is not normalized inside match-lowered blocks (parser-level restriction, not a semantic one). ### 7.2 `Result` and the `try` operator `Result` is the idiomatic error type. `try` unwraps a `Result`, early-returning the error variant on failure. ```ms type ParseResult = Result; function parseInt(s: string): ParseResult { if (s.length === 0) return Result.err("empty string"); // ... parsing ... return Result.ok(value); } function sumInputs(a: string, b: string): Result { const x = try parseInt(a); // early-returns on err const y = try parseInt(b); return Result.ok(x + y); } // try with catch — expression-form fallback const n = try parseInt(input) catch 0; // Manual branching when both paths are needed const r = parseInt(input); if (!r.ok) { handleError(r.error); return; } const value = r.value; ``` **Fields and constructors.** - `.ok` — boolean success flag - `.value` — `T` (the success value; valid when `ok === true`) - `.error` — `E` (the error value; valid when `ok === false`) - `Result.ok(v)` — construct the success variant - `Result.err(e)` — construct the failure variant **`try` rules.** - `try expr` requires the enclosing function to return a `Result` with a compatible error type. - `try expr catch fallback` is expression form: evaluates to `fallback` if `expr` errors. No enclosing-function constraint. - `try` inside a `match` arm is not supported — use an outer `if` to branch on `r.ok`. ### 7.3 `defer` — scope-exit cleanup `defer` schedules a statement to run when the enclosing scope exits, in LIFO order. It runs on **any exit**: normal return, `throw`, panic, or early-return via `try`. ```ms function readConfig(): Result { const f = try openFile("config.toml"); defer close(f); // always runs, any exit path const raw = try f.readAll(); return Result.ok(try parseToml(raw)); } // Multiple defers — reverse declaration order at scope exit defer console.log("second"); defer console.log("first"); // runs first // Resource cleanup function process(): void { const buf = allocate(1024); defer free(buf); const lock = acquire(mutex); defer release(lock); // work ... // scope exit: release(lock); free(buf); } ``` ### 7.4 `move` — ownership transfer Most MetaScript code never explicitly uses `move`. The compiler's DRC analyzer detects the **last read** of a variable and converts it to a sink (move) automatically — identical runtime effect, no syntactic ceremony. Explicit `move` is required in a small number of places: 1. **Sending values into actors.** `move` transfers ownership across the isolation boundary without a deep copy (§8.7). 2. **Explicit ownership transfer** — when the developer wants to force a sink and have the compiler reject any subsequent use. ```ms const y = move x; // y owns the data; x is zeroed return move data; // caller takes ownership; local zeroed consume(move buffer); // callee takes ownership ``` With `move`: forces the sink path, source is always zeroed; compile error on reuse. Without `move`: the analyzer chooses sink vs copy via last-read analysis. If `move x` is used but `x` is read again later (i.e., the move is not actually a last-use), the compiler emits `errFailedMove`. ### 7.5 `undefined` and the null idiom MetaScript accepts `undefined` as a type and value — it is an alias for `null`. The runtime has one null-ish value. Idiomatic MetaScript prefers `null` in source, matches on `null` explicitly, and uses the `null as unknown as T` idiom for non-optional nullable fields on reference types: ```ms interface Scope { symbols: Symbol[]; parent: Scope; // non-optional in the type } const root: Scope = { symbols: [], parent: null as unknown as Scope }; if (s.parent !== null) { // safe to use s.parent here } ``` Optional nullable fields are also supported (type becomes `T | undefined`, which is the same as `T | null` at runtime): ```ms interface User { id: string; name: string; email?: string; // optional — `string | undefined` } ``` ## 8. Async & concurrency Every concurrency primitive in MetaScript returns the same surface type, `Promise`. `async` functions, `spawn()`, and actor calls all produce `Promise`. The compiler tracks *origin* (affine for `spawn`, not for `async`/actor) and *enclosing context* internally to pick the right lowering. You `await` a `Promise` without worrying which flavor produced it. ### 8.1 `Promise` and `async` / `await` ```ms async function fetchUser(id: number): Promise { const data = await httpGet(`/users/${id}`); return data; } const user = await fetchUser(42); ``` `async` functions return `Promise`. The compiler desugars `await` into a state machine — each `await` splits the function body into states; callbacks resume execution when the awaited promise settles. In the C backend, `Promise` maps to `msFuture*` — a callback-driven future with deterministic refcounting. In the JS backend, it maps to native `Promise`. ### 8.2 Promise static API ```ms // Combinators Promise.all([p1, p2, p3]); // resolves when ALL resolve; rejects on first Promise.race([p1, p2, p3]); // settles on first (fulfill or reject) Promise.allSettled([p1, p2, p3]); // resolves with per-future outcomes Promise.any([p1, p2, p3]); // resolves on first fulfillment // Pre-settled Promise.resolve(v); Promise.reject("error"); // new Promise(executor) — executor runs synchronously const p = new Promise((resolve, reject) => { if (success) resolve(data); else reject("failed"); }); // Promise.withResolvers (ES2024) — deconstructed resolve/reject const { promise, resolve, reject } = Promise.withResolvers(); setTimeout(() => resolve(42), 100); const n = await promise; ``` | Combinator | Resolves when | Rejects when | Empty array | |--------------------|------------------|-------------------------|-----------------------| | `Promise.all` | ALL resolve | FIRST rejects | Resolves immediately | | `Promise.race` | FIRST settles | FIRST settles (reject) | Never settles | | `Promise.allSettled` | ALL settle | Never | Resolves immediately | | `Promise.any` | FIRST fulfills | ALL reject | Rejects immediately | ### 8.3 Chaining: `.then` / `.catch` / `.finally` ```ms fetchUser(42) .then((user) => { console.log(user); }) .catch((err) => { console.log("failed: " + err); }) .finally(() => { cleanup(); }); ``` | Method | Callback | Returns | Behavior | |----------------|-------------------------------|----------------|----------| | `.then(fn)` | `(value: T) => void` | `Promise`| Called on fulfillment; rejection propagates | | `.catch(fn)` | `(error: string) => void` | `Promise` | Called on rejection; fulfillment passes through | | `.finally(fn)` | `() => void` | `Promise` | Always called; original outcome preserved | Each callback returns a new `Promise`, enabling chaining. Exceptions thrown inside a callback are captured and propagate as rejections on the returned promise. ### 8.4 `Promise>` — typed async errors ★ `Promise>` is MetaScript's recommended pattern for async error handling. It combines: - **`Promise`** for async execution (suspend / resume). - **`Result`** for typed errors (no exceptions). **Key guarantee.** A `Promise>` **never rejects**. Errors are always typed `Result` values inside a successfully-resolved promise. The compiler enforces this. ```ms async function fetchUser(id: number): Promise> { const resp = await httpGet(`/users/${id}`); if (resp.status !== 200) return Result.err("not found"); return Result.ok(parseUser(resp.body)); } ``` #### 8.4.1 `try await` — composed unwrapping `await` unwraps `Promise` → `T`. `try` unwraps `Result` → `T`. Together, `try await` unwraps both layers in one expression: ```ms const user = try await fetchUser(42); // Desugars to: const $tmp = await fetchUser(42); // Promise → Result if (!$tmp.ok) return Result.err($tmp.error); // propagate error const user = $tmp.value; // Result → User ``` #### 8.4.2 `try await ... catch` fallback When you want a fallback value instead of propagating the error: ```ms const user = try await fetchUser(42) catch defaultUser; // Equivalent to: const $tmp = await fetchUser(42); const user = $tmp.ok ? $tmp.value : defaultUser; ``` #### 8.4.3 All unwrapping combinations | Expression | Input | Output | On error | |-----------------------------|----------------------------|---------|------------------------------------| | `await p` | `Promise` | `T` | Rejection → exception propagates | | `try expr` | `Result` | `T` | Early-returns `Result.err(e)` | | `try expr catch fallback` | `Result` | `T` | Uses fallback | | `try await p` | `Promise>` | `T` | Early-returns `Result.err(e)` | | `try await p catch fallback`| `Promise>` | `T` | Uses fallback | #### 8.4.4 Compiler safety: the no-rejection guarantee Inside an `async function` returning `Promise>`, the compiler enforces two rules. **V1 — Throw ban.** `throw` statements are forbidden. Use `Result.err()` instead. ```ms async function bad(): Promise> { throw new Error("boom"); // COMPILE ERROR // Fix: return Result.err("boom"); } ``` **V2 — Unguarded-await ban.** `await` on a plain `Promise` (non- `Result`) must be wrapped with `try ... catch` (or `try await ... catch`) so rejection is converted to `Result.err`. ```ms async function example(): Promise> { // ERROR: bare await on Promise — could reject const data = await riskyCall(); // OK — rejection converted to Result.err via catch fallback const data = try await riskyCall() catch "fallback"; // OK — await on Promise>; already typed const user = await fetchUser(42); // OK — inside try/catch statement try { const raw = await riskyCall(); } catch (e) { return Result.err("wrapped: " + e); } return Result.ok(42); } ``` | Awaited type | Guard required? | Reason | |---------------------------|-----------------------------------------|--------| | `Promise>` | No | Already typed errors — `Result` handles failure | | `Promise` (non-Result) | Yes — `try await ... catch` or `try/catch` | Rejection could break the no-rejection contract | #### 8.4.5 Comparison: `Promise` vs `Promise>` | Aspect | `Promise` | `Promise>` | |------------------|---------------------------|--------------------------------| | Error signaling | Rejection (untyped) | `Result.err(e)` (typed) | | Needs try/catch? | Yes | No — use `try await` | | Error type known?| No (`unknown`) | Yes (`E`) | | Can reject? | Yes | No (compiler-enforced) | | Recommended for | Fire-and-forget, side effects | All fallible async operations | **Real-world example.** ```ms // Service layer — all errors typed async function createOrder(req: OrderRequest): Promise> { const user = try await fetchUser(req.userId); const inventory = try await checkStock(req.items); if (inventory.available < req.quantity) { return Result.err(OrderError.OutOfStock); } const order = try await saveOrder(user, req); return Result.ok(order); } // Caller — clean linear flow, no try/catch blocks async function handleRequest(): Promise> { const order = try await createOrder(request); return Result.ok({ status: 200, body: order }); } ``` ### 8.5 `AbortController` / `AbortSignal` ECMAScript-compatible cooperative cancellation, usable with `spawn`, `async`, and all Promise combinators. ```ms const controller = new AbortController(); const signal = controller.signal; if (signal.aborted) { /* cancelled */ } controller.abort("timeout"); signal.throwIfAborted(); // throws AbortError if aborted const preAborted = AbortSignal.abort("already done"); ``` ### 8.6 `spawn()` — task parallelism ★ `spawn(() => work())` schedules work on the runtime scheduler and returns a `Promise` tagged — under the hood — as **affine + scope-bound**. Surface type is the same `Promise`; affine semantics are carried by internal flag bits so users don't need to learn a second type. ```ms async function fetchAll(urls: string[]): Promise { const tasks = urls.map((u) => spawn(() => httpGet(u))); const results: Response[] = []; for (const t of tasks) { results.push(await t); } return results; } // With Promise.all — automatically lowered to a zero-alloc AwaitGroup // when all elements are spawn handles const [a, b, c] = await Promise.all([ spawn(() => processChunk1(data)), spawn(() => processChunk2(data)), spawn(() => processChunk3(data)), ]); ``` #### 8.6.1 Capture rules - **`const` captures** — borrowed (read-only reference into outer scope). No copy. - **`move` captures** — ownership transfers into the spawned task. - **`let` (mutable) captures** — rejected by the compiler. They would introduce a data race. ```ms const data = loadHugeDataset(); // 100 MB const [a, b] = await Promise.all([ spawn(() => processPartA(data)), // borrows data spawn(() => processPartB(data)), // borrows data ]); // await ensures data lives until both tasks complete. No copying. const owned = makeBuffer(); const t = spawn(() => consume(move owned)); // ownership transferred // owned is zeroed here ``` #### 8.6.2 PARALOCK safety rules The compiler enforces these rules on spawn-origin promises: | Rule | Error | Description | |----------|---------------------|-------------| | R1 / E2 | unconsumed handle | Every spawn-origin `Promise` must be awaited before its scope exits | | R2 / E3 | double consume | Spawn-origin `Promise` cannot be awaited twice | | R3 / E4 | handle escape | Spawn-origin `Promise` cannot be returned from a function | | E40 | await-in-loop | `await spawn(...)` inside a loop body is a warning (sequential anti-pattern) | **Implicit upcast.** When a spawn-origin `Promise` flows into a context that can't preserve the flags (mixed awaitable array, generic `Promise` parameter, explicit upcast), the flags are silently stripped and the value becomes an ordinary `Promise`. The compiler emits a lint explaining the safety loss so it is never hidden. #### 8.6.3 Structured concurrency `spawn()` uses structured concurrency: the enclosing scope waits for all spawned tasks to complete (or be cancelled) before exiting. A task cannot outlive the scope that created it. ```ms async function work(): Promise { const t1 = spawn(() => computeA()); const t2 = spawn(() => computeB()); // scope exit: both t1 and t2 must be awaited first return (await t1) + (await t2); } ``` #### 8.6.4 Cancellation and timeouts Spawned work is cancelled cooperatively via `AbortController` / `AbortSignal` (§8.5) — the spawn closure observes the signal at suspension points and returns early when aborted. ```ms const controller = new AbortController(); const signal = controller.signal; const task = spawn(() => { while (!signal.aborted) { doWork(); } }); // ...later controller.abort("done"); await task; // resolves when the closure returns ``` There is no `.cancel()` method on the spawn handle itself and no built-in `{ timeout }` option on `spawn` — compose `AbortController` with a timer (or a racing promise) to build timeouts. #### 8.6.5 Backpressure The runtime uses a fixed worker pool sized to the CPU count, with a circular queue (256 slots). When the queue is full and all workers are busy, new `spawn()` calls execute **inline on the caller** — ensuring forward progress without unbounded memory growth. #### 8.6.6 Spawn inside actors Actors may `spawn` internally. Safety rules prevent races (PARALOCK S1 / S3): ```ms actor Worker { private data: number[] = []; compute(): number { // OK — read-only field borrow in spawn thunk const h = spawn(() => { let sum = 0; for (const v of this.data) sum = sum + v; return sum; }); return await h; } } actor Unsafe { private state: number = 0; bad(): void { spawn(() => { this.state = 42; // COMPILE ERROR (S3): cannot mutate actor field }); spawn(() => { const ref = this; // COMPILE ERROR (S1): cannot capture bare `this` }); } } ``` ### 8.7 `actor {}` — stateful parallelism ★★ Actors are long-lived stateful objects that communicate by message passing. Each actor has its own mailbox and processes one message at a time — no internal locking needed. Actors are MetaScript's killer primitive for stateful concurrency: connection pools, rate limiters, chat rooms, game entities, caches, session managers. **And they're cheap.** On the native C target, actors are **stackless fibers on a work-stealing scheduler** — not OS threads, not green threads with full stacks. A ping-pong benchmark sustains **~50 million concurrent actors on a 2024 MacBook M4**, shipped as a plain native binary (no JVM, no BEAM, no bundled runtime). Memory per idle actor is measured in tens of bytes, not megabytes; spawn / dispatch cost is dominated by atomic refcount updates, not context switches. The practical consequence: you don't have to ration actors. Common patterns that would blow out a Node.js or JVM process — one actor per open WebSocket, one actor per document in a realtime editor, one actor per player in a game server, one actor per in-flight LLM session, supervisor trees many layers deep — all fit comfortably on a single machine. Actor allocation is idiomatic, not a last-resort optimization. #### 8.7.1 Declaration ```ms actor Counter { private count: number = 0; // void return = SEND: enqueue and return immediately (fire-and-forget) increment(): void { this.count += 1; } // non-void return = CALL: returns Promise, resolved when processed get(): number { return this.count; } } const counter = new Counter(); counter.increment(); // SEND — returns immediately counter.increment(); counter.increment(); const n = await counter.get(); // CALL — Promise; 3 ``` **Method return type determines message kind.** - `void` return → **SEND** — enqueues, returns immediately. - Non-`void` return → **CALL** — returns `Promise`, resolves when the actor has processed the message. #### 8.7.2 Isolation An actor's mutable state is accessible **only from within the actor itself.** External access goes through the mailbox. The compiler enforces this: ```ms const counter = new Counter(); counter.count; // COMPILE ERROR: actor-isolated property await counter.get(); // OK: goes through mailbox ``` #### 8.7.3 `@nonisolated` and `pid` Immutable fields marked `@nonisolated` are readable from outside without going through the mailbox: ```ms actor Server { @nonisolated public readonly name: string; private connections: number = 0; constructor(name: string) { this.name = name; } getConnections(): number { return this.connections; } } const s = new Server("api-1"); s.name; // OK: nonisolated, immutable await s.getConnections(); // OK: through mailbox ``` Every actor carries a `pid: int64` field — its unique runtime identity, always nonisolated. Use `pid` for linking, monitoring, and logging. #### 8.7.4 The three transfer rules — COPY / MOVE / SHARE Data crossing an actor boundary follows one of three rules, selected by the compiler: ``` actor.method(data) | +-- Is `move` keyword present? | YES → Rule 2: MOVE — transfer pointer, invalidate source | NO ↓ +-- Is data provably immutable? | (value type, or Ref with all fields readonly) | YES → Rule 3: SHARE — pass pointer, incref, zero-copy | NO ↓ +-- Rule 1: COPY — value types copied automatically; mutable Ref rejected ``` **Rule 1 — COPY (default, always safe).** ```ms const data = loadData(); actor.ingest(data); // deep copy into mailbox console.log(data.length); // still valid — we kept our copy ``` **Rule 2 — MOVE (zero-copy, source invalidated).** ```ms const huge = loadMillionRecords(); actor.ingest(move huge); // zero-copy, ownership transferred console.log(huge.length); // COMPILE ERROR: used after move ``` **Rule 3 — SHARE (automatic for immutable data).** ```ms const config = { maxRetries: 3, timeout: 5000, endpoints: ["api1.example.com", "api2.example.com"], } as const; actorA.configure(config); // share reference (immutable) actorB.configure(config); // share reference (same pointer) config.maxRetries; // still valid — shared, refcounted ``` **Lineage.** | | Pony (6 capabilities) | MetaScript (3 rules) | |---|---|---| | Compile-time proof | Complete | Partial (COPY/MOVE proven; SHARE via immutability analysis) | | Runtime cost | Zero | Small (ARC incref / decref on shared immutable data) | | Developer experience | Steep (~months) | Simple (copy, move, or const) | #### 8.7.5 Sendable (compiler-inferred) The compiler automatically checks which values can cross actor boundaries. **Sendable (can cross):** - Primitives (`number`, `string`, `boolean`, `int8..int64`, `uint8..uint64`, etc.) - Immutable data (`const` bindings with all `const`/`readonly` fields) - Arrays / objects whose elements are Sendable - Actor references (`ActorRef` — a mailbox address) - Values explicitly `move`d **Not Sendable:** - Mutable objects (`let x = { count: 0 }; x.count = 1`) - Closures capturing mutable state - File handles, sockets, raw pointers The check is at compile time — no runtime tags. ```ms let mut x = 0; actor.send(() => x); // ERROR: mutable variable not Sendable actor.send(() => x + 1); // ERROR: captures mutable x const y = 42; actor.send(() => y + 1); // OK: const capture is Sendable ``` #### 8.7.6 Serial execution An actor processes one message at a time. No internal locking needed. ``` Actor mailbox: [msg1] → [msg2] → [msg3] → ... ↑ process one, then next ``` **Cooperative suspension.** `async` actor methods (CALL pattern) suspend the actor while awaiting an internal future, freeing the scheduler to process other actors' mailboxes. #### 8.7.7 Supervision (OTP-style) Supervisors monitor child actors and restart them on failure. ```ms import { Supervisor, RestartStrategy, RestartType, ShutdownKind, } from "std/actor/supervisor"; // Positional constructor: strategy, maxRestarts, maxSeconds const sup = new Supervisor(RestartStrategy.OneForOne, 3, 5); sup.addChild({ name: "database", start: () => new DatabaseActor(connectionString), restart: RestartType.Permanent, shutdown: ShutdownKind.Timeout, shutdownMs: 5000, }); sup.addChild({ name: "cache", start: () => new CacheWorker(), restart: RestartType.Transient, }); await sup.start(); ``` **Restart strategies.** | Strategy | Behavior | |--------------|----------| | `OneForOne` | Only the crashed child restarts | | `OneForAll` | All children restart when one crashes | | `RestForOne` | The crashed child plus all children started after it restart | **Restart types.** | Type | Behavior | |-------------|----------| | `Permanent` | Always restart on crash | | `Transient` | Restart only on abnormal exit | **Shutdown kinds.** | Kind | Behavior | |---------------|----------| | `BrutalKill` | Terminate immediately | | `Timeout` | Graceful request, then force-kill after `shutdownMs` | | `Infinity` | Wait forever (for supervisors of critical children) | Recovery is **start fresh** — a new actor is constructed via its `start` function, not resumed from the crashed state. This avoids operating on corrupted state. **Dynamic child management.** ```ms sup.startChild({ name: "worker-1", start: () => new Worker(), restart: RestartType.Permanent }); sup.terminateChild("worker-1"); sup.restartChild("worker-1"); sup.deleteChild("worker-1"); const n = sup.countChildren(); ``` **`DynSupervisor`** is a pool variant where all children share the same spec and can be added / removed dynamically: ```ms const pool = new DynSupervisor({ strategy: RestartStrategy.OneForOne, childSpec: { start: () => new Worker(), restart: RestartType.Transient }, }); pool.add(); pool.add(); pool.remove(workerPid); ``` #### 8.7.8 Links, monitors, and `onExit` ```ms import { link, unlink, monitor, demonitor } from "std/actor"; // Link — bidirectional: if either dies, both die link(actorA.pid, actorB.pid); // Monitor — unidirectional: watcher receives DOWN message when target dies const ref = monitor(watcher.pid, target.pid); ``` Actors can handle exit signals via an `onExit` method: ```ms actor Watcher { onExit(childPid: int64, reason: ExitReason): void { console.log(`child ${childPid} died: ${reason}`); } } ``` ## 9. Memory model — deterministic reference counting (DRC) MetaScript uses DRC on the native C target. On the JavaScript target, the host GC handles memory. ### 9.1 How DRC works - Interface / class instances and heap objects carry a refcount. - Assignment increments; scope-end decrements; hitting zero destroys. - Destruction is deterministic and immediate when reachable by no live reference — destructors are guaranteed to run. - Cycles are detected and broken by a small built-in cycle collector (ORC) that runs incrementally; its worst-case work is bounded and can be scheduled per frame. - `defer` handles resource cleanup that doesn't fit object lifetime (closing a socket mid-function, releasing a lock). ### 9.2 Consequences - **Zero GC pauses** in normal operation. - **Predictable destruction order**, so RAII patterns (destructors that close files, flush buffers, unlock mutexes) are first-class. - **No write barriers, no tri-color marking, no heap scanning.** ### 9.3 GC modes Selected via `--gc=`: | Mode | Behavior | |-----------|----------| | `orc` | ARC + cycle collector (default) | | `drc` | ARC only (no cycle collector; for cycle-free workloads) | | `none` | No runtime memory management (freestanding / bare-metal) | | `manual` | Developer manages allocation explicitly (freestanding) | ## 10. Metaprogramming MetaScript's metaprogramming is **normal MetaScript code** that runs in the Raiser bytecode VM during compilation. There is no special macro API — the compiler's own typed AST (`Node` from `std/meta`) is the macro author's API. ### 10.1 `@comptime` — compile-time evaluation `@comptime { ... }` evaluates a block at compile time; the result replaces the block in the AST as a literal. Supports `number`, `string`, `boolean`, `null`, `array`, and `object` returns. ```ms const SIZE = @comptime { return 4 * 1024; }; // → 4096 const GREETING = @comptime { return "hello world"; }; const TABLE = @comptime { const items: number[] = []; let i = 0; while (i < 10) { items.push(i * i); i = i + 1; } return items; }; // → [0, 1, 4, 9, 16, 25, 36, 49, 64, 81] ``` `@comptime` blocks run in the compiler's sandbox (the Raiser VM) — standard MetaScript code, validated by the checker, then evaluated before codegen. Heap allocations inside `@comptime` are reclaimed at block end. ### 10.2 Macros — typed AST manipulation Macros receive `Node` values (AST), inspect and transform them, and return new `Node` trees. The returned AST replaces the original in the compilation pipeline. #### 10.2.1 `Node` is compile-time only `Node` (from `std/meta`) is a **compile-time-only type** — like standard-reference AST node implementations. `Node` values exist only during compilation and are erased before codegen. Any `Node` remaining in the AST at codegen is a compile error. ```ms import { Node, NodeKind, createNode } from "std/meta"; // All of these produce Node — all compile-time only: const el = hello; // JSX → Node const tmpl = quote { const x = 42; }; // quote → Node const node = createNode(NodeKind.BinaryExpr, /* ... */); // manual → Node // Macros consume Node and return runtime AST: const app = @jsx(el); // Node → runtime code const code = @inject(tmpl); // Node → runtime code // Same Node, different macros, different output: const webApp = @webJsx(el); // → DOM calls const nativeApp = @nativeJsx(el); // → UIKit-style calls // Direct application (most common): const app = @jsx hello; ``` **Invariant.** Zero `Node` at codegen. Everything else is free — store a Node in a `const`, pass it between macros, compose pipelines. #### 10.2.2 Macro invocation forms Macros can receive any `Node` type as their argument — blocks, functions, expressions, JSX. | Form | User writes | Macro receives | |-------------|------------------------------------------------|----------------| | Decorator | `@log class Foo {}` | `ClassDecl` node | | Block | `@routes { GET "/" -> home; }` | `BlockStmt` node | | Function | `@effect(() => { body })` | `ArrowFunction` node | | Expression | `@validate(x + y)` | `BinaryExpr` node | | JSX | `@jsx ...` | `JSXElement` node | | Const ref | `@jsx(el)` where `const el = ` | `JSXElement` (inlined) | The function form is particularly useful — the macro sees parameter context along with the body: ```ms @effect((count: number) => { console.log("Count changed: " + String(count)); document.title = String(count); }); // Macro sees: arrowParams = ["count"], arrowParamTypes = ["number"] // arrowBody = the two statements // Analyzes the body for reactive deps on `count`; wraps in createEffect. ``` #### 10.2.3 Macro pipelines A macro can return `Node` (still compile-time) rather than runtime code. Chaining macros builds pipelines: ```ms const raw = {count()}; // Node const validated = @addAria(raw); // Node const optimized = @staticAnalysis(validated); // Node const app = @render(optimized); // Runtime code ``` After each macro expansion, the output is re-walked for nested macros (depth-limited). #### 10.2.4 Expansion order and erasure Expansion processes declarations top-to-bottom. A `Node` const must be declared before it is referenced as a macro argument. After all macros expand, `Node`-only const declarations are erased from the AST. If any `Node` value survives to codegen, the compiler errors: *"compile-time Node not consumed by a macro."* ### 10.3 JSX — pure AST JSX in MetaScript is **pure AST** — the compiler produces `JSXElement` / `JSXText` / etc. nodes but does NOT transform them. The developer writes a macro that decides what JSX means. This is strictly more powerful than JavaScript, where the compiler hardcodes `React.createElement`. ```ms // Store as compile-time Node (reusable template) const ui = Count: {String(count())} ; // Multiple backends from one JSX tree: const web = @webJsx(ui); // DOM calls const native = @nativeJsx(ui); // UIKit / Android View ``` ### 10.4 `quote` expressions `quote { ... }` lifts a block of MetaScript syntax to a `Node` value, for programmatic AST construction: ```ms import { Node } from "std/meta"; const template: Node = quote { const x = 42; console.log(x); }; const expanded = @inject(template); ``` ## 11. Decorators reference Decorators annotate declarations for special compilation behavior. Some affect codegen, some control linking, some drive metaprogramming. ```ms @builtin("Echo") static extern log(value: string): void from "msPrintln"; // Put target-specific code inside @target("c") { ... }; the compiler // keeps only the block matching --target and drops the rest. // Experimental: parser ships, expansion still landing — prefer // backend-specific file extensions (.cms / .jms / .wms) for production. @target("c") { const osSpecificPath = "/tmp/foo"; } @target("js") { const osSpecificPath = (globalThis.process?.platform === "win32") ? "C:/tmp/foo" : "/tmp/foo"; } @compile("runtime/core/system.c") // compile this .c with the module @include("runtime/core/system.h") // #include this header @link("sqlite3") // pass -lsqlite3 to the linker @passC("-DFOO=1") // extra flags to the C compiler @passL("-framework Security") // extra linker flags ``` Full reference: | Decorator | Effect | |----------------------|--------| | `@builtin("name")` | Link to a runtime intrinsic by name | | `@target("c" \| "js")` | Block-form **backend**-conditional compilation — `@target("c") { ... }`. **Experimental**: parser ships, expansion landing; prefer `.cms` / `.jms` file extensions for production splits | | `@platform("macos" \| "linux" \| "windows")` | Block-form **OS**-conditional compilation — `@platform("macos") { @compile(...); @passL(...); }`. Inner directives apply only when the build's OS target matches the tag. Ships today (used by `@metascript/ion` to gate platform-native sources). | | `@include("foo.h")` | `#include` the header; auto-compile matching `.c` if present | | `@compile("foo.c")` | Compile a specific C source file with the module. **C++ also supported** — `@compile("foo.cpp")` (or `.cxx` / `.cc` / `.c++` / `.cp` / `.C` / `.CPP`) compiles with `zig c++ -std=c++17`; any C++ source in the build switches the final link step to the C++ driver so `libc++` / `libstdc++` is pulled automatically. Mix C and C++ freely. Cross-compile inherits — `--os=windows` with C++ sources produces a self-contained `.exe` (libc++ statically linked). | | `@link("lib")` | Pass `-llib` to the linker | | `@passC("flags")` | Extra flags to the C compiler | | `@passL("flags")` | Extra flags to the linker | | `@emit("raw-c-code")`| Inline raw C at the declaration site (escape hatch) | | `@derive(Eq, Hash)` | Auto-derive traits on structs / interfaces | | `@cName("c_name")` | Export with a stable C name (vs mangled) | | `@awaitable` | Mark a generic instance as awaitable | | `@affineAwaitable` | Mark as affine (consumed on await) | | `@nonisolated` | Mark an immutable actor field as readable without the mailbox | | `@comptime` | Mark a block as compile-time (see §10.1) | **`@target` blocks (experimental).** Put target-specific code inside `@target("c") { ... }`; the compiler keeps only the block matching the current `--target` and drops the rest. Blocks can chain with `else @target("js") { ... }`. **Status**: parser ships, expansion still landing — prefer backend-specific file extensions (`.cms` / `.jms` / `.wms`) for production splits today. ```ms @target("c") { extern function malloc(size: number): number; } @target("js") { function allocate(size: number): number { return 0; } } // else chain @target("c") { const cache = "/tmp/app-cache"; } else @target("js") { const cache = (globalThis.process?.platform === "win32") ? "C:/tmp/app-cache" : "/tmp/app-cache"; } ``` **`@platform` blocks (OS-conditional build directives).** Different from `@target` (which selects a *backend* — C, JS, WASM), `@platform` gates **build directives** on the *OS* the binary is being produced for. Use it to keep platform-native sources (`@compile`), header search paths (`@passC`), and link flags (`@passL`) out of builds that don't apply. Inner directives are collected only when the tag matches the build's OS target (`--os=macos|linux|windows`, or the host platform if no `--os` is passed). Untagged directives outside any `@platform` block are always-active. ```ms // Always-active — every build sees these. @compile("./platform/common/queue.c"); @compile("./platform/common/randombytes.c"); // macOS-specific — collected only when building for macOS. @platform("macos") { @compile("./platform/macos/state.m", "-fobjc-arc"); @compile("./platform/macos/core/window.m", "-fobjc-arc"); @passC("-I/opt/homebrew/opt/sdl3/include"); @passL("-lSDL3"); @passL("-framework Cocoa"); @passL("-framework WebKit"); } // Windows-specific — collected only when --os=windows. @platform("windows") { @compile("./platform/windows/state.c"); @compile("./platform/windows/core/window.c"); @passL("-lSDL3 -luser32 -lole32 -ldwmapi"); } ``` Notes: - The shape is `DecoratedDecl(decorators=[@platform("X")], decoratedNode=BlockStmt)` — i.e. it parses as a single decorated block, not two separate statements. Don't insert a stray `;` between `@platform("X")` and the opening `{` or the pairing breaks. - Nested `@platform` blocks are not supported (only flat top-level). - `darwin` is normalized to `macos` so `@platform("macos")` matches both the friendly name and the underlying host string. - This is OS-only. For backend conditionals (`c` vs `js`), use `@target`. ## 12. Collections and advanced types ### 12.1 TypeScript-compatible baseline These behave as in TypeScript. Just annotate the types: ```ms const a: number[] = [1, 2, 3]; const t: [string, number] = ["hi", 42]; const m: Map = new Map(); const s: Set = new Set(); const r: Record = { a: 1, b: 2 }; ``` ### 12.2 MetaScript stdlib extensions — hash tables MetaScript's standard library adds four first-class collection types that go beyond TypeScript's built-ins. They live in `std/core/struct`. #### 12.2.1 `HashMap` and `HashSet` — unordered High-performance, open-addressing hash tables with a structure-of-arrays layout for cache locality. Managed by ARC with refcounted entries. ```ms import { HashMap, HashSet } from "std/core/struct"; const freq: HashMap = new HashMap(); freq.set("hello", 3); freq.set("world", 7); freq.get("hello"); // 3 | null freq.has("world"); // true freq.delete("hello"); freq.size; // 1 // Subscript syntax (compiler-lowered) freq["foo"] = 1; const v = freq["foo"]; // 1 | null const seen: HashSet = new HashSet(); seen.add(42); seen.has(42); // true seen.delete(42); ``` Iteration order is **unspecified** — use `OrderedMap` / `OrderedSet` if you need deterministic order. #### 12.2.2 `OrderedMap` and `OrderedSet` — insertion-ordered Insertion-order-preserving hash tables. Same O(1) lookup as `HashMap`, with an additional doubly-linked list through the entries so iteration is in insertion order. Distinct implementation — not a `HashMap` wrapper. ```ms import { OrderedMap, OrderedSet } from "std/core/struct"; const cache: OrderedMap = new OrderedMap(); cache.set("a", 1); cache.set("b", 2); cache.set("c", 3); for (const [k, v] of cache) { console.log(`${k} = ${v}`); // "a = 1", "b = 2", "c = 3" } const recent: OrderedSet = new OrderedSet(); recent.add("log1"); recent.add("log2"); // Iteration preserves insertion order ``` #### 12.2.3 Decision table | Need | Pick | |---------------------------------------------|------| | JS-compatible, runs on both targets | `Map` / `Set` | | Fastest lookup on the native target | `HashMap` / `HashSet` | | Stable iteration order + hash lookup speed | `OrderedMap` / `OrderedSet` | | Simple object-as-dictionary | `Record` | ### 12.3 Arrays ```ms // Dynamic array const xs: number[] = [1, 2, 3]; xs.push(4); xs.pop(); const n = xs.length; // Fixed-size array — size is part of the type const v: number[4] = [1.0, 0.0, 0.0, 0.0]; // Array operations xs.map((x) => x * 2); xs.filter((x) => x > 1); xs.reduce((a, b) => a + b, 0); xs.slice(1, 3); ``` ### 12.4 References, pointers, and optional types | Type | Purpose | |--------------|-------------------------------------------------| | `Ref` | Heap-allocated, refcounted reference (DRC) | | `Ptr` | Raw pointer — no refcount, no bounds, FFI only | | `Borrow` | Zero-copy borrow of a heap value | | `Span` | Zero-copy view over a contiguous region | | `Maybe` | Option type: `Some(T)` or `None` | | `cstring` | NUL-terminated C string pointer | `Span` is particularly useful for zero-copy slicing of arrays and strings: ```ms const buf: uint8[] = readFile("big.bin"); const header: Span = buf.slice(0, 16); // zero-copy view processHeader(header); // header stops being valid when buf is freed — lifetime checked by the compiler ``` ### 12.5 Strings — deep dive Strings are mutable UTF-8 with copy-on-write semantics in the C backend. Many idioms that would require `StringBuilder` in other languages are direct string operations in MetaScript. ```ms // Concatenation const a = "hello, " + name; const b = `hello, ${name}`; // Span over a string (zero-copy) const s: string = "hello, world"; const view: Span = s.byteSlice(7, 12); // "world" // Char code (compile-time fold of single-char literal) const A: number = "A".code; // 65 // Zero-copy bridge to bytes and back const text = "config.json"; const bytes: uint8[] = text.asBytes(); // no copy const roundtrip: string = bytes.asString(); // no copy // Formatting const msg = `User ${user.name} has ${count} messages`; const padded = count.toString().padStart(4, "0"); ``` **Why mutable strings?** To avoid the `StringBuilder`/`join` ceremony common in GC languages. The COW layout ensures shared string values are not copied until modified; `.asBytes()` / `.asString()` allow free reinterpretation between text and binary representations. ## 13. Testing framework MetaScript has a built-in testing framework. Tests are first-class declarations using the `test` keyword, with `assert` for assertions. The test runner is invoked via `msc test`. ### 13.1 Basic syntax ```ms test "parser handles empty input" { const result = parse(""); assert result.ok; assert result.value.statements.length === 0; } test "addition returns sum" { assert add(2, 3) === 5; assert add(-1, 1) === 0; } ``` `test "" { body }` declares a test block. Each test runs in isolation. `assert ` fails the test (with a clear message) if the expression is falsy. ### 13.2 Real-world patterns The MetaScript compiler itself uses this framework extensively — see `src/test/handoff/*.ms` for hundreds of examples. Common patterns: #### Type-check only ```ms import { checkSource } from "./helpers"; test "union type alias resolves to TypeKind.Union" { const ctx = checkSource(` type MyData = | { text: string } | { count: number }; export function makeItem(kind: number, data: MyData): number { return kind; } `); let found = false; for (const sym of ctx.table.current.symbols.values()) { if (sym.name === "makeItem") { const ft = sym.symbolType; assert ft.typeChildren.length === 2; assert ft.typeChildren[1].kind === TypeKind.Union; found = true; } } assert found; } ``` #### End-to-end C codegen ```ms import { compileToC } from "../helpers"; test "E2E C: new Item(42) inlines msAllocTyped" { const src = `class Item { value: number } const x = new Item(42);`; const c = compileToC(src); assert c.ok; assert c.value.contains("msAllocTyped"); assert !c.value.contains("Item_new"); } ``` #### Runtime behavior ```ms test "HashMap roundtrip" { const m: HashMap = new HashMap(); m.set("a", 1); m.set("b", 2); assert m.get("a") === 1; assert m.get("b") === 2; assert m.get("c") === null; assert m.size === 2; } ``` ### 13.3 Power assert Assertions print a structured diagnostic on failure — the expression source, intermediate sub-values, and the mismatch: ``` FAIL test "addition returns sum" assert add(2, 3) === 6; │ │ │ │ 5 false ``` This makes debugging failed tests fast even without a debugger. ### 13.4 Running tests ```bash msc test # run all tests in the project msc test src/parser/lexer.ms # run tests from one file msc test --filter="HashMap" # run tests whose name matches msc test --verbose # show every assertion ``` The runner discovers `test` blocks across the project, executes them (in parallel by default on the C target), and reports pass/fail counts plus total time. ## 14. Cross-target FFI **MetaScript is an ecosystem omnivore** — through two specific mechanisms, not ambient magic. One file can reach code from multiple worlds simultaneously with **no wrappers, no binding generators, no glue code.** **Source-level import** — compiler parses the source, type-checks it as MetaScript code: - `import { ... } from "./utils.ts"` — any TypeScript source file (yours, vendored into the project, or a shared raw source). Parsed and type-checked identically to a `.ms` file. - `import { ... } from "sqlite3.h"` — any C header. Declarations imported, types bridged, companion `.c` auto-compiled and linked. **`extern` — the universal interop mechanism.** Not a fallback; the **first-class way** to reach anything not expressible as a parseable source file: - `extern function SHA256(...): void from "openssl/sha.h"` — declare any C function ad-hoc, with or without importing the whole header. - `extern const process: { env: Record }` — bind a runtime-provided JavaScript global (browser, Node, Deno, WASI). - `extern function fetch(url: string): Promise` — declare any ambient JS function by its contract. `extern` is how MetaScript plugs into every ecosystem the compiler doesn't ship a parser for: compiled `.js` without source, Node built-ins, browser APIs, WebAssembly host imports, OS syscalls, platform SDKs. Idiomatic, not a workaround. **What does NOT work — be explicit about this:** - `import` of a plain `.js` file — use `extern` instead to declare its surface. - `import "some-npm-package"` by bare package name — npm packages are not directly importable the way they are in Node / Bun. To use one: **(a)** vendor its TypeScript source into your project and `import { ... } from "./vendor/pkg/index.ts"`, or **(b)** declare the surface you need via `extern` and rely on the JS runtime to resolve it at runtime on the JS target. **Per-target composition story:** - **Native C target** — import `.ts` sources, import `.h` headers, `extern` any C symbol. No npm resolution. - **JavaScript target** — import `.ts` sources, `extern` JS globals or runtime-provided symbols. C imports are pruned at codegen. - **WebAssembly (via C backend)** — same as native C, plus `extern` for WASM host imports. ```ms // Three interop paths in one file import { validate } from "./schemas.ts"; // TypeScript source import { Database } from "sqlite3.h"; // C header extern function SHA256( // extern — C symbol data: u8*, len: usize, out: u8* ): void from "openssl/sha.h"; extern const process: { // extern — JS global env: Record; }; ``` The rest of this section details each path. ### 14.1 C / C++ **Calling C from MetaScript.** Two paths, both driven by `import` or `extern`. ```ms // Path 1: import a header directly import { sqlite3_open, sqlite3_close, sqlite3 } from "sqlite3.h"; // Path 2: inline extern declarations when no header is handy extern function SHA256(data: u8*, len: usize, out: u8*): void from "openssl/sha.h"; ``` - `import` from a `.h` parses declarations, imports the types, and auto-compiles the companion `.c` if present. - `extern function ... from "path.h"` declares a C function by signature; `@link`, `@passC`, `@passL` handle flags. - Pointer types (`u8*`, `T*`), struct layout, and C string conventions are preserved. Use `msStringToCStr(s)` to pass a MetaScript string to C; `msStringFromCStr(p)` to wrap a C-returned buffer. **Type mapping (C → MetaScript).** | C type | MetaScript type | |---------------------------|----------------------| | `int`, `int32_t` | `int32` / `number` | | `int64_t` | `int64` | | `char*`, `const char*` | `string` (or `cstring` for raw) | | `T*` | `Ptr` | | `struct Foo` | `Foo` (extern class) | | `enum E` | `enum E` | **Calling MetaScript from C.** Every exported MetaScript function on the C target emits a C-callable entry point with a mangled name. To export a stable C name, use `@cName`: ```ms @cName("msc_parse_input") export function parseInput(buf: u8*, len: usize): int32 { /* ... */ } ``` The emitted `.h` exposes: ```c int32_t msc_parse_input(const uint8_t* buf, size_t len); ``` Include `out//msc_exports.h` in the consuming C program. ### 14.2 JavaScript **Calling JS from MetaScript.** Two paths: `extern` declarations for runtime-provided globals, and `import` of `.ts` files for typed JS code you vendor in or write yourself. ```ms // Browser / Node globals that have no type source extern function fetch(url: string, init?: RequestInit): Promise; extern const process: { env: Record }; // Importing a TypeScript file (yours, vendored, or shared) import { parseConfig } from "./utils.ts"; import { Logger } from "./vendor/logger.ts"; ``` - `.ts` files are parsed as TypeScript (valid TS = valid MetaScript) and type-checked the same way. - For Node- or browser-only code, wrap it in `@target("js") { ... }` to guard it from C codegen (experimental — see §11). - Vendor a `.ts` file into your project (or publish one to the MetaScript package registry), and `import` just works. **Calling MetaScript from JS.** Exported functions and classes compile to ES2020 modules; consume them normally: ```ms // src/analytics.ms export function trackEvent(name: string, props: Record): void { /* ... */ } ``` ```js // consumer.js import { trackEvent } from "./analytics.js"; trackEvent("login", { method: "oauth" }); ``` ### 14.3 WebAssembly WebAssembly is reached via the C backend plus a cross-compilation flag on the OS axis. Two toolchains are supported: ```bash # WASI-targeted .wasm (server / edge runtimes, command-line) msc build --target=c --os=wasm --release # → app.wasm, via zig cc with -target wasm32-wasi # Browser WASM (Emscripten: .wasm + .js glue) msc build --target=c --os=emcc --release # → app.wasm + app.js, via emcc with -sWASM=1 -sALLOW_MEMORY_GROWTH=1 ``` Because the WASM path goes through C, all C-target features work unchanged — DRC, `spawn()` scheduling (single-threaded until threads-proposal), FFI, MetaScript stdlib. `extern` functions import from the host environment as WASM imports; exports are tagged with `@cName`. Typical edge-runtime target: `--os=wasm` + `--release` produces a stripped WASI module in the tens of KB, suitable for Cloudflare Workers, Fastly Compute, wasmtime, and WasmEdge. ## 15. Compile targets ### 15.1 Native C - Emits portable C (C99) that compiles with any modern clang or gcc. - Default toolchain is `zig cc` (for cross-compilation convenience). - Output: a single statically-linked binary or a shared library (`--emit=shared`). - **Binary size.** A minimal program is around **20 KB**. No bundled runtime. Realistic CLIs and servers land in 300 KB – 5 MB depending on what is linked. Node.js and Bun deployments of the same logic are typically ~100 MB. The difference between a Docker image that fits the lambda cold-start budget and one that doesn't. - **Memory footprint.** A running HTTP server stays **under 1 MB** resident. A minimal Node.js process starts at ~50 MB. For many- process deployments (per-request workers, sidecar patterns, actor supervisors), this 50× factor compounds quickly. - **Multi-core throughput.** The scheduler fans work across all CPU cores natively — `spawn()` and actors use the full CPU. A *minimal HTTP ping/pong server* built with `std/http` benchmarks at **~13× Node.js and ~5× Bun** on identical hardware — a microbenchmark isolating the scheduler + I/O path. Real workloads with business logic, DB I/O, crypto, or serialization will narrow that ratio to whatever the dominant bottleneck permits; the architectural win is that there is no single-thread event-loop ceiling to bump into, and no `cluster` module or worker pool is needed. - Startup: constructors run via `__attribute__((constructor))`. - Stdlib: mbedtls, sqlite, miniz, monocypher, argon2 vendored for crypto / storage / compression builds. - Cross-compile: `msc build --os=linux --cpu=arm64`, etc. ### 15.2 JavaScript - ES2020 modules. No transpilation step required for modern runtimes. - Prelude maps to JS-native (`console` is JS `console`, `Promise` is JS `Promise`, `Result` is a plain object with `ok` / `value` / `error`). - Node, Deno, Bun, and Cloudflare Workers all work without flags. - JS-exclusive code inside a `@target("js") { ... }` block can use `globalThis` directly. - Bundlers (Vite, Webpack, esbuild, Parcel) consume the output as ordinary ES modules. ### 15.3 WebAssembly Reached via the C backend (see §14.3). Two flavors: - **`--os=wasm`** (WASI) — `.wasm` module via `zig cc -target wasm32-wasi`. Target: CLI tools, edge runtimes, embedded WASM hosts. - **`--os=emcc`** (Emscripten) — `.wasm` + `.js` glue via `emcc`. Target: browsers, where Emscripten's runtime bridges DOM APIs. Binary sizes for WASM builds are similar to the native C target (tens to hundreds of KB for typical programs, before host-specific overhead). ## 16. `msc` CLI The `msc` binary is a **statically-linked single-file toolchain** that bundles the parser, type-checker, codegen (C / JS / WASM), LSP, formatter, test runner, and package manager into one executable. Runs natively on every mainstream platform — **macOS, Linux, Windows, FreeBSD, on both x86_64 and arm64**. No JDK, no Python runtime, no Node — one ~5 MB binary. Cross-compilation to any supported target works from any host (e.g., build a Linux-arm64 binary from your M-series Mac in one command). ``` msc [args] [flags] Build / run commands: build [file] Compile (default entry: build.ms or src/index.ms). run Build and run. check Type-check only. test [file] Run test blocks. fmt [file] Format source. init [name] Create a new project. clean Remove build artifacts. lsp Start the language server. upgrade Upgrade msc to the latest release. Package manager commands: add @ Add a dependency (pass -D for devDeps). remove Remove a dependency. install Install locked dependencies. update [name] Bump one (or all) direct deps. publish Publish the current package. login Authenticate with the registry (GitHub). logout Clear credentials. whoami Print current user. org Manage organizations. Common flags: --target= Compile target (default: c). --os= Target OS (C only; cross-compile). --cpu= Target CPU (C only). --cc= Explicit C compiler. --release Optimize for size + speed. --danger Disable safety checks (expert use). --strip Strip symbols from the binary. --emit=c Emit C source only; do not link. --output=, -o= Output path. --gc= Memory management mode (default: orc). --sanitize= Clang/GCC sanitizer passthrough. --passC= Extra flags to the C compiler. --passL= Extra flags to the linker. --strict-defs Warn on uninitialized var use. --strict-nil Warn on unguarded class deref. --check fmt: check only, do not modify. --gendeps Emit .d files (Make-style dep lists). --force, -f Force rebuild (ignore caches). --error-max= Stop after n errors. --time, -t Show phase timings. --verbose Show each compilation step. --colors=, --no-color Color output control. --help, -h Show help. --version, -v Show version. ``` ### 16.1 Editor integration and LSP MetaScript ships with first-class editor support — not a community afterthought. The `msc lsp` subcommand runs a full Language Server Protocol backend; every editor that speaks LSP plugs in. **Official editor plugins:** | Editor | Status | Notes | |---------|--------|-------| | **VSCode** | First-party extension | Auto-installs / updates `msc` on first run; zero manual setup | | **Neovim** | First-party plugin | Works with both built-in LSP client and `nvim-lspconfig` | | **Zed** | First-party integration | Uses Zed's native LSP support | | Any LSP-capable editor | Via `msc lsp` | Sublime, Helix, Emacs (eglot / lsp-mode), etc. | **What you get out of the box:** - Type information on hover (including inferred generics, narrowed discriminated unions, and actor isolation status). - Completion driven by the actual checker — not a heuristic fuzzy matcher. Suggestions reflect current scope, imports, and type context. - Go-to-definition, find-references, rename refactor across files. - Inline diagnostics as you type (same error messages the compiler emits — no drift between LSP and `msc build`). - Semantic token-based syntax highlighting (struct fields, actor methods, `@decorators`, `move` captures visually distinct from ordinary bindings). - Auto-import suggestions for stdlib and workspace symbols. - Code actions: auto-add missing `Result.err` branch, fill exhaustive `match` arms, convert `throw` to `Result.err`, extract function. - Inlay hints for inferred types and struct parameter ABI (so you can see when a struct is passed by value vs by pointer). **Syntax highlighting** uses a shared Tree-sitter grammar, so bare GitHub / Gitea / static site renderers highlight `.ms` files correctly without any editor plugin. **The VSCode extension auto-installs the compiler** on first open — downloads the right `msc` binary for your OS/arch, wires up the LSP, and is ready before you finish reading the notification. No `brew`, no `apt`, no manual PATH setup required for the newcomer path. ## 17. Package manager The package manager is built into `msc` — no separate tool. `build.ms` is both the build configuration and the package manifest; there is no parallel `package.json`. ### 17.1 Package naming - Scoped: `@org/name` (recommended for stdlib and organization packages). - Unscoped: `name` (for standalone packages). ### 17.2 Sources | Source | `deps` syntax | Use case | |------------|-----------------------------------|----------| | Registry | `"1.2.3"` | Normal published packages | | Git | `"git:/@"` | Forks, unreleased code, private packages | | File | `"file:../local-lib"` | Local path (monorepo, development) | Registry is `https://metascriptlang.org/pkg`. ### 17.3 `build.ms` ```ms // build.ms export const project = { name: "my-app", version: "0.1.0", targets: ["c", "js"], // emit both by default }; export const deps = { "@std/http": "1.2.0", "@my-org/auth": "git:github.com/my-org/auth@v0.4.1", "local-lib": "file:../local-lib", }; export const devDeps = { "@std/testing": "0.9.0", }; export const includes = [ "runtime/platform.h", ]; export const links = [ "sqlite3", "ssl", "crypto", ]; export const passC = [ "-DFEATURE_FOO=1", ]; ``` ### 17.4 Versioning Versions are `..[-]` — plain semver, **no `v` prefix** (`1.2.3`, not `v1.2.3`). **No range operators** — every dependency version is an exact minimum. The following are rejected: ```ms // REJECTED deps: { "foo": "^1.2.3", // error: version ranges unsupported "bar": "~1.2.3", // error "baz": ">=1.2.0", // error "qux": "1.x", // error "zee": "*", // error } ``` ### 17.5 Minimum Version Selection (MVS) MetaScript uses Go-style MVS to resolve dependency graphs into a flat lockfile. Given the root deps and the transitive deps of each package- version: ``` for each package in the graph: selected = max(all minimums declared for that package) ``` **Properties.** - **Deterministic** — same input, same output, forever. No environment- dependent resolution. - **Monotonic** — adding a new dep never lowers an existing package's version. Upgrades are always explicit. - **No backtracking** — single pass, O(n) in graph size with memoization. **Upgrade commands.** - `msc add @` — add a new dep at exact version. - `msc update ` — bump `` to the latest registry version, then re-run MVS. - `msc update` — bump every direct dep. ### 17.6 Lockfile (`msc.lock`) Plain JSON with: - `version` — schema version. - `generated` — ISO-8601 timestamp. - `packages` — map from `@` to an entry with `source`, `integrity` (SHA-256), and transitive `deps`. Auditable by eye, diffable in a PR, reproducible forever. ### 17.7 Cache layout Packages live in `~/.metascript/cache/` with: - `download/` — compressed source archives. - `src/` — extracted sources, one dir per `@`. - `staging/` — in-flight downloads (concurrent-safe via `flock()`). `msc install` populates the cache from `msc.lock` and wires imports. ## 18. Prelude The prelude is auto-imported in every module. It contains: - **`console`** — static class with `log`, `error`, `warn`, `info`, `debug`. - **`print`** — alias for `console.log`. - **`Result`** — the typed error result (see §7.2). - **`Promise`** — the single awaitable type returned by `async`, `spawn()`, and actor calls. - **Numeric primitives** — `int8`, `int16`, `int32`, `int64`, `uint8`–`uint64`, `float32`, `float64`, and `int` / `float` / `double` aliases. - **Pointer primitives** (C target) — `u8`, `u8*`, `usize` (intptr- sized), `cstring`. - **`Date.now()`** — millisecond timestamps. - **Builtin globals** — `Math`, `JSON`, `String`, `Number`, `Array`, `Object`, `Error`, and other TypeScript-compatible globals. ## 19. Idioms and common pitfalls ### 19.1 Nullable fields on reference types MetaScript supports `undefined` (as an alias for `null`), but idiomatic code uses `null` explicitly. For non-optional nullable fields: ```ms interface Scope { symbols: Symbol[]; parent: Scope; } const root: Scope = { symbols: [], parent: null as unknown as Scope }; if (s.parent !== null) { /* use s.parent */ } ``` For optional fields, use `?`: ```ms interface User { id: string; email?: string; // string | undefined (same as string | null at runtime) } ``` ### 19.2 Prefer `Result` over `throw` ```ms // Idiomatic function parse(input: string): Result { const tokens = try tokenize(input); const ast = try parseTokens(tokens); return Result.ok(ast); } // Allowed but less idiomatic function parseThrows(input: string): Ast { const tokens = tokenize(input); // throws on error return parseTokens(tokens); } ``` `Result` appears in the signature, composes via `try`, and does not require runtime unwinding. ### 19.3 `match` vs `switch` `match` is the default. Use `switch` only when you need break-through fall-through semantics, which is rare. ### 19.4 `struct` vs `interface` - Use `struct` for value-oriented data: vectors, colors, dates, coordinates, short-lived records. - Use `interface` for entities with identity: users, sessions, handles, long-lived state. - Heuristic: if two copies with the same fields should be indistinguishable, use `struct`. If each instance has identity and is referred to by name, use `interface`. ### 19.5 `for` loop forms inside `match` Inside `match` arms, prefer `for..of` or `while`. The C-style `for` loop is not normalized inside match-lowered blocks (parser-level restriction). ### 19.6 Closures and captured variables Array and object captures share by reference (JS semantics). Struct captures are copied into the closure's environment by default. ### 19.7 `@target` blocks Wrap target-specific code in `@target("c") { ... }` / `@target("js") { ... }` to keep it conditional per target. Experimental today; for stable per-target splits prefer backend-specific file extensions (`.cms` / `.jms` / `.wms`). See §11 for the full rules. ### 19.8 `defer` order Multiple `defer` statements run in LIFO (reverse declaration) order. If order matters, pay attention to declaration sequence: ```ms defer console.log("second"); defer console.log("first"); // runs first at scope exit ``` ### 19.9 `spawn` captures `const` captures are borrowed, `move` transfers ownership, `let` is rejected. If you capture a large immutable buffer, it is shared without copy. If you capture owned data you want to hand off, use `move`. ## 20. Project layout A typical MetaScript project: ``` my-project/ ├── build.ms # dependencies, targets, @compile/@include ├── msc.lock # pinned versions + integrity hashes ├── src/ │ ├── index.ms # entry point │ ├── api/ │ ├── domain/ │ └── runtime.c # vendored C files referenced by @compile ├── std/ # (optional) vendored stdlib override ├── vendor/ # local C libraries └── tests/ └── integration.test.ms ``` `msc build` uses `src/index.ms` as the default entry (overridable via `project.entry` in `build.ms`). `msc test` discovers every `test` block in the project. ## 21. Worked examples ### 21.1 Hello world ```ms // runs identically on C and JS targets console.log("hello world"); ``` ### 21.2 HTTP server ```ms import { createServer } from "@std/http"; const server = createServer((req, res) => { if (req.url === "/ping") { res.status(200).text("pong"); } else { res.status(404).text("not found"); } }); await server.listen(3000); ``` - `msc build --target=c` produces a single native binary. - `msc build --target=js` produces a Node-compatible bundle. - `msc build --target=c --os=wasm` produces a WASI `.wasm`. ### 21.3 Counter actor with supervision ```ms import { Supervisor, RestartStrategy, RestartType } from "std/actor/supervisor"; actor Counter { private count: number = 0; increment(): void { this.count += 1; } get(): number { return this.count; } } const sup = new Supervisor(RestartStrategy.OneForOne, 3, 5); sup.addChild({ name: "counter", start: () => new Counter(), restart: RestartType.Permanent, }); await sup.start(); ``` ### 21.4 Typed async service with `Promise` ```ms import { Database } from "@std/db"; async function fetchUser(db: Database, id: string): Promise> { const rows = try await db.query("SELECT * FROM users WHERE id = ?", [id]); if (rows.length === 0) return Result.err("not found"); return Result.ok(rows[0]); } async function handleRequest(req: Request): Promise> { const user = try await fetchUser(db, req.params.id); return Result.ok({ status: 200, body: JSON.stringify(user) }); } ``` ### 21.5 Mixing a TypeScript helper and a C library in one file ```ms // --target=c emits a sqlite-linked native binary // --target=js emits a Node bundle; C imports are pruned, JS paths remain import { groupByRegion, sumAmounts } from "./sales-helpers.ts"; // TS file import { Database } from "sqlite3.h"; // C library interface Sale { region: string; amount: number; } export function summarizeSales(rows: Sale[]): HashMap { const grouped = groupByRegion(rows); const totals: HashMap = new HashMap(); for (const [region, group] of grouped) { totals.set(region, sumAmounts(group)); } return totals; } export function persistTotals(totals: HashMap, dbPath: string): void { const db = Database.open(dbPath); defer db.close(); db.exec("CREATE TABLE IF NOT EXISTS totals (region TEXT, amount REAL)"); for (const [region, amount] of totals) { db.run("INSERT INTO totals VALUES (?, ?)", region, amount); } } ``` `sales-helpers.ts` is plain TypeScript — yours, vendored from a library, or shared across projects. The same file works in a pure TypeScript project too. ## Links and further reading - [Language reference (this file)](https://metascriptlang.org/llms-full.txt) - [Short index](https://metascriptlang.org/llms.txt) - [Website](https://metascriptlang.org) - [Interactive playground](https://metascriptlang.org/playground) - [AI-assisted learning chat](https://metascriptlang.org/learn) - [Package registry](https://metascriptlang.org/pkg) - [Install script (Unix / macOS)](https://metascriptlang.org/install.sh) - [Install script (Windows)](https://metascriptlang.org/install.ps1) ## Changelog See .