# MetaScript — Complete Language Reference > MetaScript is a statically-typed language with a surface syntax that > extends TypeScript: valid TypeScript files are valid MetaScript. On top of > that familiar foundation, MetaScript adds value types, discriminated > unions, `match` expressions, `Result` error handling, `defer`, > and deterministic reference counting. One source file compiles to > **native C** or **JavaScript**, so the same codebase ships as a native > binary on the server (and embedded / desktop / CLI) and as a > JavaScript bundle in the browser and in Node-like runtimes. > > Native binaries are tiny. A minimal MetaScript program compiled to C > strips down to **~20 KB** — there is 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. An HTTP server written in MetaScript > 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. The > combination of tiny binaries, small memory footprint, and native > multi-core execution makes MetaScript a natural fit for containers, > edge runtimes with tight cold-start budgets, embedded hardware, and > throughput-sensitive servers. > > This file is the full language reference. For a shorter index, see > . --- language: MetaScript version: 0.2.9 last_updated: 2026-04-18 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 --- ## How this document is organized TypeScript concepts that already exist in the wider ecosystem (variables, functions, control flow, classes, generics, union types, conditional types, mapped types, tsconfig-style options) behave as they do in TypeScript unless noted otherwise. This document focuses on: 1. Features unique to MetaScript. 2. Differences where MetaScript diverges from TypeScript semantics. 3. Extern / FFI patterns between MetaScript, C, and JavaScript. 4. Target-specific behavior. For cases not covered here, the TypeScript reference is a correct starting point. ## TypeScript coverage MetaScript implements most common TypeScript surface syntax. The parser, type checker, and resolver accept: - `let`, `const`, and `var` declarations with type annotations. - Function declarations, arrow functions, default/rest parameters, destructuring parameters, overload signatures. - `class` with fields, methods, constructors, `public` / `private` / `protected`, `static` members, inheritance, `abstract`. - `interface` (with MetaScript semantics — see below). - Generics: functions, classes, interfaces, type aliases, constraints (``), defaults. - Union types (`A | B`), intersection types (`A & B`), literal types, `typeof` queries, `keyof`. - Conditional types (`T extends U ? X : Y`), mapped types (`{ [K in keyof T]: ... }`), template literal types. - Discriminated unions (idiomatic in MetaScript — see below). - `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). - `as` casts, type assertions, `as unknown as T` for nullable field initialization. - JSX (with per-target compilation — DOM calls on JavaScript, static HTML strings on C). - `tsconfig.json`-compatible module resolution (paths, baseUrl, aliases). Where MetaScript diverges from TypeScript is documented below. When in doubt about syntax the TypeScript Handbook doesn't cover a MetaScript specific thing, assume TypeScript semantics. ## Unique features ### `struct` — value types `struct` declares a **value type**. Instances are stack-allocated by default and copied on assignment. This is the main semantic addition to TypeScript's type surface. ```ms struct Vec2 { x: float64; y: float64; } function scale(v: Vec2, k: float64): Vec2 { return { x: v.x * k, y: v.y * k }; } const a: Vec2 = { x: 1.0, y: 2.0 }; const b = scale(a, 3.0); // a is copied into scale; b is a new value ``` - Struct fields are laid out contiguously in memory (tight packing on C). - Large structs can be passed by pointer automatically when the compiler detects the caller doesn't mutate them (`const T*` in C); the language surface stays value-typed. - Structs cannot have reference cycles (they are stack-allocated). - Use `struct` for points, vectors, colors, small records, intermediate values. Use `interface` for things with identity (users, entities, handles). ### `interface` — reference types `interface` declares a **reference type** — heap-allocated and reference-counted via DRC. Object literals that satisfy the interface construct a reference. ```ms interface User { id: string; name: string; joinedAt: number; } 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 increments ``` - Fields are mutable unless the interface is declared `readonly`. - Methods can be attached via extension functions — see below. - Unlike TypeScript's structural-only `interface`, MetaScript's is also the declaration of a concrete heap-allocated shape. ### Extension methods Attach methods to any type (including primitive-like builtins) with UFCS-style declarations: ```ms function distanceTo(this self: Vec2, other: Vec2): float64 { const dx = self.x - other.x; const dy = self.y - other.y; return sqrt(dx * dx + dy * dy); } const origin: Vec2 = { x: 0, y: 0 }; const point: Vec2 = { x: 3, y: 4 }; point.distanceTo(origin); // → 5.0 ``` Extension methods are resolved at the call site (`obj.method()`) but compile to direct function calls, so there is no v-table overhead. ### `match` expressions Pattern matching with guards, destructuring, and exhaustiveness checking. This replaces most uses of `switch`. ```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, }; } ``` - **Expression form**: `return match (x) { ... }`. Each arm is an expression; no explicit `return` inside. - **Block form**: `match (x) { pattern => { ...stmts; return ...; } }` when an arm needs multiple statements. - **Wildcards**: `_` matches anything. - **Alternatives**: `"a" | "b" | "c" => ...`. - **Guards**: `pattern when (condition) => ...`. - **Destructuring**: match binds variables from the pattern directly. - **Exhaustiveness**: the checker errors if a case is missing, unless a wildcard is present. Bare identifiers in patterns are **bindings**, not value comparisons — `x => ...` captures, it does not compare. Use string/number literals or enum members for value comparison. ### `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 — provide a fallback value 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; ``` - `Result.ok` (boolean), `.value` (T), `.error` (E). - `Result.ok(v)` constructs the success variant; `Result.err(e)` the failure variant. - `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. - `try` inside a `match` arm is not supported — use an outer `if` to branch on `Result.ok` in that case. ### `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 `defer`s in the same scope execute in reverse declaration order. ### Unions — undiscriminated and discriminated MetaScript supports two flavors of sum type. **Undiscriminated unions** are the familiar TypeScript shape — a union of object literals, matched by field presence. Use them 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, }; } ``` The checker narrows inside each arm based on the literal field. This is the pattern TypeScript users already know. **Discriminated unions** are a MetaScript-specific construct. They use `match` in **type position**, keyed by an enum, and bind variant fields per enum member. Construction is validated at compile time — you cannot omit the discriminant and you cannot 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` is not a BinExpr field // { op: "+", left: l, right: r } // discriminant `kind` required ``` Field access resolves per variant. Fields common to multiple variants (same name and type) resolve unambiguously; variant-specific fields are narrowed by the discriminant. ```ms function getKind(d: NodeData): NodeKind { return d.kind; // discriminant — 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 where the discriminant 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. | ### `comptime` — compile-time evaluation Expressions marked `@comptime` are evaluated at compile time and can be used in constant contexts. This enables zero-cost abstraction over configuration, lookup tables, and generated code. ```ms @comptime function fibTable(n: number): number[] { const out: number[] = [0, 1]; for (let i = 2; i < n; i += 1) { out.push(out[i - 1] + out[i - 2]); } return out; } const FIB_20: number[] = fibTable(20); // computed at compile time ``` Comptime functions can call regular functions (which are evaluated in the compiler's sandbox) and return values, types, or even code fragments used by metaprogramming. ### Decorators (`@`) 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"; @target("c") function osSpecificPath(): string { return "/tmp/foo"; } @target("js") function osSpecificPath(): string { return "/var/folders/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 ``` Common decorators: | Decorator | Effect | | --------- | ------ | | `@builtin("name")` | Link to a runtime intrinsic by name. | | `@target("c"|"js")` | Limit declaration to a specific backend (multiple overloads allowed). | | `@include("foo.h")` | `#include` the header; auto-compile matching `.c` if present. | | `@compile("foo.c")` | Compile a specific C source file with the module. | | `@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 declaration site (escape hatch). | | `@derive(Eq, Hash)` | Auto-derive traits on structs/interfaces. | | `@awaitable` | Mark a generic instance as awaitable. | | `@affineAwaitable` | Mark as affine (consumed on await). | ### `move` `move` is an ownership-transfer keyword used in a handful of specific places — sending values into actors and explicitly consuming affine promises (e.g. the result of `spawn()` when ownership of the captured data needs to transfer). ```ms const data = loadLargeData(); const task = spawn(() => process(move data)); // data moves into the task // `data` is no longer usable here const result = await task; ``` Use `move` only where the type system asks for it. Most code never needs it — `const` captures are borrowed automatically. ### 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 Result type. - `Promise` — the single awaitable type returned by `async`, `spawn()`, actor calls, and parallel iterators. - Numeric primitives: `int8`, `int16`, `int32`, `int64`, `uint8`–`uint64`, `float32`, `float64`. - Primitive pointer types on C target: `u8`, `u8*`, `usize` (intptr-sized). - `Date.now()` for millisecond timestamps. ## Memory model — deterministic reference counting (DRC) MetaScript uses DRC on the native C target. On the JavaScript target, the host GC handles memory. How DRC works: - Interface instances and heap objects carry a refcount. - Assignment increments; scope-end decrements; hitting zero destroys. - Cycles are detected and broken by a small built-in cycle collector that runs incrementally (you can bound its work per frame). - Destruction is deterministic and immediate when reachable by no live reference — you can rely on destructors running. - `defer` handles resource cleanup that doesn't fit object lifetime (e.g., closing a socket mid-function). Consequences: - Zero GC pauses in normal operation. The cycle collector's worst-case work is bounded and can be scheduled. - Destruction order is predictable, so RAII patterns (destructors that close files, flush buffers, unlock mutexes) are first-class. - No write barriers, no tri-color marking, no heap scanning. ## Concurrency Every concurrency primitive in MetaScript returns the same surface type, `Promise`. `async` functions, `spawn()`, actor calls, and `.parallel().collect()` all produce a `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. ### `spawn()` — task parallelism `spawn(() => work())` schedules work on the runtime scheduler and returns a `Promise` with two internal flag bits: `AwaitableAffine` (awaited at most once) and `AwaitableScopeBound` (cannot escape the creating scope). The surface type is still `Promise` — the flags carry the safety rules without introducing a new type name. ```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; } // or run them together with Promise.all: const [a, b, c] = await Promise.all([ spawn(() => processChunk1(data)), spawn(() => processChunk2(data)), spawn(() => processChunk3(data)), ]); ``` Captures follow borrow-friendly rules: - `const` captures are borrowed (read-only references into the outer scope). - `move` captures transfer ownership into the spawned task. - `let` (mutable) captures are rejected by the compiler — they'd introduce a data race. `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. ### `actor {}` — stateful parallelism An actor is a long-lived object with a private mailbox. Its state is accessible only from within the actor; external callers go through the mailbox. Method return type determines message kind: `void` returns are **SEND** (fire-and-forget), non-void returns are **CALL** (request/reply via `Promise`). ```ms actor Counter { private count = 0; // void return → SEND: enqueues the message, returns immediately. increment(): void { this.count += 1; } // T 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 ``` Isolation is compiler-enforced: ```ms const counter = new Counter(); counter.count; // compile error: actor-isolated property await counter.get(); // OK: goes through the mailbox ``` Actors run serially — one message at a time — so internal state needs no locks. Actors can be reached from anywhere in the program; the compiler routes external method calls through the mailbox automatically. ### Supervision Supervisors monitor child actors and restart them on failure, with three restart strategies. ```ms const sup = new Supervisor({ strategy: "one-for-one", // "one-for-all" | "rest-for-one" maxRestarts: 3, maxSeconds: 5, // max 3 restarts within 5s, else supervisor crashes }); sup.addChild({ name: "database", start: () => new DatabasePool(connectionString), restart: "permanent", // always restart on crash }); sup.addChild({ name: "cache", start: () => new CacheWorker(), restart: "transient", // only restart on abnormal exit }); await sup.start(); ``` Restart strategies: | Strategy | Behavior | | ------------- | -------- | | `one-for-one` | Only the crashed child restarts. | | `one-for-all` | All children restart when one crashes. | | `rest-for-one`| The crashed child plus everything started after it restart. | 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 after a crash. **Links and monitors** connect actors for failure propagation: ```ms actorA.link(actorB); // bidirectional: if either dies, both die actorA.monitor(actorB); // unidirectional: A receives a down message // when B dies ``` ### `.parallel().collect()` — parallel iteration Data-parallel work over a collection. Chunks borrow the input without copying. ```ms const squared = numbers .parallel() .map(n => n * n) .collect(); ``` Like `spawn()`, parallel iteration uses structured concurrency — the caller waits for all chunks to complete. ### Cancellation and timeouts `spawn()` promises can be cancelled cooperatively; tasks observe cancellation at await points. ```ms const task = spawn(() => longRunning()); task.cancel(); // cooperative signal // timeout option on spawn await spawn(() => work(), { timeout: 5000 }); ``` ### Runtime cost on native On the native C target, actors are stackless fibers on a work-stealing scheduler. On a 2024 MacBook (M4), a ping-pong benchmark sustains **~50 million concurrent actors** — enough headroom for large event simulations and high-connection server workloads, shipped as a plain native binary. ## Cross-target extern interop MetaScript programs interoperate with external code on every target. The patterns below show how to call **out** from MetaScript, and how to expose MetaScript **in** to the target's native environment. ### 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. **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. ### 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, use `@target("js")` to guard it from C codegen. - Ecosystem integration: vendor a `.ts` file into your project (or publish one to the MetaScript package registry), and `import` 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" }); ``` ## Compile targets in detail ### 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**. There is no bundled runtime to drag along. Realistic CLIs and servers land in the 300 KB – 5 MB range depending on what's linked in. Node.js and Bun deployments of the same logic are typically ~100 MB. This is the difference between shipping a Docker image that fits in a lambda cold-start budget and one that doesn't, or between a CLI that downloads in 50 ms and one that takes a minute. - **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()`, actors, and `.parallel().collect()` all use the full CPU. An HTTP server benchmarks at **~13× Node.js and ~5× Bun** on identical hardware. The gain comes from avoiding single-thread event-loop bottlenecks; 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. ### 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. - `@target("js")` exclusive code can use `globalThis` directly. - Bundlers (Vite, Webpack, esbuild, Parcel) consume the output as ordinary ES modules. ## `msc` CLI ``` msc [args] [flags] Commands: build [file] Compile (default entry: build.ms or src/index.ms). run Build and run. check Type-check only. test Run test blocks. fmt Format source. init [name] Create a new project. clean Remove build artifacts. lsp Start the language server. upgrade Upgrade msc to the latest release. add @ Add a dependency. remove Remove a dependency. install Install locked dependencies. publish Publish the current package. login Authenticate with the registry (GitHub). logout Clear credentials. whoami Print current user. Common flags: --target= Compile target (default: c). --os= Target OS for C builds. --cpu= Target CPU. --release Optimize for size + speed. --strip Strip symbols from binary. --emit=c Emit C source only (no link). --gc= Memory management mode. --verbose Show each compilation step. ``` ## Package manager - Package names: `@org/name` (scoped) or `name` (unscoped). - Registry: . - Sources: registry (default), `git:` (git URL), `file:` (local path). - Lockfile: `msc.lock` pins exact versions + integrity hashes. - `build.ms` declares dependencies: ```ms // build.ms export const deps = { "@std/http": "1.2.0", "@my-org/auth": "git:https://github.com/my-org/auth#v0.4.1", "local-lib": "file:../local-lib", }; export const devDeps = { "@std/testing": "0.9.0", }; ``` - `msc add @std/http@1.2.0` edits `build.ms` and updates `msc.lock`. - `msc install` resolves from the lockfile, populates `~/.metascript/cache`, and wires imports. ## Worked examples ### Hello world ```ms // runs identically on C and JS targets console.log("hello world"); ``` ### 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. ### Counter actor with supervision ```ms actor Counter { private count = 0; increment(): void { this.count += 1; } get(): number { return this.count; } } const sup = new Supervisor({ strategy: "one-for-one", maxRestarts: 3, maxSeconds: 5, }); sup.addChild({ name: "counter", start: () => new Counter(), restart: "permanent", }); await sup.start(); ``` ### 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[]): Map { const grouped = groupByRegion(rows); const totals = new Map(); for (const [region, group] of grouped) { totals.set(region, sumAmounts(group)); } return totals; } export function persistTotals(totals: Map, 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. ## Idioms and common pitfalls ### No `undefined` MetaScript has no `undefined`. For nullable fields, use `null` with the `null as unknown as T` idiom: ```ms interface Scope { symbols: Symbol[]; parent: Scope; // must always be a Scope } const root: Scope = { symbols: [], parent: null as unknown as Scope }; ``` When reading a field, check for null explicitly: `if (s.parent !== null)`. ### 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` is preferred because it appears in the signature, composes through `try`, and does not require runtime unwinding. ### `match` vs `switch` `match` is the default. Use `switch` only when you need break-through fall-through semantics, which is rare. ### `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. - A useful 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`. ### `for` loop forms ```ms for (const x of array) { /* ... */ } // iterate values for (let i = 0; i < n; i += 1) { /* ... */ } // C-style while (cond) { /* ... */ } // condition loop ``` Inside `match` arms, prefer `for..of` or `while` — the C-style `for` is not normalized inside match-lowered blocks. This is a parser-level restriction, not a semantic one. ### Closures and captured variables Array and object captures share by reference (JS semantics). Struct captures are copied into the closure's environment by default. ### `@target` overload The same function name can have multiple `@target`-scoped implementations. At compile time, exactly one is selected per target. ```ms @target("c") function platformPath(): string { return "/tmp/foo"; } @target("js") function platformPath(): string { return (globalThis.process?.platform === "win32") ? "C:/tmp/foo" : "/tmp/foo"; } ``` ### `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 ``` ## Project layout A typical MetaScript project: ``` my-project/ ├── build.ms # dependencies, targets, @compile/@include ├── msc.lock # pinned versions ├── 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 ``` `build.ms` example: ```ms export const project = { name: "my-app", version: "0.1.0", targets: ["c", "js"], // emit both by default }; export const deps = { "@std/http": "1.0.0", "@std/json": "1.0.0", }; export const includes = [ "runtime/platform.h", ]; export const links = [ "sqlite3", "ssl", "crypto", ]; export const passC = [ "-DFEATURE_FOO=1", ]; ``` ## 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 .