# Glyph for agents This file takes you from zero to writing correct, runnable Glyph in one read. No source-diving required. If you only fetch one file about Glyph, fetch this one. ## What Glyph is Glyph is a statically typed language that **transpiles to TypeScript**. It looks almost like TypeScript, but it is deliberately stricter on a few axes so that code is verifiable, greppable, and safe for an agent to edit without breaking it. Every Glyph module compiles to a readable `.ts` file, runs anywhere TypeScript runs, and can import any npm package. You adopt it one file at a time. ## Install and run ```sh npm install -g @glyphlang/glyph # the compiler (CLI is `glyph`) npm install -g tsx typescript # needed for `glyph run` and the `tsc` stage of `build`/`check` ``` No global install needed. Every command below also works through `npx`, which is what to use in a sandbox that cannot install globally or does not keep an install between steps: ```sh npx @glyphlang/glyph init my-app # same CLI, nothing installed cd my-app && npx @glyphlang/glyph run ``` `glyph run` and the `tsc` stage still need `tsx` and `typescript` resolvable. In a project that has them in `devDependencies` a local `npm install` is enough; otherwise install them globally or run in a sandbox that already has them. ```sh glyph init [dir] # scaffold a runnable starter (src/, .types/, package.json) glyph check [path] # type-check a file or tree, writing nothing; runs the @example/@doc @run gate by default (--no-test to skip) glyph check --agent [path] # the --json object plus, per diagnostic, the repair constraints an edit must keep and the glyph_symbol description of every symbol it names glyph fix [path] # apply the repairs the compiler fully determines (unused imports, the missing arms of a match, a did-you-mean); it says what it declined and why glyph run [path] [args...] # type-check, compile, and run main(argv); no path means the current project; hyphenated args reach the program, `--` before ones that collide with glyph's own flags glyph build src/ --out dist/ # compile a tree to TypeScript (tsc --strict and @example/@doc @run by default) glyph build src/ --out dist/ --json # emit diagnostics as JSON (code, severity, file path, module, range, entity, cause, expected/actual, alternatives, help) for tools/agents glyph build src/ --out dist/ --no-test # skip the @example / @doc @run / property tests glyph fmt [path] # format in place (one canonical layout) glyph fmt --check [path] # exit non-zero if anything is unformatted (CI), writes nothing glyph gen openapi spec.yaml --out src/ # generate committed Glyph types from an OpenAPI/JSON Schema spec (--client: a typed std/http client; --handlers: server stubs + a router) glyph gen dts types.d.ts --out src/ # generate committed Glyph types from a TypeScript .d.ts (needs node + typescript) glyph gen zod schemas.ts --out src/ # generate committed Glyph types from zod schemas (needs tsx + zod) glyph llms # reprint this bootstrap offline (alias: glyph docs) glyph --explain E0204 # long-form explanation + fix for any error code glyph --explain E0204 --json # the same as data, with a wrong program from the negative corpus that draws it, compiled now glyph mcp [root] # run an MCP server (stdio) exposing analysis to an agent as tools glyph query ... # ask one of those tools from the command line and print its JSON glyph doctor # check node/tsx/tsc, and this compiler against the latest release (--offline to skip the lookup) glyph upgrade [dir] # move the project's exact Glyph pin to the latest release and npm install it (--to , --dry-run) glyph --update # move this installed compiler to the newest release (--update-dry-run). Acts on the tool; `upgrade` acts on a project ``` A project pins its compiler exactly (`"@glyphlang/glyph": "0.1.x"`, no `^`), because a 0.1.x release may add a diagnostic that rejects code which compiled before. Do not widen that pin to a range to make an install resolve; move it with `glyph upgrade` and rebuild. `glyph run` builds the whole directory the file sits in, so it reports exactly the diagnostics `glyph build` would report on that tree, warnings included. They go to stderr after the program's output, followed by a `glyph run: N error(s), M warning(s) in the source tree` line. A sibling module that fails to compile is not importable but does not stop the run, and today does not change the exit code: `glyph run` exits with whatever `main` returned. If you drive Glyph through the Model Context Protocol, `glyph mcp [root]` speaks MCP over stdio and exposes eleven tools over the project: - `glyph_symbol` — everything the compiler holds about one symbol, addressed by its `module::name` identity or by a position: kind, visibility, a record's fields with types, a union's variants with payloads and the syntax that constructs each, a callable's parameters and return, an interface's members, whether a `match` over it must be exhaustive, and its `@example` text. The module half is any module the compiler holds, including the stdlib modules it carries, so the prelude `Result` is `std/result::Result`. Ask this before writing a call, a record literal or a `match` over a type you did not declare. - `glyph_impact` — what breaks if you make one named change to one declaration (`add_variant`, `remove_variant`, `rename`, `change_arity`, `change_signature_type`, `remove`), per site, with the diagnostic each will raise. Call it before changing a declaration, not after. - `glyph_variants` — every `match` site in the project over one tagged union and which variants each site's arms name, with the union's own variants and their construction syntax. - `glyph_references` — every edge into a symbol across the project, split by relation (`CALLS`, `REFERENCES`, `GENERATED_FROM`), each with its provenance. - `glyph_definition` — where the name at a position is defined, following imports, with the `module::name` identity so the answer chains into `glyph_impact`. - `glyph_hover` — the type at a position, read inside the file's project: an expression, a declaration name, a parameter, an annotation, a variant, or a binding. A name declared in another module answers from that module's own declaration, so the binding in `import m { N }`, an imported function at a call, an imported variant used as a value and a field read off an imported record all have a type. A namespace import binding does not: it names a module, and a module has no type. - `glyph_symbols` — search the project's declarations by name substring; each entry carries the identity, whether it is exported, its kind and its signature. - `glyph_diagnostics` — check one file inside its project, returning `{ diagnostics, unindexed, not_run }` and the file's identity. `diagnostics` holds `glyph check --json`'s own coded diagnostics with ranges; `unindexed` names the project files that could not be read; `not_run` names the three checks `glyph check` makes and this does not, `tsc`, the `E0104` import naming no module, and `E0400` for a failing `@example`. A client that read a top-level array reads `diagnostics` now. - `glyph_assignable` — can a value of one type go where another is declared, asked of the checker's own comparison. `WILL_FAIL` with the code it raises, `COMPATIBLE` only where a rule accepted, `UNDETERMINED` where no rule covers the pairing. - `glyph_dependencies` — what one declaration depends on: every declaration its own source names, split by relation (`CALLS`, `REFERENCES`, `FIELD_ACCESS`), each edge with both ends, its provenance and where it sits. The mirror of `glyph_references`, read from the same tables at the other end. - `glyph_exports` — what one module makes visible to an importer, from the compiler's own export query: every `pub` declaration and every variant a `pub` union hoists, each as a `glyph_symbols` entry with its identity, kind and signature. Positions are 0-based `line`/`character` (UTF-16). This is the interactive complement to `glyph build --json`, which remains the batch path for coded diagnostics. Without an MCP client, `glyph query` asks the same tools from the command line and prints the same JSON: one verb per tool, taking the tool's own arguments as flags. ```sh glyph query symbol --entity orders::Order glyph query assignable --path src/main.glyph --from 'Nullable' --to int glyph query impact --entity orders::Order --change change_signature_type glyph query dependencies --entity checkout::announce glyph query exports --module orders glyph query hover --path src/main.glyph --line 12 --character 6 ``` The JSON goes to stdout and the exit code is 0. A refused question (a symbol this project does not hold, a type spelling that does not parse) writes the reason to stderr and exits 2. ## The canonical program shape ```glyph module hello import std/io import std/process fn main(argv: Array) -> number { io.println("hello from glyph") return 0 } ``` - Every file starts with `module `. - A sibling module in the same source directory is imported by its bare module name: `import world` for `src/world.glyph`. No package prefix. Only `std/...` and real npm packages carry a path. - To use both a module's namespace and its bare constructors, import it twice: `import rules` and `import rules { Walked, Refused }`. That is the general rule, not a quirk of any one module. - `main(argv: Array)` is the entrypoint. Its `number` return is the process exit code. - `main` **may be `async`**: `async fn main(argv: Array) -> number`. The runtime `await`s it. - **`await` on a method chain.** A fluent chain of value methods awaits the whole chain: `await cursor.find({}).to_array()` awaits `to_array` (the async terminal), as in JavaScript. The Result idiom awaits the head call: `await load(p).map_err(f)` runs `map_err` on the awaited `Result`. A bare or namespaced function head (`load(...)`, `http.get(...)`) is the Result idiom; a value-method head (`x.method(...)`) is fluent. You write the natural thing either way. - Imports are either **named** (`import std/result { Ok, Err }` brings names into scope) or **namespaced** (`import std/array` then `array.map(...)`). - An npm package whose export *is* a function (`module.exports = f`: express, lodash, debug, chalk@4, commander, `gray-matter`'s `matter(text)`) is imported with `import express { default as app }`. `as` is legal only after `default`; renaming an ordinary imported name is still an error. ## Syntax cheat-sheet ### Bindings (`let` / `const`) and mutation (`mut`) `mut` is a **statement prefix on a mutation**, not a kind of binding. You introduce a binding with `let` (function-level) or `const` (module-level), and to *change* it later you write a `mut` statement: ```glyph module bindings type Role = | Admin | Member type User = { role: Role, } pub fn demo(user: User, grid: Record, key: string, value: number, xs: Array, item: number) -> number { let total = 0 // binding (immutable by default; prefer let) mut total = total + 5 // reassignment: `mut` PREFIXES the assignment mut user.role = Admin // field assignment mut grid[key] = value // index assignment mut xs.push(item) // mutating method call return total } ``` `mut` is legal in exactly four forms — `mut x = e`, `mut x.field = e`, `mut x[k] = e`, `mut x.method(args)`. A bare assignment without `mut` (`total = ...`) is a parse error, and `mut foo()` (a free-function call) is illegal. Because every mutation is marked, `grep -n "^\s*mut "` is a complete audit of all mutation in a file. ### `match` is the only conditional There is **no `if`/`else`**. Every branch is a `match`, and **every arm ends with a trailing comma** (including the last). ```glyph module conditionals type Role = | Admin | Member | Guest pub fn describe(role: Role, n: number, argv: Array) -> string { let label = match role { Admin => "admin", Member => "member", Guest => "guest", } let sign = match n > 0 { // match on a bool true => "positive", false => "non-positive", } let kind = match argv { // string-literal + array-destructuring patterns [] => "empty", ["add", text] => "add", [head, ..._] => "other", else => "fallback", // `else` is the catch-all, only as a whole arm } return "${label} ${sign} ${kind}" } ``` A `match` that is the whole value of a `let` or a `mut` accepts everything a statement accepts: a block-bodied arm, an `await` in an arm, a `break` or `continue` that leaves the surrounding loop, and an arm that reads the binding it is assigning (the accumulator form). ```glyph module accumulators import std/array import std/io fn is_fence(line: string) -> bool { return line == "---" } fn no_cache() -> Array { return [] } async fn fetch_all(urls: Array) -> Array { return urls } pub async fn render(lines: Array, offline: bool, urls: Array) -> void { let in_fence = false for line in lines { mut in_fence = match is_fence(line) { // reads the binding it assigns true => !in_fence, false => in_fence, } } let cache = match offline { true => no_cache(), // `{}` here is an empty BLOCK, not a record false => await fetch_all(urls), } io.println("${in_fence} ${array.len(cache)}") } ``` A `match` nested **inside a larger expression** (an argument, an operand, a field of a literal) compiles to a closure, so its arms must be single expressions and a `return` inside one is illegal. Hoist it into its own `let` and the restriction goes away. A name in a constructor's payload position tests the payload when it is a variant of that payload's own union, and binds it when it is not. So `Err(Blank) => .., Err(e) => ..` handles the `Blank` error and then every other one, which is how it reads. Two arms that ask about the same tag twice (`Err(e) => .., Err(other) => ..`) are `E0305`: only the first could ever run. ### Loops (`for` / `loop`) There is no `while`. `for` iterates a bounded collection; `loop` is the unbounded form and needs a `break`. ```glyph module loops import std/array import std/io type Item = { name: string, } fn done() -> bool { return true } fn step() -> void { } pub fn walk(items: Array, scores: Record, lines: Array) -> void { for item in items { // one binding: the element io.println(item.name) } for i, item in items { // two bindings: index (0-based number), element io.println("${i}: ${item.name}") } for key, value in scores { // over a Record: key (string), value io.println("${key} = ${value}") } // The array form is picked from the iterand's declared type. Iterating a call's // result directly gives you a STRING index and nothing complains, so bind it: let rows: Array = array.slice(lines, 1) for i, row in rows { io.println("${i}: ${row}") } loop { // unbounded; `break`/`continue` are legal match done() { true => break, false => step(), } } } ``` ### Closures ```glyph module closures import std/io pub fn demo() -> number { let double = fn(n: number) -> number { n * 2 } // tail expression is the return value let log = fn(s: string) -> void { io.println(s) } log("two times four") return double(4) } ``` A function type is written the same way, and `async fn(...) -> T` is the type of one you have to await: ```glyph module function_types type Fetched = { url: string, outcome: bool, } async fn check(url: string) -> bool { return url != "" } pub fn apply(f: fn(number) -> number, n: number) -> number { return f(n) } pub fn task_for(url: string) -> async fn() -> Fetched { return async fn() -> Fetched { return { url: url, outcome: await check(url) } } } ``` A plain `fn() -> T` emits `() => T`, so an async value does not fit it. Write the `async` out. ### `Result` / `Option` and the `?` operator ```glyph module parsing import std/result { Result, Ok, Err } pub fn parse_age(s: string) -> Result { let n = number.parse(s)? // `?` unwraps Ok, or returns the Err from this fn return Ok(n) } ``` Rules for `?`: it may appear **only inside a function whose return type is a `Result`**; the operand must be a `Result`; on `Ok` it unwraps to the success value, on `Err` it returns that error from the enclosing function. The error type `E` must match the enclosing function's `E` **exactly** (there is no `From` conversion in v1). ### Record literals and sum types ```glyph module records type Role = | Admin | Member | Guest type User = { email: string, role: Role, } pub fn one() -> User { let u: User = { // bare object literal; type comes from the annotation email: "a@b.com", // every field, trailing comma, no `TypeName {}` prefix role: Admin, } return u } ``` A union **variant that carries a payload** is constructed `Variant({ field: value })`: ```glyph module shapes type Shape = | Circle({ radius: number }) | Square({ side: number }) pub fn unit() -> Shape { let c: Shape = Circle({ radius: 2 }) return c } ``` There is **no object-literal shorthand** (`{ email }` is rejected; write `{ email: email }`). ### JSX (components) `component` declarations emit React function components. JSX control flow uses a **restricted set of directives** — ``, ``, ``, ``, `` — not arbitrary `{cond && ...}` expressions. A component takes no parameter or one props record, which is the object its element's attributes build: `component Greeting(name: string)` is E0229. ```glyph needs-deps component Greeting(props: { name: string }) { return
Hello, {props.name} Hello, stranger
} ``` ### Template strings `"Hello, ${user.email}"` interpolates expressions, calls included: `"total: ${format_money(sum)}"`. Write `\${` for a literal dollar-brace. The one thing the interior cannot hold is another string literal (`"${f("x")}"`) — bind it to a `let` first. A `"..."` string decodes six escapes and nothing else: `\n` (newline), `\t` (tab), `\r` (carriage return), `\"` (a literal quote), `\\` (a literal backslash), and `\u{HEX}` for an arbitrary Unicode code point by its hex value, `\u{1b}` for ESC among them. Anything else after a backslash is a lex error (E0001). A normal string may also span lines: a raw newline inside `"…"` is kept verbatim, and it means the same thing as `\n`. ``` let usage = "usage: report --json machine-readable output " ``` There is a second spelling, `"""..."""`, which is raw: escapes are not decoded, so a `\n` inside one stays a backslash and an `n`. Everything else is the same, including interpolation — `"""a ${b}"""` interpolates just like `"a ${b}"`. Reach for it when the text is full of backslashes or quotes (a regex, a snippet of another language, a `@doc` block). `glyph fmt` copies a multi-line `"""` literal through verbatim; a single-line one that interpolates comes back in the ordinary `"..."` spelling, same content. ## The standard library Two ways to read a module's real signatures when it is not written out below (`std/regex`, `std/intl`, `std/nullable` and `std/schema` are shipped and importable but not detailed here). Import a name it does not export and read the error, which lists the ones it does: ``` [E0105] import: `nonexistent` is not exported by `std/random` (exports: Rng, seeded) ``` Or read the emitted runtime: `glyph build` writes the stdlib source it used to `/.glyph-runtime/std/*.ts`. That directory holds only the modules your project imports, not all 37, so it answers "what does this give me" for a module you already use and cannot introduce you to one you do not. Call namespaced functions as `module.fn(...)`. Types and constructors come in via named imports. Signatures below are in Glyph terms. ### Prelude — available with no import ``` number.to_string(n: number) -> string number.parse(s: string) -> Result result.all(xs: Array>) -> Result, E> // first Err wins print(message: string) -> void // quick stdout line assert(condition: bool) -> void // throws if false ``` Ambient types (usable with no import): `number`, `string`, `bool`, `void`, `Array`, `Record`, `Schema`, `Issue`. ### std/result ``` type Result // constructors: Ok(value), Err(error) result.map(f) // method: transform the Ok value result.map_err(f) // method: transform the Err value ``` ### std/option ``` type Option // constructors: Some(value), None ``` ### std/array ``` array.find(xs, predicate) -> Option array.filter(xs, predicate) -> Array array.map(xs, f) -> Array array.zip(a, b, f) -> Array array.len(xs) -> number array.push(xs, x) -> Array // returns a new array array.concat(a, b) -> Array array.reverse(xs) -> Array array.slice(xs, start, end?) -> Array array.any(xs, predicate) -> bool array.contains(xs, value) -> bool array.sort(xs, compare) -> Array array.fold(xs, init, f) -> A // f is (acc, x); no index array.fold_while(xs, init, f, done) -> A // stops before the next element once done(acc) is true array.try_fold(xs, init, f) -> Result // f returns Result; the first Err is the result array.max(xs: Array) -> Option // None on [] array.min(xs: Array) -> Option // None on [] array.sum(xs: Array) -> number // 0 on [] array.max_by(xs, key: fn(T) -> number) -> Option // ties: first element wins array.min_by(xs, key: fn(T) -> number) -> Option // ties: first element wins array.index_of(xs, value) -> Option array.flat_map(xs, f) -> Array // flattens one level array.range(count) -> Array // [0, 1, ..., count-1] array.range_from(start, end) -> Array // [start, ..., end-1] ``` `fold_while` is the early exit `fold` lacks: `done(acc)` is checked before each element, so a search stops the moment the answer is known (alpha-beta pruning is `done = fn(w) { return w.alpha >= w.beta }`). `try_fold` is the error-carrying form: its callback returns `Result`, the first `Err` is the result, and a `match` over the call must have an `Err` arm. There is no `Step` type. ```glyph module main import std/array import std/io fn spend(spent: int, cost: int) -> int { return spent + cost } fn over(spent: int) -> bool { return spent >= 10 } fn add_positive(sum: int, x: int) -> Result { return match x < 0 { true => Err("negative ${number.to_string(x)}"), false => Ok(sum + x), } } pub fn main() -> void { // 15: stops before the 7, because 4 + 5 + 6 already reached the budget io.println(number.to_string(array.fold_while([4, 5, 6, 7], 0, spend, over))) match array.try_fold([1, 2, 0 - 1, 4], 0, add_positive) { Ok(total) => io.println(number.to_string(total)), Err(e) => io.println(e), // "negative -1"; the 4 is never visited } } ``` ### std/string ``` string.from(value) -> string string.join(parts, separator) -> string string.split(s, separator) -> Array string.len(s) -> number string.trim(s) -> string string.lower(s) -> string string.upper(s) -> string string.contains(s, substring) -> bool string.starts_with(s, prefix) -> bool string.ends_with(s, suffix) -> bool string.repeat(s, count) -> string // a negative count yields "" (TS throws) string.pad_start(s, width, pad?) -> string // pad defaults to a space string.pad_end(s, width, pad?) -> string string.slice(s, start, end?) -> string string.index_of(s, needle, from?) -> Option // None, not -1 string.replace_all(s, from, to) -> string // every occurrence string.trim_start(s) -> string string.trim_end(s) -> string ``` Argument order: every module except `std/regex` takes the subject first; `std/regex` takes the pattern first. So `string.replace_all(s, from, to)` and `regex.replace_all(pattern, text, replacement)` are opposite orders, as are `string.split(s, separator)` and `regex.split(pattern, text)`, and every parameter of all four is a `string`, so a swap compiles and prints the wrong thing. Indices are UTF-16 code units, and there is no codepoint accessor (no `chars`, no `char_at`) by design: one that can split a surrogate pair is worse than none. Walk codepoints by encoding to bytes with `encoding.hex_encode` and reading two hex digits at a time. Glyph models the return type of these functions, so `for i, part in string.split(s, ",")` binds a numeric index with no annotation. The six with a `?` parameter (`string.slice`, `string.index_of`, `string.pad_start`, `string.pad_end`, `array.slice`, `json.stringify`) are the exception: the arity check has no range yet, so they stay untyped. That means a `match` on `string.index_of` with no `None` arm is not an E0200 and throws at run time. Write the `None` arm. ### std/io ``` io.println(message) -> void io.eprintln(message) -> void // to stderr io.print(message) -> void // no newline: a prompt shares its line io.eprint(message) -> void // to stderr, no newline io.is_terminal() -> bool // is stdout a terminal, not a pipe io.stdin_is_terminal() -> bool // is a person typing, not a file piped in io.read_line() -> Option // one line, as soon as it arrives; None at EOF (files: fs.open_lines) io.read_to_string() -> string // the rest of stdin ``` `read_line` does not wait for stdin to close, so an interactive loop (prompt, read, respond) works. It strips a trailing `\r`, and returns a final newline-less line once before `None`. `read_to_string` drains the same buffer: first call gets all of stdin, a call after some `read_line`s gets the rest. ### std/json ``` json.parse(text) -> Result> // casts; use parse_with to validate json.parse_with(text, schema) -> Result> json.stringify(value, options?) -> string // options: { indent: number } ``` For a record/union type `T`, `json.parse(text)` is auto-rewritten to validate against `T.schema`. Use the `json.parse` namespace form (not the named-import form) to get validation. ### Runtime validators (`T.parse` / `T.is` / `T.schema`) Every record (and non-generic union) type `T` you declare also generates a runtime descriptor with three members. This is the mechanism behind `json.parse`, and it is how a boundary value becomes typed: ``` T.is(value: unknown) -> bool // shape guard for declared fields T.parse(value: unknown) -> Result> // validate an unknown into a Result T.schema // a Schema (e.g. T.schema.array()) ``` Use `T.parse` on an already-decoded `unknown` (a request body, a config object); use `json.parse(text)` when you have a raw JSON *string*. There is no `as` cast in Glyph, so `T.parse` (or a `match`/`is` narrowing) is the only way to go from `unknown` to `T`. ```glyph module validate type User = { id: number, name: string } pub fn handle(body: unknown) -> string { return match User.parse(body) { // untrusted input, validated Ok(user) => user.name, Err(_) => "invalid", } } ``` A record descriptor is strict by default: it confirms the declared fields *and* rejects a value carrying undeclared keys. Put `@open` above a `type` to allow extra keys (`@open` then the `type` line). A **generic** record type (`Paginated`) also gets a descriptor. Call it with the type argument: `Paginated.parse(body)` validates the page deeply — each `items` entry is checked as a `User`, not just for presence. The `is` pattern works the same: `match v { is Paginated => ..., else => ... }`. The compiler synthesizes the per-parameter checker at the call site, so the type argument must be given explicitly. A generic descriptor omits the `.schema` member. Scope today: descriptors cover non-generic and generic record types; tagged unions and imported/`.d.ts` types don't get one (materialize an imported type with `glyph gen dts` to give it one). A `.d.ts` (or OpenAPI) discriminated union (`{ kind: "a"; ... } | { kind: "b"; ... }`) materializes as a tagged union of generated variant records plus a `parse_(v)` dispatcher that reads the tag and validates into the right variant. To build a validator *combinator* (a `zod`-style `object_schema`) whose output type follows the shape you pass, use the `infer_output` type operator so you don't repeat the output type by hand: ```glyph fragment-of=infer-output fn object_schema>>( shape: Shape, ) -> Schema> { // the body builds the validator; the whole program is below } // The shape must produce a `User`, or this does not compile: const user_schema: Schema = object_schema({ name: string_schema(), age: number_schema(), }) ``` Whole, and compiled. `string_schema` and `number_schema` stand in for the leaf parsers a validator library gives you, which is what `extern_ts` is for: ```glyph example=infer-output module schemas type User = { name: string, age: number, } fn string_schema() -> Schema { return extern_ts("{ parse: (v: unknown) => typeof v === 'string' ? { ok: true, value: v } : { ok: false, issues: [] } } as never") } fn number_schema() -> Schema { return extern_ts("{ parse: (v: unknown) => typeof v === 'number' ? { ok: true, value: v } : { ok: false, issues: [] } } as never") } fn object_schema>>( shape: Shape, ) -> Schema> { return extern_ts("shape as never") } // The shape must produce a `User`, or this does not compile: const user_schema: Schema = object_schema({ name: string_schema(), age: number_schema(), }) ``` `infer_output` unwraps each field's parser to the type it outputs, so the compiler derives the schema's output type from the shape and checks it against your annotation. It matches a parser field *structurally* (any `{ parse(input: unknown) -> Result }`), so the wrapper need not be named `Schema` — your own `Codec` works too. The generic parameter is bound with `` (this is how generic bounds are written; they lower to a TypeScript `extends` clause). ### std/fs ``` type ErrorKind = NotFound | IsADirectory | NotADirectory | PermissionDenied | AlreadyExists | Other({ code: string }) type FsError = { kind: ErrorKind, message: string } type FileInfo = { is_dir: bool, is_file: bool, size: int, modified: int } // size bytes, modified epoch ms fs.read_text(path) -> Result fs.write_text(path, contents) -> Result fs.append_text(path, contents) -> Result // append, creating the file (append-only logs) fs.make_dir(path) -> Result // create dir + parents, idempotent (mkdir -p) fs.exists(path) -> bool fs.remove(path) -> Result // ErrorKind.NotFound for a missing file fs.read_dir(path) -> Result, FsError> // entry names, not full paths; not recursive, OS order fs.is_dir(path) -> bool // false for a missing or unreadable path fs.stat(path) -> Result // follows symlinks fs.read_bytes(path) -> Result // the octets, undecoded (std/bytes) fs.write_bytes(path, contents) -> Result fs.append_bytes(path, contents) -> Result type LineReader // a file open for reading line by line; opaque fs.open_lines(path) -> Result // a directory opens, then fails on the first next_line fs.next_line(r) -> Result, FsError> // next line without its `\n` (and a `\r` before it); Ok(None) at the end fs.close_lines(r) -> void // for stopping early; idempotent ``` `match e.kind` is checked for exhaustiveness like any union you declared: cover all six kinds (including `fs.ErrorKind.Other({ code })`) and you need no `else` arm; omit one and the build fails with E0200. Walk a tree with `read_dir` + `is_dir` + `path.join`; there is no `walk` helper. `read_text` holds the whole file; `open_lines` holds one 64 KiB chunk per reader and hands out a line at a time. A read error is `Err`, never `None`, so a `match` over `next_line` with no `Err` arm is E0200. Reaching the end closes the reader and so does an error; `close_lines` is for stopping early. No `owned` tracking: close it or let `next_line` reach the end. The reads block the event loop, so not in a program that also serves a socket or HTTP. ``` let r = fs.open_lines(path)? let first = fs.next_line(r)? // Ok(Some(line)), Ok(None) at the end, Err on a read error fs.close_lines(r) // only when stopping before the end ``` The `_text` calls decode and encode UTF-8; use `_bytes` for any file that is not text. `read_text` on a PNG replaces every byte that is not valid UTF-8 with U+FFFD and reports success. ### std/bytes An immutable sequence of octets, and the codecs between octets and text. Reach for it whenever the data is not text: a binary file, an HMAC key, a wire frame. ``` type Bytes // a Uint8Array at run time type BytesError = { message: string, index: int } // index is a position in the rejected input bytes.empty -> Bytes bytes.from_array(xs: Array) -> Result // rejects anything outside 0..255 bytes.to_array(b) -> Array bytes.from_text(text) -> Bytes // UTF-8 encode; total bytes.to_text(b) -> Result // UTF-8 decode; rejects, never substitutes U+FFFD bytes.len(b) -> int bytes.get(b, i) -> Option bytes.slice(b, start, end?) -> Bytes // a copy; bounds clamp bytes.concat(a, b) -> Bytes bytes.join(parts: Array) -> Bytes bytes.equals(a, b) -> bool // by value; `==` compares references bytes.index_of(haystack, needle) -> Option bytes.starts_with(b, prefix) -> bool bytes.to_hex(b) -> string / bytes.from_hex(s) -> Result bytes.to_base64(b) -> string / bytes.from_base64(s) -> Result bytes.to_base64url(b) -> string / bytes.from_base64url(s) -> Result bytes.to_base32(b) -> string / bytes.from_base32(s) -> Result ``` Every decode returns a `Result` and names the offending position in `index`, because node's `Buffer` accepts malformed input silently: `Buffer.from("zz", "hex")` is an empty buffer and no error. There is no integer codec; a big-endian read is ordinary arithmetic over `bytes.get`, and past 32 bits a shift is wrong anyway, so divide. The module touches no host API, so it runs in a Web Worker. ### std/net TCP. Events are individual functions, as in `std/websocket`; there is no event-name string to misspell and no callback parameter to narrow. ``` type Socket type Server / type ServerError = { kind, message, code } net.listen(host, port, on_connection) -> Result // async; resolves when BOUND net.stop(s) / net.port(s) / net.on_stop(s, fn() {}) / net.on_server_error(s, fn(e) {}) net.connect(host, port) -> Socket net.on_connect(s, fn() {}) / on_text(s, fn(text) {}) / on_data(s, fn(bytes) {}) net.on_close(s, fn() {}) / on_error(s, fn(message) {}) net.send(s, text) / net.send_bytes(s, data) net.close(s) / net.destroy(s) / net.no_delay(s, enabled) net.peer_address(s) -> Option / net.peer_port(s) -> Option ``` `listen` resolves when the port is **bound**, so a port already in use is an `Err` you match rather than a throw, and `e.kind` is `"in_use"`/`"denied"`/ `"unavailable"`/`"other"` rather than a string to scrape. A server is a resource: `stop` it, `on_stop` says when it stopped, stopping twice is a no-op. `host` has no default: `"127.0.0.1"` is local-only, `"0.0.0.0"` is the network. There is no `serve`; it resolved only on close, nothing could close it, and every caller was forced to write a `match` arm that could never run. Use `on_text` for a text protocol: it holds a decoder per socket, so a character split across two packets survives, where decoding each chunk alone would produce two replacement characters. Use `on_data` for binary. TCP has no message boundaries, so carry your own delimiter; two `send`s can arrive as one read. A server cannot be stopped once started. ### std/url Parse, build and resolve URLs, over the host's WHATWG parser. No node dependency. A `Url` is a record because its parts are data. ``` type Url = { scheme, host, port: Option, path, query, fragment: Option } type Param = { key: string, value: string } url.parse(text) -> Result // absolute only url.join(base, relative) -> Result // resolves ../ and //host url.format(u) -> string url.query_params(query) -> Array // in order, repeats kept url.query_param(query, name) -> Option url.to_query(params) -> string url.encode_component(text) -> string url.decode_component(text) -> Result ``` Use it rather than splitting the string: `https://evil.com@example.com/` has host `example.com`, and a hand-rolled parser gets that wrong in the direction that matters. `query_params` is an array because `?tag=a&tag=b` is legal. `join`'s `Err` comes from the *base*, not the relative reference. The WHATWG parser resolves anything that is not itself a URL as a relative path, so `join("https://x.test/feed.xml", ":::")` is `Ok(https://x.test/:::)`. Only a `base` the parser cannot make sense of fails: `join("not a base", "/x")` is `Err(cannot resolve "/x" against "not a base")`. Do not expect an `Err` arm to catch a malformed link; it never will. ### std/dns ``` type MailHost = { priority: int, host: string } dns.lookup(hostname) -> Result // async; asks the OS, sees /etc/hosts dns.ipv4(hostname) / ipv6 / text -> Result, string> // async; queries DNS directly dns.mail(hostname) -> Result, string> // async ``` `lookup` before dialling, `ipv4` when the question is about the record itself. `text` joins the 255-byte chunks DNS splits a long TXT value into. ### std/tls ``` tls.connect(host, port, timeout_ms) -> Result // async; Ok means the certificate was accepted; the deadline is required ``` A TLS socket is a `net.Socket`, so `std/net`'s functions all apply. Verification is on and cannot be turned off. Resolves after the handshake, so the failure arrives before there is anything to write. No TLS server; terminate in front. ### std/process ``` process.args() -> Array process.exit(code) -> never process.set_exit_code(code) -> void // record the code, keep running process.exit_code() -> number // what it will currently leave with process.env(name) -> Option process.cwd() -> string ``` ### std/record ``` record.get(r, key) -> Option // absence-aware read record.has(r, key) -> bool record.keys(r) -> Array record.values(r) -> Array record.set(r, key, value) -> Record // returns a new record record.remove(r, key) -> Record ``` ### std/random A seeded, reproducible PRNG (mulberry32), not cryptographic (use `std/crypto` for security-sensitive randomness): ``` type Rng seeded(seed: number) -> Rng // a generator fixed by the seed rng.next() -> number // method: next float in [0, 1) rng.int(lo: number, hi: number) -> number // method: whole number in [lo, hi) rng.bool(probability?: number) -> bool // method: true with this probability (default 0.5) rng.pick(items: Array) -> T // method: a uniform element ``` ### std/store A shared-state primitive. A `Store` holds a value; create one at module scope (`const s = create(initial)`) so many functions share it without threading a `let` through `main`. The binding stays `const`, so every mutation is a greppable `s.set(...)`/`s.update(...)`: ``` type Store create(initial: T) -> Store // a store seeded with initial store.get() -> T // method: read the current value store.set(next: T) -> void // method: replace it store.update(change: fn(T) -> T) -> void // method: map it ``` An empty-collection seed can't infer its element type, so pass an explicit type argument: `const tasks = create>([])`. ### std/task Structured-concurrency helpers over promises. Pass task thunks (`fn() -> T`, usually `async`); `all` is fail-fast, `all_settled` keeps every outcome so a partial failure never loses the successes: ``` type Settled // { ok: true, value: T } | { ok: false, error: unknown } all(tasks: Array T>) -> Array // concurrent, joined in order (fail-fast) race(tasks: Array T>) -> T // first task to settle pool(limit: number, tasks: Array T>) -> Array // at most `limit` in flight (fail-fast) all_settled(tasks: Array T>) -> Array> // one outcome per task, never rejects pool_settled(limit: number, tasks: Array T>) -> Array> // bounded, never rejects ``` Each is `async`, so `await` the result. JavaScript can't force-cancel a running task, so a failure in `all`/`pool` abandons its siblings' work rather than halting it; the `_settled` forms exist for when you want every outcome instead. Read a failed outcome's `unknown` with `string.from(e)`. ### std/set A hash set with value semantics for primitives; maps use the built-in `Record`. Like `std/store`, state lives in a closure and every mutation is a greppable method call: ``` type Set create(initial?: Array) -> Set // an empty or seeded set set.add(value: T) -> void // method: insert set.has(value: T) -> bool // method: membership set.remove(value: T) -> bool // method: delete (was it present) set.size() -> number // method: cardinality set.values() -> Array // method: members as an array unique(values: Array) -> Array // de-duplicate, order-preserving ``` ### std/path Cross-platform filesystem paths over node's `path`; the host separator is respected, so the same code runs on Unix and Windows: ``` join(parts: Array) -> string // join segments dirname(p: string) -> string basename(p: string) -> string extname(p: string) -> string is_absolute(p: string) -> bool normalize(p: string) -> string relative(from: string, to: string) -> string ``` ### std/crypto Hashing, HMAC, and randomness over node's `crypto`. Each algorithm comes in two forms: the plain name takes a UTF-8 string and returns lowercase hex; the `_bytes` form takes and returns `Bytes` from `std/bytes`, for a key or wire protocol that is arbitrary octets rather than text: ``` sha1(input: string) -> string // legacy protocols only (RFC 4226/6238 OTP, WS handshake) sha256(input: string) -> string sha512(input: string) -> string sha1_bytes(input: Bytes) -> Bytes sha256_bytes(input: Bytes) -> Bytes sha512_bytes(input: Bytes) -> Bytes hmac_sha1(key: string, input: string) -> string hmac_sha256(key: string, input: string) -> string hmac_sha512(key: string, input: string) -> string hmac_sha1_bytes(key: Bytes, input: Bytes) -> Bytes hmac_sha256_bytes(key: Bytes, input: Bytes) -> Bytes hmac_sha512_bytes(key: Bytes, input: Bytes) -> Bytes random_uuid() -> string // a v4 UUID random_hex(count: number) -> string // count random bytes, hex (length count * 2) random_bytes(count: number) -> Bytes // count random octets, for a key or a nonce timing_safe_equal(a: Bytes, b: Bytes) -> bool // compare a secret without leaking where it differs ``` A key from `bytes.from_base64` or `crypto.random_bytes` should use the `_bytes` form: routing real key octets through a string replaces any byte that is not valid UTF-8, so the string form computes a different MAC than the specification says. Verify anything an attacker supplies against a secret with `timing_safe_equal`, not `bytes.equals`, which returns as soon as it finds a mismatch and so leaks timing. ### std/math Numeric helpers over JavaScript's `Math`: ``` math.PI, math.E // constants math.abs(x: number) -> number math.floor(x: number) -> number math.ceil(x: number) -> number math.round(x: number) -> number math.trunc(x: number) -> number math.sqrt(x: number) -> number math.sign(x: number) -> number // -1, 0, or 1 math.min(a: number, b: number) -> number math.max(a: number, b: number) -> number math.pow(base: number, exponent: number) -> number math.imul(a: number, b: number) -> number // 32-bit integer multiply math.clamp(x: number, lo: number, hi: number) -> number ``` ### std/encoding base64, base64url, and hex text encodings, for when both sides are strings. When either side is octets, the codecs are on `std/bytes` (`bytes.to_base64`, `bytes.from_hex`), which also covers base32 and refuses malformed input instead of skipping it. These six do not: `base64_decode("!!!")` is `""` with no error, and a decode that is not valid UTF-8 comes back with U+FFFD substituted: ``` encoding.base64_encode(s: string) -> string encoding.base64_decode(s: string) -> string encoding.base64url_encode(s: string) -> string // URL-safe alphabet, no padding encoding.base64url_decode(s: string) -> string encoding.hex_encode(s: string) -> string encoding.hex_decode(s: string) -> string ``` ### std/log Structured (JSON-line) logging. Each call emits one JSON object with `level`, `msg`, and a timestamp to stdout (info/debug) or stderr (warn/error): ``` type Level // "debug" | "info" | "warn" | "error" log.debug(message: string) -> void log.info(message: string) -> void log.warn(message: string) -> void log.error(message: string) -> void log.with_fields(level: Level, message: string, fields: Record) -> void ``` ### std/collections Ordered collections beyond `Array`/`Record`. A `Deque` is a double-ended queue; ends that may be empty return `Option`: ``` type Deque deque(initial?: Array) -> Deque dq.push_back(value: T) -> void // method dq.push_front(value: T) -> void // method dq.pop_back() -> Option // method dq.pop_front() -> Option // method dq.peek_front() -> Option // method dq.peek_back() -> Option // method dq.len() -> number // method dq.values() -> Array // method ``` ### std/timers Run something later, or repeatedly, and stop it again. Scheduling is a global in JavaScript, and Glyph resolves imported module names rather than ambient globals, so this module is the only way to reach it. A pending timer keeps the process alive, which is what makes a scheduled program a program rather than a script that exits: ``` type Timer // opaque; give it to cancel timers.after(delay_ms: number, handler: fn()) -> Timer // once timers.every(interval_ms: number, handler: fn()) -> Timer // until cancelled timers.cancel(timer: Timer) -> void // safe to repeat timers.unref(timer: Timer) -> Timer // stop holding the process open timers.sleep(delay_ms: number) -> Promise // for `await` ``` `unref` opts a background tick out of holding the process open, and `cancel` on a timer that already fired (or was already cancelled) does nothing, so teardown paths are safe to run more than once. ### std/websocket A WebSocket client and server. Each event is its own function taking exactly what that event carries, rather than the host's `addEventListener(name, handler)` with an event object whose useful field depends on the name, so no handler parameter needs narrowing and no event-name string can be misspelled: ``` type Socket // one connection, either end type Server // a listening server websocket.connect(url: string) -> Socket // ws:// or wss://; returns before it opens websocket.connect_with(url: string, protocols: Array) -> Socket websocket.protocol(socket: Socket) -> string // the subprotocol the server accepted, or "" websocket.listen(host: string, port: int, on_connection: fn(Socket) -> void) -> Result // async; resolves when BOUND; ServerError is std/net's websocket.stop(server: Server) -> void // graceful; safe to call twice websocket.port(server: Server) -> int websocket.on_stop(server: Server, handler: fn() -> void) -> void websocket.on_open(socket: Socket, handler: fn()) -> void websocket.on_message(socket: Socket, handler: fn(text: string)) -> void // text frames websocket.on_binary(socket: Socket, handler: fn(data: Bytes)) -> void // binary frames websocket.on_close(socket: Socket, handler: fn(code: number, reason: string)) -> void websocket.on_error(socket: Socket, handler: fn()) -> void websocket.send(socket: Socket, text: string) -> bool // false if not open websocket.send_bytes(socket: Socket, data: Bytes) -> bool // false if not open websocket.close(socket: Socket) -> void // safe to repeat websocket.is_open(socket: Socket) -> bool ``` A server connection is the same `Socket` as a client one, so `on_message`, `on_binary`, `send`, `send_bytes`, `close` and `is_open` work on either end, and `listen` has `net.listen`'s contract, including the explicit host and the structured `ServerError`. Text and binary frames are delivered to separate handlers, and a frame reaches exactly one: before 0.1.80 a binary frame was decoded as UTF-8 and delivered to `on_message`, which silently corrupts anything that is not a JSON protocol. The server implements RFC 6455 (handshake, masked client frames, fragmented messages reassembled before delivery, ping answered with pong); compression and a TLS (`wss://`) listener are not implemented, so terminate TLS in front. ### std/time `import std/time` buys the `time` namespace (`time.now()`, `time.sleep(d)`, and `time.Duration` as both a type and a factory). `import std/time { Duration }` buys the bare name `Duration`, also as both. They are independent, so code that writes `time.sleep(d)` and types values as `Duration` needs both lines: ``` import std/time import std/time { Duration } ``` ``` type Duration // Duration.ms(n), or time.Duration.ms(n) time.now() -> number // epoch milliseconds time.sleep(duration) -> void // async; await it time.debounce(delay, f) -> fn // returns a debounced function time.format_iso(epoch_ms) -> string // ISO-8601 UTC string (no need for a Date via extern_ts) time.parse_iso(iso) -> Option // epoch ms; strict ISO-8601 only (see below) time.add_days(epoch_ms, days) -> number time.add_hours(epoch_ms, hours) -> number time.year(epoch_ms) / month(epoch_ms) / day(epoch_ms) -> number // UTC; month is 1-12 ``` `parse_iso` takes a bare `YYYY-MM-DD` (UTC midnight) or a datetime with an explicit `Z`/`+HH:MM`/`-HH:MM` offset, and returns `None` for anything else. An offset-less datetime (`"2026-01-03T10:00"`) is rejected: ECMAScript reads it in local time, which would move the day the UTC accessors report. `"2026-1-3"`, `"January 5 2026"`, and an impossible day like `"2026-02-31"` are `None` too. ### std/stream and std/test (property testing) ``` type Stream stream.ints() -> Stream // 0, -1, 1, -2, 2, ... stream.bools() -> Stream stream.from(values) -> Stream test.property(predicate, gen, count?) -> Result ``` Property tests are deterministic (sampled by index, no RNG). Run them with `@example` (see Testing below). ### std/http (client + server) ``` type Request = { url: string, method: string, headers: Record, body: unknown, raw: string } type Response = { status: number, headers: Record, body: unknown, raw: string, url: string } type HttpError = { status: number, message: string, kind: "timeout" | "network" | "status" | "argument" } type Fetch = { url: string, method: string, body: Option, timeout_ms: number, redirect: "follow" | "manual" | "error" } // timeout_ms defaults to 30000; 0 means no timeout type Handler = fn(Request) -> Result // may be async http.get(url) -> Result // client; async, await it; 30 000 ms deadline http.post(url, body) -> Result // client; async; put/patch/del/head likewise http.head(url) -> Result // status and headers, no body http.fetch_of(url, method) -> Fetch // the verbs' defaults: timeout_ms 30000, redirect "follow", no body http.send(f: Fetch) -> Result // your own deadline and redirect policy; timeout_ms 0 opts out http.to_text(resp) -> Result // the body as the exact text received http.listen(host, port, handler) -> Result // async; resolves when BOUND // Server is std/net's: stop with net.stop http.json(status, body) -> Response // application/json response http.text(status, body) -> Response // text/plain response http.html(status, body) -> Response // text/html response http.redirect(status, location) -> Response // 30x with a `location` header http.with_header(resp, name, value) -> Response // a copy carrying one more header http.query(req) -> Record // parse the URL query string http.path(req) -> string // URL path without the query http.form(req) -> Record // parse an x-www-form-urlencoded body http.raw(req) -> string // unparsed request body, for HMAC signature verification ``` `req.body` is the parsed body; `http.raw(req)` is the unparsed bytes as received, what an HMAC signature must be verified over (re-serializing the parsed body changes whitespace and key order, so a recomputed signature would not match). `http.form(req)` parses that raw body as `x-www-form-urlencoded` (`+` is a space, percent escapes decode, a repeated key keeps the last value) without touching `req.body`. `Response.headers` is required and every constructor fills it in. The content type is inferred from the body only when the headers do not already name one. Every character Node refuses to write in a header is stripped from the value first, so a `location` built from user input can neither split the response (CR/LF) nor crash the server (anything above U+00FF). A client call reports the response headers it received, with the names lowercased. Every client verb gives up after 30 000 ms with `kind: "timeout"`; a peer that accepts and never answers is an `Err`, not a hang. To change the bound, build the request with `http.fetch_of(url, "GET")`, set `timeout_ms` on it (0 means no timeout, written at the call site so it is greppable), and `await http.send(req)`. A deadline above 2147483647 ms is refused with an `Err` of `kind: "argument"` naming the limit. `match e.kind` is exhaustive over all four kinds. A `Handler` returns `Ok(response)` for any status (a 404 is a normal `Ok`) or `Err(message)` (sent as a 500). `await http.listen(host, port, handler)` binds the port and returns a `Server`; the process stays alive on its own while a listener is pending (see the execution model below), so `main` returns normally. ### std/sqlite (persisted SQL over node:sqlite) ``` type Row = Record // a queried row: the untrusted boundary type Db // an open database handle open(path) -> Db // ":memory:" or a file path (persists) db.exec(sql) -> void // DDL / statements with no params or result db.run(sql, params) -> number // INSERT/UPDATE/DELETE; returns rows affected db.last_insert_id() -> number // last AUTOINCREMENT rowid on this connection db.query(sql, params) -> Array // rows come back as Record db.query_one(sql, params) -> Option // first row, or None db.close() -> void ``` A query returns rows as `Record`, so a row is a validated boundary like a request body: reach for `RowType.parse(row)` before trusting it, never a cast. SQLite has no boolean type (a flag column is an integer `0`/`1`), so model the storage shape and the domain shape as separate types and map between them. A full example is `examples/apps/tasks/main.glyph`. ### std/decimal (exact money math, no floats) ``` type Decimal decimal(text) -> Result // parse "10.50"; Err on malformed, never NaN from_int(units, scale) -> Decimal // from_int(1050, 2) is 10.50 zero: Decimal d.add(o) / d.sub(o) / d.mul(o) -> Decimal // methods; exact d.div(o, scale) -> Decimal // method; rounds half away from zero to `scale` digits d.round(scale) / d.neg() / d.abs() -> Decimal d.cmp(o) -> int d.eq(o) -> bool d.is_zero() -> bool d.is_negative() -> bool d.to_string() -> string d.to_number() -> number // to_number is lossy, display only ``` Use this for money, never `number`: JS `number` is a float (`0.1 + 0.2 != 0.3`) and loses precision past 2^53. Glyph has no operator overloading, so operations are methods (`price.add(tax)`). Construction validates and returns a `Result`. For exact large *whole* numbers (account ids, counters, 64-bit values) use the `bigint` prelude type, not `number`: write literals as `123n`, and a record field typed `bigint` validates `typeof === "bigint"` at the boundary (a JSON number is rejected, not silently truncated). `number` loses precision past 2^53; `bigint` does not. `tsc` keeps `bigint` and `number` apart (no mixed arithmetic). A `where` refinement types an *invariant* that a boundary must validate: `type Amount = int where value >= 0`, `type Rating = int where value >= 1 && value <= 5`, `type NonEmpty = string where value.length > 0`. The predicate (over a bound `value`) is woven into the type's descriptor, so `Amount.parse(x)` rejects a negative value, not just a non-number. The predicate runs wherever the type is used: a record field typed `Amount`, an `Array` element, an `Option` payload, a union variant's payload, and `json.parse`. So does the descriptor of a type imported from another Glyph module, so a field typed by an imported record is checked against that record and not just for presence. v1 refines primitive base types only (`int`/`number`/`string`/`bool`/`bigint`); a `where` on a record or union is a compile error, not a silent drop. ### std/taint (untrusted-input discipline as types) ``` type Tainted type Trusted taint(value) -> Tainted // wrap untrusted input sanitize(t: Tainted, clean: fn(T) -> T) -> Trusted // escape/validate, then trust trust_unchecked(value) -> Trusted // escape hatch (literals); greppable expose(t: Trusted) -> T // unwrap at the sink reveal_tainted(t: Tainted) -> T // read raw, only to inspect/sanitize ``` Type a sink's parameter `Trusted` (a SQL runner, a shell command, an HTML renderer) and a `Tainted` cannot reach it without going through `sanitize` first: `tsc` rejects the call. This is discipline enforced by types (you opt in per sink), not automatic flow analysis. SQL injection becomes a compile error. ## Importing external code (npm packages and Node builtins) A Glyph import path is emitted **verbatim** as the TypeScript module specifier: ```glyph needs-deps import react { useState } // emits: import { useState } from "react"; import http { createServer } // emits: import { createServer } from "http"; ``` So you import an npm package by its package name, and a **Node builtin by its bare name** (`http`, `fs`, `path`) — **not** `node:http` (the `:` is not a legal path character in a Glyph import; Node resolves the bare name to the builtin anyway). To give the type-checker types for an external module, drop an ambient declaration file under `/.types/`. Anything matching `/.types/**/*.d.ts` is auto-discovered and type-checked with your build. (Full guide with a worked example: `docs/guide/external-imports.md`.) To write **whole hand-written TypeScript** a Glyph module calls (an idiom Glyph can't spell, a node-stream loop, a `new Promise`), put the `.ts` under `/extern/` and import it as `import extern/`. The build stages `/extern/**` into the output and emits a relative specifier, so the file is type-checked (its exported types enforce your Glyph calls) and resolved at runtime, and a rebuild never prunes it. This is the only way a Glyph module imports a local `.ts`: relative imports are illegal (D15), so `extern/*` is the reserved, greppable path. (`extern_ts("...")` is the smaller escape hatch, for a single inline type or expression rather than a module.) `glyph run`'s build cache hashes every `.ts` and `.tsx` under `/extern/` by path and by contents, so editing, renaming, adding, or deleting a shim rebuilds and re-type-checks; symlinked shims are followed. A class-based client is instantiated with `new`, which Glyph has for exactly this interop and nothing else: `import kafkajs { Kafka }` then `let k = new Kafka({ clientId: "app", brokers: [b], })`. `new ()` emits a verbatim TypeScript `new` and is type-checked by `tsc` against the real constructor (a wrong argument is a real error; an undefined callee is E0103). Glyph has no `class` declarations of its own; `new` only constructs a type that comes from an npm package, a `.types` ambient declaration, or `extern_ts`. A factory-style client (`createClient()`, `createConnection()`) needs no `new` at all: call it directly. Worked example: ``` src/ main.glyph .types/ http.d.ts // declare module "http" { export function createServer(...): ... } ``` ```glyph needs-deps module main import http { createServer } // ... createServer is now typed from .types/http.d.ts ``` ## The execution model `glyph run` (and a built `main`) does, in effect: ```ts const code = await main(process.argv.slice(2)); process.exitCode = typeof code === "number" ? code : 0; ``` It **awaits `main` and assigns the exit code**; it does not force the process to stop. The process leaves when nothing is left to wait for, which is Node's own rule. A CLI that computes and prints has nothing pending and exits the moment `main` returns, with that code. A **long-running process** (a server, a watcher, a bot) therefore does not need `main` to stay suspended. Start the work and return: a listening socket, a `timers.every` interval, or a pending promise all keep the process alive on their own. That is why `http.listen` and `net.listen` resolve as soon as the port is bound rather than staying pending: there is nothing for `main` to wait on, and a server that runs forever is one nobody stopped. Two consequences worth knowing: - `main`'s `return 0` runs long before the first request arrives. It is the code the process will eventually leave with, not a statement about when it leaves. - Anything that fails **after** `main` has returned has to report itself, because `main`'s return value is already spent. `process.set_exit_code(1)` records a failure without tearing down work still in flight; `process.exit(1)` stops immediately. A listener that cannot bind its port is the usual case. ## Testing Tests live next to the code and run on build: ```glyph module examples @example double(21) == 42 pub fn double(n: number) -> number { n * 2 } ``` ```glyph module properties import std/stream import std/test import std/result { Ok } @example test.property(fn(n: number) -> bool { n + 0 == n }, stream.ints()) == Ok(void) pub fn identity_holds() -> bool { true } ``` They run on every `glyph build` (needs `tsx` on PATH; `--no-test` skips them). An `@example expr == expr` passes when both sides are structurally equal; a bare `@example expr` asserts the expression is `true`. A `@doc """..."""` block whose Markdown holds a `glyph @run` fence executes that fence too. A failing one fails the build, under `--json` too. **Limitation:** an `@example` that compares against a prelude constructor (e.g. `Ok`) must import it (`import std/result { Ok }`). ## Gotchas (read these once, save an hour) - **`bool`, not `boolean`.** The boolean type is spelled `bool`. - **`void` is a value and a type.** `-> void` is a valid return type, and `void` is a usable value (`Ok(void)`). - **The tail expression is the return value.** A non-`void` function or block returns its last expression; an explicit `return` is optional (both `{ n * 2 }` and `{ return n * 2 }` work). `return` is **not** mandatory. - **Object-literal shorthand is rejected.** Write `{ email: email }`, never `{ email }`. - **Every `match` arm needs a trailing comma**, including the last. - **Object keys may be quoted strings.** Use `{"Content-Type": x}` for keys that are not identifiers; an identifier key stays bareword (`{ plain: x }`). Object-literal *shorthand* is still rejected — always write the value. An interpolated key (`{"${e}": x}`) is not allowed (no computed keys). - **`mut` is narrow, but it is not a restriction on parameters.** It marks the four assignment and mutating-call forms above; there is no `mut` *declaration* modifier, so you cannot write `fn f(mut x: T)` or a `mut` field in a record. A parameter's fields are mutable in place: inside `fn drain(s: Stats)`, `mut s.energy = s.energy - 30` is legal and the caller sees it. You do not have to rebuild and return a whole record to change one field of it. - **Write a `//` comment on its own line, above what it documents.** `glyph fmt` keeps a comment above the same item, including inside a record, a union variant list, an array or object literal, an argument list, and a `match` (a construct holding an interior comment stays one-element-per-line so the comment has an item to sit above). A comment always lands on its own line, so one written at the end of a code line moves to the line above the next item. - **No `node:` import prefix.** Import Node builtins by bare name (`import http`). ### The five wrong forms, and the code each draws Each of these is a whole program that does not compile, and the diagnostic beside it is the one this compiler raises. They are compiled on every build of this document, so the pairing cannot go stale. Constructing a record by naming its type. Glyph has no `TypeName { ... }` form: ```glyph expect-error E0228 module wrong type Order = { id: string, } pub fn make() -> Order { return Order { id: "a" } } ``` Assigning without `mut`: ```glyph expect-error E0008 module wrong pub fn count(xs: Array) -> number { let total = 0 for x in xs { total = total + x } return total } ``` Reaching for `if`/`else`: ```glyph expect-error E0006 module wrong pub fn label(ready: bool) -> string { if (ready) { return "go" } else { return "wait" } } ``` Object-literal shorthand: ```glyph expect-error E0002 module wrong type User = { email: string, } pub fn make(email: string) -> User { return { email } } ``` Spelling the boolean type `boolean`: ```glyph expect-error E0103 module wrong pub fn ready(flag: boolean) -> boolean { return flag } ``` ## Diagnostic codes Every error and warning carries a stable code and a one-line fix. `glyph --explain ` prints the long form; `glyph build --json` emits them machine-readably. The full catalogue: | Code | Meaning | Fix | |---|---|---| | E0001 | Lexical error (unterminated string, invalid escape, stray character) | Fix the string/escape/character | | E0002 | Expected a different token (Glyph is stricter than TS) | Match the expected syntax | | E0003 | Unexpected token in this position | Remove or relocate it | | E0004 | Expected end of file (likely an unbalanced brace) | Balance your braces | | E0005 | Construct recognized but not implemented | Use a supported form | | E0006 | `if`/`else` used where Glyph has none (`match` is the only conditional; D3) | Rewrite as a `match` | | E0007 | Range or comparison pattern (`500..599 =>`) in a `match` arm; not in v1 | Enumerate the values as separate arms | | E0008 | Assignment without `mut` (`x = e` should be `mut x = e`, or `let x = e` for a new binding; D5) | Write `mut x = e`, or `let x = e` for a new binding | | E0009 | Retired. An object pattern's field takes any pattern, so `Full({ color: Black })` matches the field value; the code is no longer emitted | Nothing; `{ color: Black }` matches the field value | | E0010 | A union variant given more than one positional payload field, in a declaration (`Node(Color, Tree, int)`) or in a match arm's pattern (`Node(c, k)`); a variant carries one payload, and a multi-field payload is a record (D8) | One record payload: `Node({ left: A, right: B })` | | E0011 | A construct nested past the parser's 64-level limit. Recursive descent spends stack per level, so without the limit a deep enough file aborts the process with a stack overflow instead of reporting anything. A level is one construct the parser descends into, counted once however it is spelled: parentheses around an operand are that operand's level, not a second one, so `-(x)` nests as deep as `-x` | Pull the inner levels into `let` bindings | | E0100 | Duplicate top-level name | Rename one; names are unique | | E0101 | Relative import (`./`, `../`; D15). A module is named from the source root, with no package prefix: `std/io` for the stdlib, `helper` for a sibling file, `queries/report` for a file in a subdirectory. Two stages raise it: the parser, at a leading `./` or `../`, under the `parse` stage tag, and the resolver, at a `.` or `..` segment deeper in a path, under `collect`. Both carry the same help | Use an absolute module path (`std/io`, `myapp/x`) | | E0102 | Barrel file: only imports, no declarations (D15) | Add a declaration or remove the file | | E0103 | Unresolved name | Declare it, import it, or fix the spelling | | E0104 | Unresolved import: a local import naming no module under the project root. A local import path resolves from the project root, the nearest directory holding a `package.json` with a `"glyph"` key, else the directory passed to `glyph build`/`glyph run` (D15/D41), not from the importing file's directory. When a file with that name exists elsewhere under the root the message says where, and when it belongs to a different project the message says that instead | Check the path / that the module exists | | E0105 | Name not exported by the imported module (reported for a named import, `import lib { Secret }`, and for a name written through a namespace import, `import lib` plus either a `lib.Secret` annotation or a `lib.secret()` call) | Check the export name | | E0106 | Unused import (warning) | Remove it | | E0107 | Unused variable binding (warning) | Remove it, or prefix the name with `_` | | E0108 | Unreachable code after `return`/`break`/`continue` (warning) | Remove the dead code | | E0109 | A TypeScript reserved word (`class`, `new`, `switch`, `eval`, ...) used as a declaration, parameter, or binding name | Rename the declaration or binding | | E0110 | A top-level declaration whose name shadows a global the emitted module depends on (`Error`, `Number`, `Object`, `Array`, `Promise`, `Record`, or a prelude name such as `number`, `par`, `print`, `string`, `Issue`) | Rename the declaration | | E0111 | `type Key = string \| number`: bare primitive names on the right of `\|` declare tagged-union variants, not a union of those types | Name each case, or `extern_ts("string \| number")` | | E0112 | A module with no `pub` declaration, no `main`, and no `import` anywhere in the project naming it: nothing in it is reachable (warning) | Mark what callers need `pub`, or delete the module | | E0113 | A `module` line starting with `std` or `extern`: both prefixes are the compiler's own (D15), so nothing can import this file | Rename the module so its first segment is neither `std` nor `extern` | | E0200 | Non-exhaustive `match` on a tagged union (yours, a prelude `Result`/`Option`, or a stdlib one such as `fs.ErrorKind`), or a string-literal union (`"free" \| "pro"`, D30) missing a literal. Either kind counts whether it is declared in this module or imported from a sibling. A union that takes type parameters counts the same as its bare form, whether the matching module declares it or imports it, and whether the argument is an open type parameter or a concrete instantiation (`tree.Tree`). What the imported path still does not do is look inside a variant's payload, so `B(X)` over an imported union whose payload is itself a union is not counted (`docs/dogfooding-gaps.md` G143) | Handle every variant, or add an `else` | | E0201 | `?` used outside a `Result`-returning function | Return `Result`, or handle with `match` | | E0202 | `?` applied to a non-`Result` operand | Drop the `?`, or return a `Result` | | E0203 | `?` error type does not match the function's `E` (no `From` in v1) | `.map_err(...)` to line the error types up | | E0204 | Type mismatch | Make the value and the expected type agree | | E0205 | `owned` used on a non-`resource` type (D25) | Mark the type `resource`, or drop `owned` | | E0206 | `owned` resource not consumed on every path (D25) | Consume it (move to an `owned` param) on all paths | | E0207 | `owned` resource used after it was consumed (D25) | Reorder so uses precede the consume | | E0208 | Non-exhaustive `match` on an array (length not covered) | Cover the length, or add a catch-all | | E0209 | Non-exhaustive `match` on a `bool` | Cover `true` and `false`, or add `else` | | E0210 | A field a record type does not declare: a field access (`x.rowz`), or a JSX attribute naming a field of a component's props record. Includes a record declared in a sibling module under any import spelling, where the message names that record's own type | Fix the field name / add it to the type | | E0211 | Call argument type does not match the parameter type. A JSX attribute on a component element counts: the element passes one props object, so the attribute is checked against the props record's field of that name | Pass a value of the expected type | | E0212 | `mut` reassigns a `const` binding (D20) | Use a function-level `let` | | E0213 | Wrong number of call arguments | One argument per parameter | | E0214 | Component declared with multiple parameters (use a props record) | Take a single props record | | E0215 | Aliasing an `owned` handle (D25) | Consume it directly, don't rebind | | E0216 | Unreachable `match` arm after a total pattern (D9) | Remove it, or move the catch-all last | | E0217 | Discarded `Result`. A warning rather than an error: its `Err` is silently ignored | `match`/`?` it, or `let _ = ...` to say it's intentional | | E0218 | Non-exhaustive `match` on `number`/`string` (no catch-all for the unbounded rest; a bounded string-literal union is E0200 instead, including one imported from another module) | Add an `else` arm | | E0219 | `@redact` names a field the type does not have (D24) | Fix the field name | | E0220 | A `match` arm's PascalCase head is not a variant of the scrutinee's union (a typo or wrong-union variant, escalated with a nearest-variant suggestion instead of being read as a silent binding catch-all; covers the bare `Loadign`, payload `Loadign(x)`, and qualified `Feed.Loadign` shapes, for a union declared in the module and for one reached through a namespace import (`model.Loadign`); D9) | Fix the spelling (a `did you mean` suggestion is offered), or add the variant | | E0221 | Unknown `@annotation` (D27); the recognized set is `@example`, `@doc`, `@redact`, `@open`, `@pure`, `@public` | Use a recognized one: `@example`, `@doc`, `@redact`, `@open`, `@pure`, `@public` | | E0222 | `await` outside an `async fn` (the innermost enclosing callable decides, so a sync lambda inside an `async fn` is flagged) | Mark the enclosing callable `async fn` (a sync lambda is its own context) | | E0223 | A `match` arm produces no value while the `match` is used as a value (an empty block, or a block whose tail is a `let`/`mut`/`for`/`loop`) | End the arm with an expression, or `return` from it | | E0224 | Reading a key out of a `Record` map (`m.name` or `m[k]`), where the key may not be there. Use `record.get`, which returns `Option` | `record.get(m, k)` returns `Option`; `record.has(m, k)` tests for it. Writing (`mut m[k] = v`) is fine | | E0225 | A field of a parameter is read before an `await` and written after it, so a concurrent write in between is lost. Move the read after the `await` | Move the read after the `await`. A local counter across an `await` is fine and is not reported | | E0226 | A `match` whose scrutinee has no variant set to count against, where every arm's pattern can fail and no arm is a catch-all. Add an `else` | Add an `else` arm | | E0227 | `Nullable` where `T` is itself `Nullable` or `Option` (D45). Two states would share one runtime spelling, or a tagged object would sit under a null-tolerant field. Write `Nullable` over the plain type | Write `Nullable`; convert to an `Option` inside the program with `nullable.to_option` | | E0228 | A type's name where a value is expected (`return Order { id: "a" }`). Glyph has no `TypeName { ... }` construction form: the value is written on its own and the annotation carries the type. The message names the construction the declaration has, so a record lists its fields, a tagged union its variants, a string-literal union its literals. The receiver of a type's own descriptor (`Order.parse`, `Order.is`) is not this error | Write the value on its own (`return { id: "a" }`) and let the annotation carry the type. `Order.parse(raw)` and `Order.is(v)`, the descriptor forms, are not this error | | E0229 | A component's one parameter is not a record (`component Greeting(name: string)`). React calls a component with the one props object its element builds, so the parameter holds that object whatever it declares: an element with attributes fails `tsc` and `` hands it `{}`. Raised for a primitive, a string-literal union, a tagged union, an `Array`, a `Nullable` or a function type; a generic parameter, `unknown` and `Record` draw nothing | Take one props record: `component Greeting(props: { name: string })`, then read `props.name`; a function called directly rather than rendered is an `fn` | | E0300 | Construct not supported by the v1 TypeScript emitter | Use a supported form | | E0301 | An `` that is not the immediate sibling of its `` (D6) | Move the `` next to its `` | | E0302 | `?` in an arm of a `match` nested inside a larger expression (bind the match first) | Bind the match first (`let x = match ...`), then use `?` | | E0303 | `?` in a position with nothing to hoist the unwrap into, such as a `match` scrutinee (bind the operand first) | Bind the operand first (`let r = f(x)?`), then use `r` | | E0304 | `parse`/`is` on a record holding a field whose type has no runtime check (a host handle, an `extern_ts` type, a generic tagged union); declaring the record is fine | Split the wire type from the domain type: parse the checkable fields, then build the record | | E0305 | Two arms of one `match` lower to the same `case` label, so the later arm can never run | Remove the later arm, or give the two arms patterns that test different values | | E0310 | `glyph run` on a module with no `fn main` (it's a library — nothing to run) | Add `fn main`, or `glyph build` it as a library | ### A diagnostic in the self-correction loop `glyph build --json` and `glyph check --json` give you the machine-readable version an agent can act on directly, and the `glyph_diagnostics` MCP tool answers with the same object from the same Rust type. A program that forgets a `match` arm over a union declared in `orders`: ``` $ glyph check --json --no-tsc { "ok": false, "errors": 1, "diagnostics": [ { "code": "E0200", "severity": "error", "stage": "typecheck", "message": "non-exhaustive match on `OrderStatus`: missing variants `Paid`, `Cancelled`", "help": "Add an arm for each missing variant, or an `else` arm to catch the rest.", "note": "Tagged unions are sealed (D9): adding a variant forces every match to be updated. A `_`/`else` catch-all is allowed but forfeits that guarantee.", "file": "main.glyph", "module": "main", "range": { "start": { "line": 6, "col": 10, "offset": 119 }, "end": { "line": 8, "col": 4, "offset": 158 } }, "entity": "main::describe", "cause": "orders::OrderStatus", "expected": null, "actual": null, "alternatives": null, "alternatives_kind": null, "related": ["Pending", "Paid", "Cancelled"], "union": { "kind": "declaration", "module": "orders", "name": "OrderStatus", "declaration": "orders::OrderStatus" }, "missing_fields": null, "missing_variants": ["Paid", "Cancelled"], "explain": { "command": "glyph --explain E0200", "docs": "https://github.com/chadetov/glyph/blob/main/docs/error-codes.md#e0200" } } ] } ``` Read `code` + `help`, add the missing arm, rebuild. That is the loop the design is built for, and the fields beside them are there so the next step needs no second call: - `file` is a path under the root the module keys are counted from, so you can open it. `module` is the key `glyph_symbol`, `glyph_variants` and `glyph_impact` take. - `entity` is the declaration the diagnostic sits in. `cause` is the symbol at fault, which is a different thing: for `o.totl` inside `fn main`, `entity` is `main::main` and `cause` is `orders::Order`. - `expected` and `actual` are the two types, as the checker displays them, on every code that compares two (E0203, E0204, E0211). - `alternatives` is what may legally stand where the wrong thing stands, when the compiler holds a finite list: a record's own fields against a field typo, a union's variants against a mismatch, a module's exports against an unknown import, the did-you-mean a mistyped pattern head gets. - `alternatives_kind` says which of those four it is: `fields`, `variants`, `literals` or `exports`. The names alone do not say, because `Paid` and `read` are both bare identifiers on the wire and writing the first means constructing a variant while writing the second means `"read"`. The compiler sets it where it builds the list, so it is right for a union written inline as well as for one with a name. It is null exactly when `alternatives` is. - `related` is the union's whole variant list on a non-exhaustive match; `missing_variants` is the gap inside it. - `missing_fields` lists the required fields a record literal or a component element left out, on the E0204 or E0211 that refused it, in the declared record's order. A field declared with `?` is never listed. - `explain` says where the code's own explanation is. `glyph --explain E0200 --json` returns it as data, with the compiled counter-example that draws it. Every one of these keys is always present. A fact the compiler does not hold arrives as an explicit `null`, never as a missing key, so absence has one spelling on every code and on both surfaces. ### `glyph check --agent`: the same object, plus what the next edit needs `--agent` prints the `--json` object with two more keys on every diagnostic, so the edit that answers a diagnostic needs no second call. `constraints` are the invariants an edit repairing this diagnostic has to keep. They are not the repair. Each one is something the compiler already enforces, written out so the edit that satisfies the diagnostic does not break the guarantee the diagnostic existed to protect. For the `E0200` above: ```json "constraints": [ "preserve exhaustiveness: this match must name every case of `OrderStatus`, so add one arm for each of `Paid`, `Cancelled`.", "do not add an `else` arm: `OrderStatus` is declared in this project, and a catch-all forfeits the guarantee that adding a variant to it later forces this match to be updated (D9)." ] ``` `E0204` and `E0211` say not to cast, and name the type the position requires. `E0210` says the record's fields are closed and lists them. `E0206` and the other `owned` codes say to keep the consume. A code the compiler holds no such invariant for gets an empty list rather than a sentence written to fill it. `symbols` is the `glyph_symbol` answer for every symbol the diagnostic names: the declaration at fault (`cause`), the declaration it sits in (`entity`), and the types in `expected`/`actual` when they name a declaration. For the `E0200` above, `symbols` carries `orders::OrderStatus` with each variant's name, payload and construction syntax, which is what a pattern has to match. A symbol the tool refuses is listed in `symbols_absent` with the tool's own reason, so "we did not look" and "we looked and it is not there" stay distinguishable. ### `glyph fix`: the repairs the compiler fully determines `glyph fix [path]` writes only what the compiler settles on its own: - unused imports are dropped, whole or name by name (`E0106`); - a non-exhaustive match gains one arm per missing case (`E0200`), with the pattern the union's declaration implies and a body marked `TODO(glyph fix)` that prints the case and exits; - an arm head that is not a variant takes the checker's suggestion (`E0220`), and only when that variant is one the match is actually missing; - a field the record does not declare takes the one declared field a character away from it (`E0210`). Nothing here guesses. A rule that cannot settle the answer says so and why, on the same output as what it applied: a union no project declares has no payload shapes to write patterns from, two fields a character away from the typo are two, not one. The `E0200` arms compile, and they are written to be replaced: run `glyph fix`, then fill in the bodies it marked. ## Recipes by task (copy, adapt) - **read a file:** `import std/fs` then `match fs.read_text(path) { Ok(t) => ..., Err(e) => ... }` - **parse untrusted JSON to a type:** `let v = json.parse(body)?` then `T.parse(v)` (validates structure and leaf values) - **HTTP GET + decode:** `let r = await http.get(url).map_err(fn(e) { e.message })?` then `T.parse(r.body)` - **serve HTTP:** `import std/http { serve, text, Request, Response }`; handler returns `Result` via `Ok(text(200, "..."))` - **branch:** `match x { A => ..., B => ..., }` (no `if`; every case, trailing comma) - **fail:** return `Result`; propagate with `?`, recover with `match` - **share state:** module-level `const s = store.create(init)`; `mut s.update(fn(v) { ... })` - **run concurrently:** `await task.all([fn() { a() }, fn() { b() }])` - **bounded concurrency:** `await task.pool(4, tasks)` runs the thunks with at most 4 in flight (fail-fast); `task.pool_settled(4, tasks)` keeps going past a failure - **regex:** `regex.matches(pat, text)`, `regex.find_all(pat, text)`, `regex.captures_all(pat, text)` for the groups of every match - **walk a directory:** `let names = fs.read_dir(dir)?` for entry names, `fs.is_dir(path.join([dir, name]))` to recurse, `let info = fs.stat(p)?` for size/mtime - **hash / uuid:** `crypto.sha256(s)`, `crypto.random_uuid()` - **binary data:** `import std/bytes`; `fs.read_bytes(p)?` for a file that is not text, `bytes.from_base64(s)?` / `bytes.to_hex(b)` to cross to text - **HMAC over real octets:** `crypto.hmac_sha256_bytes(key, msg)` (the string form loses any byte that is not valid UTF-8); compare with `crypto.timing_safe_equal` - **paths:** `path.join(["a", "b"])`, `path.extname(p)` - **cleanup:** `defer handle.close()` (runs on every exit path) - **generic bound:** `interface Named { fn name() -> string }` then `fn f(x: T)` - **hide a helper:** omit `pub` (module-private by default); `pub` to export ## Where to go deeper - Five-minute tour: `docs/guide/tour.md` - For TypeScript developers (deltas + gotchas): `docs/guide/for-typescript-developers.md` - Tutorial (a todo CLI): `docs/guide/tutorial.md` - Full standard-library reference: `docs/reference/stdlib.md` - Language spec: `docs/language/spec.md` - Error codes and fixes: `docs/error-codes.md` - Editor setup: `docs/guide/editor-setup.md`