# Wantzel: the full reference, for machines > Wantzel is a compiled, strictly typed language with Pascal-like spelling: strict, fast and verbose, with no dependencies. The compiler is one executable with the standard library inside and writes static Linux x86-64 programs. Its errors name the file, the line and the fact, so that an agent can act on them. Compiler version: 0.6.0 (released 2 October 2026) Target: Linux x86-64 (ELF) Source: https://wantzel.com/docs/ Generated: 2026-10-17 from the documentation of that version Contents: Language specification; Syntax; Command line; Library; Writing Wantzel; How-to; Conventions; Design. --- # Language specification The binding reference: what the compiler supports today. Everything here is checked by the compiler's own test suite. Planned extensions live in §9 and are only valid once they move here. Whoever implements something reads this file first; whoever changes the language updates this file in the same commit. Where the tour below and a numbered section seem to differ, the numbered section binds. Last updated: 2026-09-25 (`import` for the library, `include` always a file — §1b). ## A tour ``` import json; const MAX = 100; var table: array[1..MAX] of int; i, total: int; function square(n: int): int; begin return n * n; end; // works on any char array, with a bounds check against the length passed in function fill(b: array of char; c: char): int; var k: int; begin k := 0; while k < len(b) do begin b[k] := c; k := k + 1; end; return len(b); end; begin i := 1; while i <= MAX do begin table[i] := square(i); total := total + table[i]; i := i + 1; end; end. ``` **Types.** `int` (64-bit), `real` (64-bit IEEE), `char` (0..255), `bool`, `str` (literals), `array[lo..hi] of T` and `record`. No implicit conversions: `ord()`/`chr()` are required, an `int` is not a condition, `and`/`or`/`not` work only on `bool` (bits go through `band`/`bor`/`bxor`/`bnot`). No pointers, no pointer arithmetic — indirection goes through arrays whose bounds the compiler knows. Routines take up to ten arguments; an `array of T` parameter counts as two (address and length) and keeps its bounds check. **Safety.** The compiler enforces, while translating: mandatory declarations, every `forward` fulfilled, signatures compared, a function result never discarded, a `procedure` never returning a value. In the generated code, checked with file and line: ``` runtime error: array index out of range at examples/httpd.wz:42 ``` - every array index against both bounds; - division and `mod` by zero; - `chr()` outside 0..255, `schar()` and `scan()` outside the string or array; - a `function` that ends without `return`; - locals and arrays zeroed on every call — no undefined initial values. Not checked: integer overflow, and anything through `addr`/`sysN`. A guard only protects you if it can reach its verdict without trusting the value it inspects: every bound above compares against a compile-time constant or a length in the call frame, neither of which the caller can corrupt. The one bound read from the value is a `str`'s length (the eight bytes before its text), so an unassigned `str` is address 0, length 0, and `slen` answers 0 for it. Rule of thumb: **a zeroed variable is valid everywhere** — `int` 0, array all zero, `str` empty. Anything reached through `addr`, `view` or `sys*` is checked by nothing. **Compiled JSON schemas.** A strictly typed compiled language can do this where a dynamic stack cannot: describe a message shape, and the compiler makes a record type, a parser, a writer and a JSON Schema from it — no DOM, no reflection, no allocation. ``` type Rpc = schema jsonrpc: text; id: json?; // '?' makes the field optional method: text of ("initialize", "tools/list", "tools/call", "ping"); params: json?; end; var msg: Rpc; ... if Rpc.parse(buf, 0, n, msg) < 0 then ... // invalid, or a field is missing if msg.method = Rpc.method.tools_call then ... // the enum is an integer if msg.id_ok then ... // was the field there? ``` A field can carry the JSON key it corresponds to, as a string literal before the colon — needed because identifiers are case-insensitive (`rdX`/`rdx` are one name in Wantzel, two on the wire) and a key like `content-type` or `$schema` is not a valid identifier at all: ``` type Reg = schema rdx: int; rdxcap "rdX": int; // the same letters, a different key ctype "content-type": text; // not a name any language would accept end; ``` Field types: `int`, `real`, `bool`, `text` (view), `text[N]` (copy), `text of (...)`, `json`, arrays of those and nested schemas, each with `?` for optional and a `"description"` for `Rpc.jsonschema`. A `tools` block turns a declaration into a complete MCP tool table — `tools/list`, argument parsing, dispatch, `structuredContent` — all from one place (see `examples/mcptools.wz` and §7b). ## 0. What kind of language is this? Small, strictly typed, procedural, reading almost like prose: `begin`, `end`, `procedure`, `:=`. A Pascal reader needs no introduction, but this is not an ISO-7185 or Free Pascal dialect — the deviations are deliberate (§8). Design rule: **one clear choice per concept, nothing exotic.** Familiar spelling where it costs nothing; where older languages offer several ways to say a thing, Wantzel picks one. Predictable for people and for a model generating code. ## 1. Program ``` import io; // a module of the standard library; include "x.wz"; reads a file (§1b) const MAX = 100; // int, char ('a'), bool, real (1.5, -2.5), str ("..."); also inside a routine var i, total: int; // only global or at the start of a routine buf: array[0..1023] of char; procedure greet; // routines: see §4 begin end; begin // main program greet; end. // the dot is required ``` Escapes in a `char`/`str` literal: `\n` `\t` `\r` `\0` `\\` `\'` `\"`, and `\xHH` — **exactly two** hex digits, upper or lower case. Two and not a variable number, deliberately: a hex escape that keeps eating digits (as in C) means a generator cannot write a byte then a literal hex character without changing what it wrote, so `"\x41BC"` is three characters. Anything else after a backslash is refused. Comments: `// to end of line`, the only form. Identifiers and keywords are **case-insensitive** (`Point` and `point` are the same name). Identifiers: letters, digits, `_`, and a **dot** as a namespace separator (`io.puts`, `mcp.buf`) — cosmetic: `import io;` reads source text, it does not open a namespace. First character must be a letter or `_`; a part **after** a dot may start with a digit (`Reading.level.1`, from a schema enum whose value is `"1"`). A real literal still reads as one number: `3.5` is a value, never a name. ## 1b. Imports and includes Two statements bring in more source, and they never overlap: ``` import io; // a module of the standard library of THIS compiler import json; // a name: no quotes, no .wz, no path import tools; // a keyword is allowed here: it names the module include "model.wz"; // a file next to this file include "../lex.wz"; // a file, relative to this file include "/abs/x.wz"; // a file, by absolute path ``` **`import name;`** reads module `name` from the standard library that is part of the running compiler (`wantzel --lib` lists them). Nothing on disk is consulted: not the working directory, not a `lib/` directory anywhere, no environment variable, no flag. The name is case-insensitive, like every name. **`include "path.wz";`** reads a file: the path is relative to the directory of the file that contains the `include`, or absolute when it starts with `/`. A name without a `/` is the file next to this one — never a library module, even when a module has that name. Both are **textual**: the text is read in at that point, once. A second `import` of the same module or `include` of the same file (compared by identity, not by spelling) is skipped. Order counts — a name is visible after the `import` or `include` that declares it, and `import tools;` belongs after the `tools` block (§7b). Imports and includes nest at most 16 deep. A module is never the same thing as a file: a copy of `lib/io.wz` included as a file and `import io;` are two different sources. **There is no way to replace a module.** The library is a property of the compiler version: the same source and the same compiler file give the same executable, wherever either sits. To experiment with a changed module, copy it and include the copy as a file. The wrong spellings are errors that carry the right one: | written | message | |---|---| | `import "io";`, `import "io.wz";`, `import io.wz;` | `import takes a module name, without quotes or .wz: import io;` | | `import "mine.wz";` (not a module) | `import takes a module name, without quotes or .wz; a file of your own is included: include "mine.wz";` | | `import jsn;` | `no library module 'jsn' in this compiler; wantzel --lib lists them -- did you mean: import json;` | | `include io;` | `include takes a file name in quotes: include "io.wz"; -- for the library module write: import io;` | | `include "io.wz";` with no `io.wz` beside the file | `io.wz is not a file next to this one; the library module is imported: import io;` | | `include "sub/x.wz";` that does not exist | `cannot open the included file: ` | | `io.puts` without `import io;` | `undeclared identifier: io.puts is declared in the library module io; add: import io;` | ## 2. Types Five scalar types, one composite type. | type | meaning | literal | |---|---|---| | `int` | 64-bit two's complement | `42`, `-7`, `0xFF` | | `char` | one byte, 0..255 | `'a'`, `'\n'`, `'\\'`, `'\''`, `'\x41'` | | `bool` | `true` / `false` | | | `real` | 64-bit IEEE-754 (C `double`, Python `float`); the only decimal type | `1.5`, `0.25`, `2e-3`, `6.02e23` (a dot or exponent makes a `real` literal; `1.` and `.5` are not literals) | | `str` | an **immutable string literal** (address + length); not a value you build up | `"text\n"`, `"caf\xc3\xa9"` | | `array[lo..hi] of T` | fixed array of `int`, `char`, `bool` or `real`; `lo`/`hi` constant ints; `lo` may be ≠ 0 | | **No implicit conversions.** `int` ↔ `char` via `ord()`/`chr()`; `int` → `real` via `real(i)`; `real` → `int` via `trunc(r)` (towards zero) or `round(r)` (nearest, halves to even). An `int` is not a condition. `and`/`or`/`not` work only on `bool`; bits are done with `band`/`bor`/`bxor`/`bnot`/`shl`/`shr`. `real` computes with `+ - * /` and unary `-`; `/` exists only for reals (`int` uses `div`), `div`/`mod`/`shl`/`shr` exist only for ints. Comparison with `= <> < <= > >=`; NaN equals nothing and orders nowhere. Division by zero gives `inf`/NaN as IEEE prescribes, not a runtime error. A literal with at most 18 significant digits and an exponent up to ±22 rounds correctly; beyond that the last bit may differ. Constants: `const PI = 3.14159; NEG = -2.5;`. No 32-bit type: `pack32(r): int` gives the IEEE single bits (for compact storage), `unpack32(i): real` gets them back. Text ↔ real is library. **Text as a value** is always an `array of char` (with `len()` as length, or a separate `int` for the filled part). A routine that takes text declares `s: array of char` and accepts a literal, an array, a slice or a view (§4). `str` is solely the type of a literal itself, for `slen`/`schar`/`sadr` and existing library routines; new code does not need it. Strings cannot be compared with `=`; compare per `char` or with `json.eq`. ### Records ``` type Vec = record x, y: real; end; Body = record id: int; pos, vel: Vec; // nested records tags: array[0..3] of int; // arrays in records label: array[0..7] of char; end; var bodies: array[1..3] of Body; // arrays of records b: Body; ``` - Fields via the dot: `b.pos.x`, `bodies[i].tags[0]`, `bodies[i].pos.y := 1.0`. - Copying a record as a whole: `b := bodies[2]` (both of the same type). - A record has no value of its own: not in an expression, not as a parameter, not as a function result. Pass records as `array of T` (a one-element slice suffices, §4) and let a routine fill a record through such an array. - `addr(b.pos)` and `len(b.tags)` work; `addr(bodies[2]) - addr(bodies[1])` is the record size. - Layout: fields in declaration order; `char`/`bool` fields and arrays lie on byte boundaries, everything else on 8 bytes; total size is a multiple of 8. A record may not contain itself. - `type` appears only at program level, before the routines using it. A type name is an ordinary identifier (case-insensitive) and may not coincide with a variable or routine. No pointers, sets, enumerations, strings with content, or nested arrays (`array of array`). See §9. ## 3. Expressions and operators Precedence, high to low, as in Pascal: 1. `not`, unary `-` 2. `*` `/` `div` `mod` `and` `shl` `shr` 3. `+` `-` `or` 4. `=` `<>` `<` `<=` `>` `>=` `div`/`mod` are Pascal semantics (truncation towards zero: `-7 div 2 = -3`, `-7 mod 2 = -1`), checked against division by zero. `and`/`or` on `bool` are **short-circuit** (`false and f()` does not call `f`). Ordering (`<` etc.) only on `int`, `char`, `real`. Overflow of `int` is not checked. ## 3b. Visibility: `local` Every name is global and the namespace is shared, so two files cannot both declare `hidden.n`. `local` in front of a top-level declaration makes the name visible **only in the file that declares it**: ``` local var hidden.n: int; // this file only local const HIDDEN.MAX = 64; local procedure hidden.bump; local function hidden.count: int; var shared.seen: int; // public, as before ``` - Applies to `var`, `const`, `procedure`, `function` at the top level; not to `type` or `schema`. - **Public is the default** — a declaration without `local` behaves as before, so existing source needs no change. - The unit is the **file**. No module system, no separate compilation, no nesting: `local` means "this file", nothing else. - Naming a local from another file is `undeclared identifier`, same as a name that does not exist — because from there, it does not. A `local` in front of a `const` block **inside** a routine is different and predates this: that one scopes to the call. Where you write it decides which is meant. ## 4. Routines ``` function kwadraat(n: int): int; begin return n * n; // 'return' with a value; required on every path end; procedure vul(b: array of char; c: char; n: int); // 'array of T' = address + length, with bounds check var i: int; begin i := 0; while i < n do begin b[i] := c; i := i + 1; end; end; function later(x: int): int; forward; // declare, define later ``` - Arguments are passed **by value**; an `array of T` parameter is a view (address + length) and writing into it is visible to the caller. No `var` parameters: use an array or a global. - A record variable may appear where `array of ` is expected — it arrives as a view of one element (`Point.parse(buf, 0, n, p)`). - Where `array of T` is expected, you may give: a whole array (`p(buf)`), a **slice** `p(buf[lo..hi])` (inclusive, like a declaration; `buf[3..2]` is empty; bounds checked), an array field of a record (`p(r.tags)`), an element slice of an array of records (`p(items[i..i])`, the way to pass one record), a **string literal** as `array of char` (`p("text")`), or `view(addr, n)`: `n` elements of type `T` at an arbitrary address (for `mmap`, kernel buffers, byte reinterpretation). `view` and slices exist only in that position; they are not values. - At most ten arguments; an array counts for two. - No nested routines; no recursion limit (recursion is allowed). - `function` must execute `return` on every path (runtime error otherwise); `procedure` may not return a value; a function result may not be discarded. - Local variables and arrays are **always zeroed** on entry. - A routine may have `const` and `var` blocks after the header, in any order; local constants have the same forms as global ones. - Callbacks: declare a routine `forward` in a library, define it in the application (`procedure app.request; forward;`). No function pointers. ## 5. Statements ``` x := e; // assignment p(a, b); y := f(a); // call if c then s1 else s2; // no ';' before 'else' while c do s; for i := a to b do s; // i is an ordinary int variable; b is computed once for i := b downto a do s; begin s1; s2; end; // block break; continue; // in while/for return; return e; halt(code); // terminate the program ``` `for` counts with step 1; after `break` the variable keeps the value it stopped on, after a complete loop it is one past the end value. No `case`: write an if-chain, and note that `else` binds to the nearest unclosed `if`. No `with`, no `goto`. ## 6. Built-in routines | | meaning | |---|---| | `ord(c)`, `chr(i)` | char ↔ int; `chr` checks 0..255 | | `real(i)`, `trunc(r)`, `round(r)` | int ↔ real | | `sqrt(r)` | square root (`sqrtsd`) | | `pack32(r)`, `unpack32(i)` | real ↔ IEEE single bits in an int | | `len(a)` | number of elements of an array (also of an `array of T` parameter) | | `addr(a[i])`, `addr(x)` | address as `int`, for syscalls | | `slen(s)`, `schar(s, i)`, `sadr(s)` | length, i-th byte (checked), address of a `str` | | `scan(a, from, upto, c)` | index of the first `c` in `a[from..upto)`, or `upto`; SIMD | | `view(addr, n)` | only as an array argument: `n` elements at address `addr` (§4); the only unsafe primitive besides `sys*` | | `band bor bxor bnot`, `shl shr` | bits | | `argc()`, `argch(k, i)` | number of arguments; i-th byte of argument `k` (`chr(0)` at the end, and for any `k`/`i` out of range — it answers rather than traps) | | `halt(code)` | exit | | `sys1(nr, a)` … `sys6(nr, a..f)` | raw Linux syscall | Library routines (`lib/`) are ordinary Wantzel: `io.*`, `net.*`, `http.*`, `json.*`, `mcp.*`, `fs.*` — see `library.md`. ### The syscall number is the contract `sys1`..`sys6` are builtins the compiler translates straight to machine code: no wrapper, no error handling, no errno translation. Arguments go into the System V registers, and the compiler emits the two bytes of the x86-64 `SYSCALL` instruction literally. The syscall number is an ordinary constant (`SYS.fork = 57`, `SYS.wait4 = 61` in `lib/io.wz`); the compiler knows no syscall by name. **Everything the operating system offers is reachable without touching the language** — a new capability is a constant plus a call, not a compiler change. `fork`, shared memory and `flock` were each called impossible here before someone checked. Nothing is checked on the other side: a wrong number or argument gives a negative return (`-errno`) or, worse, something that appears to work. Like `addr` and `view`, this is unsafe: no runtime check applies to it. ## 7. `schema` — compiled JSON A `schema` describes the shape of a JSON object. The compiler turns it into, in one go: a **record type** with the same name, `Name.clear`, `Name.parse`, `Name.write` and the constant `Name.jsonschema`. No DOM, no reflection — the parser reads straight from the input buffer. ``` type Point = schema x: real; y: real; tag: text of ("start", "mid", "end")?; // enum → int, with constants Point.tag.start ... end; type Path = schema id: int "unique id"; // a description goes along into the JSON Schema name: text[64]; // text as a copy (unescaped), at most 64 bytes note: text?; // text as a view (_at/_end) in the input buffer origin: Point; // nested schema pts: array[0..99] of Point; // arrays of int, real, bool, text[N] or schema keys: array[0..15] of text[32]; meta: json?; // arbitrary value as a view kind: text of ("open", "closed"); end; var p: Path; // a schema is an ordinary record type ... r := Path.parse(buf, 0, n, p); // position after the object, or -1 if r < 0 then ... if p.kind = Path.kind.closed then ... p.pts[i].x // ordinary field access p.name[0..p.name_n - 1] // the text copy as a slice p.keys[i * 32..i * 32 + p.keys_len[i] - 1] // element i of a text array n := Path.write(out, 0, p, buf); // JSON back from out[0]; the position after the object, or -1 when it does not fit; buf supplies the views io.puts(STDOUT, Path.jsonschema); // {"type":"object","properties":{...},"required":[...]} ``` Per field `f`, in the record: | field declaration | in the record | |---|---| | `int`, `real`, `bool`, enum | `f` | | `text` (view), `json` | `f_at`, `f_end` | | `text[N]` | `f: array[0..N-1] of char`, `f_n` | | `array[0..N-1] of T` | `f: array[0..N-1] of T`, `f_n` (number read) | | `array[0..N-1] of text[M]` | `f: array[0..N*M-1] of char`, `f_len[0..N-1]`, `f_n` | | nested schema `S` | `f: S` | | always | `f_ok` (key present), `f_null` (value was `null`) | Rules: - A missing required field (without `?`) makes `parse` return `-1`. - An **unknown key is ignored but recorded** — `parse` steps over it and succeeds; `json.ignoredn` counts skipped keys, `b[json.ignored0..json.ignored1)` is the first. This lets a caller send a superset of a schema — what a client written against a schema-validating server does — and refusing it would turn a harmless extra field into a failed call. The recording matters because a renamed or misspelt **optional** field would otherwise silently take its default with nothing saying so; a renamed **required** field still fails, since the field it should fill is absent. - A key the schema does declare is strict: a value it cannot hold gives `-1`, and `b[json.badkey0..json.badkey1)` names a key the parse rejected outright. - `null` sets `f_null` and `f_ok`. A text that does not fit `text[N]`, or an array with more than `N` elements, gives `-1`. An enum value outside the list gives `-1` in the field but no error. - `write` always writes required fields, optional ones only if `f_ok`; `f_null` writes `null`. `write` returns `-1` when the object does not fit in `dst` (or the start position is negative or past the end) and writes nothing usable — a short buffer is a **refusal**, not a truncation, because half a JSON object is a syntax error, not a shorter object. This differs deliberately from the appenders in `lib/` (`io.push`, `json.putraw`, `json.putstr`), which truncate at `len(dst)` and hand back a still-usable position — a shortened message is still a message. **Test the result of `write`**; a generated `tool.run` does, and reports the failure to the peer instead of dying on it. - A nested schema must be declared earlier; a schema name is a type name (no variable with the same name). - A `json` field in an output record can be filled by `Other.write` of another schema into the same view buffer: `r.base_at := tool.vn; tool.vn := Other.write(tool.vbuf, tool.vn, o, tool.vbuf); r.base_end := tool.vn;` (nesting one schema's output inside another's). - Descriptions after the type (and after `?`) appear as `"description"` in the JSON Schema; `text[N]` gets `maxLength`, arrays `maxItems`. ## 7b. `tools` — a tool table as a declaration A `tools ... end;` block declares an MCP tool table **once**. From each line the compiler generates the JSON Schemas, argument parsing, dispatch to your handler and result writing — the whole surface a hand-written MCP server otherwise builds one `mcp.add(...)` JSON fragment at a time. **This is the way to expose tools in Wantzel; do not build `tools/list` or a call reply by hand.** One declared line replaces tens of lines of hand-assembled JSON, and it stays correct as the schema changes, because the JSON comes from the same declaration the parser and dispatcher use. ### The smallest complete program ``` import json; type ConvertArgs = schema celsius: real "the temperature to convert"; end; type ConvertResult = schema fahrenheit: real; end; tools convert(ConvertArgs): ConvertResult "Convert Celsius to Fahrenheit." readonly idempotent; end; import toolsmcp; // AFTER the tools block: it reads it function tool.convert(a: array of ConvertArgs; r: array of ConvertResult): int; begin r[0].fahrenheit := a[0].celsius * 9.0 / 5.0 + 32.0; return 0; // 0 = success; otherwise: return tool.fail("message") end; begin mcp.stdio; end. ``` That is a complete MCP server over stdio. `tools/list` from that program, generated, not written: ``` {"name":"convert","description":"Convert Celsius to Fahrenheit.","inputSchema":{"type":"object","properties":{"celsius":{"type":"number","description":"the temperature to convert"}},"required":["celsius"]},"outputSchema":{"type":"object","properties":{"fahrenheit":{"type":"number"}},"required":["fahrenheit"]},"annotations":{"readOnlyHint":true,"destructiveHint":false,"idempotentHint":true}} ``` ### Line anatomy ``` convert(ConvertArgs): ConvertResult "Convert Celsius to Fahrenheit." readonly idempotent; // ^name ^input schema ^output schema ^description (→ JSON Schema) ^annotations ``` `name(ArgsSchema): ResultSchema "description" annotations;` — one line per tool, inside one `tools ... end;` block per program, placed after the schemas it uses. `ArgsSchema` and `ResultSchema` are ordinary `schema` types (§7); the same mechanism serves both directions. ### What you write versus what the compiler generates | you write | the compiler generates | |---|---| | the `tools ... end;` block | `tool.list` — the complete `tools/list` text: `inputSchema`, `outputSchema`, `description`, `annotations` | | | `tool.count` and `tool.id_` | | | per tool, the records `tool.in_` and `tool.out_` (from the schemas) | | | `tool.byname(b, at, upto)` — finds a tool by its `"name"` in a request buffer | | | `tool.run(idx, b, at, upto, dst)` — parses the arguments, calls `tool.`, writes the result JSON | | | `tool.fail(s)` — call it from a handler to refuse with a message | | `function tool.(a, r): int; ... end;` per tool (`forward`-checked: a missing handler is a compile error) | the dispatch that calls it from `tool.run` | | `import tools;` or `import toolsmcp;` | `app.tools`, `app.call` (MCP), and — `tools` only — `tool.rest(prefix)` (REST) | The module `tools` is two parts: `toolsmcp` (`app.tools`, `app.call`, MCP only, no HTTP) and the REST half, which brings `http` and with it the forward `app.request` the program must define. A server that speaks MCP over stdio only imports `toolsmcp` and needs no `app.request`. `tools` is a keyword: do not use it as a namespace in your own code. **A tool name becomes a routine name in the `tool.` namespace**, so it cannot be one the compiler or `lib/tools.wz` already uses there. `count`, `list`, `run`, `byname`, `fail`, `err`, `rest`, `vbuf` and `vn` are taken, as are names starting with `id_`, `in_` or `out_`; the compiler reports it at the tool's own line, e.g. `tool name "count" is reserved: the tools block generates tool.count; choose another name`. Choose a more specific name (`wordcount`, not `count`). **Annotations**: `readonly`, `idempotent`, `destructive` — each `false` by default; write the ones that apply, in any order, after the description. **Errors and status codes.** A handler returns `0` for success or `tool.fail("message")` to refuse. `tool.run` reports three distinct failures, kept apart because they have different owners: | `tool.run` returns | meaning | REST (`tool.rest`) | |---|---|---| | `-1` | the arguments did not satisfy the input schema | 422 | | `-2` | the handler refused with `tool.fail("message")` | 400 | | `-3` | the result JSON does not fit in `dst` | 500 | **Only `-1` gives 422.** Every other handler-side rejection gives 400, the same as any other rejection — a 422 means specifically "this JSON does not match the schema", never "the handler looked at valid arguments and said no". A handler rejecting on the content of an otherwise valid argument (an out-of-range number, an unknown id) still gets 400. `-3` gives 500: the request was valid and the limit is ours, so it is kept apart from `-1`, which would otherwise blame the caller for a limit on our side. **View fields: buffer and escaping.** A `text`/`json` field (a view) in the INPUT schema cannot be read by the handler. `tool.run` parses the call into `tool.in_` from the request buffer `b`, but the handler `tool.(a, r)` only ever receives the parsed record, never `b` itself — a view field is a pair of offsets into a buffer the handler cannot reach. Declare every argument field `text[N]` (a copy) instead; `int`, `real`, `bool`, an enum, a nested schema or an array of those are unaffected, since those never point outside the record. Only the input side has this restriction, because only the input side is parsed into a record a different routine then uses without the source buffer. For `text`/`json` fields (views) in the output schema, the compiler generates the buffer `tool.vbuf` (1 MB) and fill position `tool.vn` (0 at every call): the handler writes text into `tool.vbuf` from `tool.vn` and points `f_at`/`f_end` at it; `tool.run` writes the result with `tool.vbuf` as the source of the views. A handler that fills `tool.vbuf` to its limit is refused loudly: `tool.run` checks `tool.vn` right after the handler returns and, at or past `len(tool.vbuf)`, fails with `tool : reply exceeds 1048576 bytes` instead of writing a reply built from silently truncated text (`io.push` and `json.escslice`, the appenders a handler uses to fill it, stop at `len(tool.vbuf)` by their own contract rather than refuse). **A view field is written LITERALLY**, byte for byte, with no escaping (`Result.write` uses `json.putraw`). A `text[N]` field is escaped automatically (`json.putslice`). So a handler building a `json`-view or `text`-view field's content from anything not already valid JSON / JSON-string-safe must escape it itself first, with `json.escslice(tool.vbuf, tool.vn, src, from, upto)` — skipping that produces invalid JSON, not merely wrong characters, because an unescaped quote or control character inside the slice ends the JSON string early. ## 8. Deliberate deviations from Pascal | Pascal | Wantzel | why | |---|---|---| | `f := value` as the result | `return value` | one way, no implicit result variable | | `integer`, `longint`, `byte`, `word`, ... | only `int` (64) and `char` (8) | no choice = no mistake | | `boolean` | `bool` | short | | `string`, `shortstring`, `ansistring`, `pchar` | `str` (literal) + `array of char` | no hidden allocation | | `real`, `single`, `double`, `extended` | only `real` (64) | idem | | `var` parameters | array view or global | no aliasing of scalars | | `(* *)`, `{ }` | only `//` | a brace inside the comment text ended it early, and the error landed far from its cause | | `shl`/`shr`/`and`/`or` on ints | `shl`/`shr`, `band`/`bor`/`bxor`/`bnot`; `and`/`or` only on bool | logical and bitwise never confused | | `exit`, `halt` | `return`, `halt(code)` | | | units, `uses` | `import` for the library, `include` for a file | both textual; no module system, no namespaces | | `set`, `with`, `goto`, pointers | absent and not coming | | | `case` | absent; write an if-chain | a generator chose it once in five where it could | | variant records, `packed` | absent | one layout, see Records | | syscalls unreachable | `sys1..sys6`, `addr` | the whole runtime is Wantzel | ## 9. Planned (not yet valid) **Before 1.0, anything here can still change.** This is a young language: if a construct costs more than it gives, it goes, and code using it needs editing. Plan for it, and re-read this section on each release. That is not licence to churn. **The bar is evidence, not taste** — a proposal resting on symmetry or familiarity does not clear it, one that shows what a construct costs in practice does. It is then weighed against the test in `design.md`: a change should remove a choice between things that meant the same, never a distinction. After 1.0 this reverses: compatibility is the default, and a break needs the stronger case. What an application still needs meanwhile is library (`lib/`), not language. One thing was **removed**: the `{ }` block comment. It ended at the first `}`, so a brace inside the comment text (a JSON example, the words "default {}") closed it early and the rest of the sentence compiled as code, with the error appearing far from its cause. Most recorded syntax errors of that era came from it, including several written after the pitfall was documented. `//` has no such failure mode; a source that still uses `{ }` is refused with a message naming the replacement. **Not coming:** heap/`new`/`dispose`, pointers, strings with content as a language type, sets, `with`, `goto`, variants, classes, generics, exceptions, threads, operator overloading, implicit conversions. ## 10. Runtime errors Always with file:line: array index out of bounds · division by zero · `chr()` outside 0..255 · `schar()`/`scan()` out of range · function without `return`. ## 10b. Memory No heap, no pointers, no garbage collector. The model, in three sentences: 1. **All memory is an array with an upper bound.** A global array lives in bss and costs nothing until touched: declaring `array[0..1073741823] of char` (1 GB) is free, every page you write to costs 4 KB. Local arrays live on the stack and are zeroed. 2. **An index is the pointer.** Where C would keep a pointer to an element, Wantzel keeps the index number. An index cannot dangle, is checked against the bounds at every use, and stays valid if you write the whole array to disk and read it back. 3. **Whatever is larger than the program wants to pin down comes from the operating system through `mmap`, and becomes an ordinary array with `view`.** The size is decided at startup or by the file; capacity is disk or virtual memory, not RAM. Which form to use when: | situation | form | |---|---| | number known and small (< a few MB) | static array + counter | | number unknown, but a reasonable upper bound exists | static array with that bound; measure the bound first | | size only known at startup | anonymous `mmap` of that size + `view` | | data that has to survive a restart | file + `mmap`, records as the format, indexes as references | | larger than RAM | file + `mmap`; the kernel pages | | temporary workspace per call | local array (stack, max 8 MB total) or a slice of a global arena | | key → value | a hash table on two arrays | | references between objects | indexes in one table, with a free list for reuse | No allocator per object: every structure gets one reservation, at compile time or at startup, and inside it every reference is an index. ## 11. Limits Every hard limit (arguments, identifier and literal length, include depth, numbers of globals/locals/routines/schemas/tools, source and code size, static memory, stack) is listed with its measured value in the compiler's own test suite, which guards each of them. ## 12. Target Linux x86-64 ELF: a static binary that reaches the kernel through raw syscalls. No assembler, linker, C library or external tool. The compiler is written in Wantzel and implements the whole language — see design.md. The library (`io`, `fs`, `net`, `http`, `json`, ...) and `sys1`..`sys6` are the whole interface to the operating system: a `sys*` call is the syscall instruction, and the number is an ordinary constant, so anything the kernel offers is reachable from source. The compiler compiles itself and reproduces itself byte-identically, and it is one file: the standard library travels inside it (§1b and design.md). --- # Syntax The whole of it: forty-one keywords, one way to say each thing. This page is form only: how to write Wantzel. What it means is `language.md`, which is binding. For idioms and pitfalls see `writing-wantzel.md`; for project-level agreements (entry points, name prefixes) see the Conventions section of `howto.md`. ## The forty-one keywords ``` and array begin bool break char const continue div do downto else end false for forward function if import include int local mod not of or procedure real record return schema shl shr str then to tools true type var while ``` Case does not distinguish names: `Foo`, `foo` and `FOO` are one name, and so are `STORE.SET` and `store.set` — a constant in capitals beside a routine in lower case is a collision, not a convention. `local` in front of a top-level `var`, `const`, `procedure` or `function` keeps that name inside its file; nothing outside can see it. Public is still the default. ## A program ``` import io; // a module of the standard library: a name, no quotes // (a file of your own is `include "shapes.wz";`: in quotes) const MAX = 100; GREETING = "hello"; type Point = record x: int; y: int; end; var count: int; grid: array[0..MAX - 1] of Point; function twice(n: int): int; begin return n * 2; end; procedure greet(who: str); begin io.puts(STDOUT, who); end; begin // the main block, last count := twice(21); greet(GREETING); end. // a full stop, not a semicolon ``` Fixed order: imports and includes, then declarations, then the main block. No header line — a file starts with its first declaration. `import` names a module of the library inside the compiler; `include` names a file, relative to the file it stands in — the rules are in language.md. A routine must be declared before use, or `forward`. ## Types | | | |---|---| | `int` | 64-bit signed integer | | `real` | 64-bit float | | `bool` | `true` or `false` | | `char` | one byte | | `str` | a string **literal**; not a buffer you build | | `array[a..b] of T` | fixed size, known at compile time | | `record ... end` | fields, no methods, no inheritance | One type per concept: no `integer` beside `int`, no `single` beside `real`. Text you assemble lives in an `array of char`; `str` is what you write between quotes. ``` var n: int; ratio: real; ok: bool; c: char; name: str; buf: array[0..255] of char; grid: array[0..799] of char; // 10 rows of 80: row * 80 + col, no nested array ``` ## Declaring ``` const MAX = 100; // the type follows from the value PI = 3.14159; type Colour = record r, g, b: int; // several names, one type end; var i, j, k: int; // the same p: Colour; ``` ## `schema` and `tools` ``` type ConvertArgs = schema celsius: real; end; tools convert(ConvertArgs): ConvertResult "Convert Celsius to Fahrenheit." readonly idempotent; end; ``` A `schema` is a JSON-shaped record type; a `tools ... end;` block declares an MCP tool table, one line per tool, and the compiler generates its `tools/list`, argument parsing and dispatch. Form only, here — what each generates is `language.md` §7 and §7b, which are binding. ## Routines A `function` returns a value; a `procedure` does not. Parameters are by value, except an `array of T`, passed as a reference with its length. ``` function area(w: int; h: int): int; begin return w * h; end; procedure fill(a: array of char; c: char); var i: int; // locals go here, before begin begin i := 0; while i < len(a) do begin a[i] := c; i := i + 1; end; end; function later(n: int): int; forward; // declared now, defined below ``` A function that reaches `end` without `return` is a runtime error, not a silent zero. ## Statements ``` x := 1; // assignment greet("hi"); // a call if x > 0 then y := 1; // one statement if x > 0 then y := 1 else y := 2; if x > 0 then // several: begin ... end begin y := 1; z := 2; end; while x < 10 do x := x + 1; for i := 1 to 10 do total := total + i; for i := 10 downto 1 do total := total + i; while true do begin if done then break; // leave the loop if skip then continue; // next turn end; return v; // from a function return; // from a procedure halt(1); // end the program with an exit code ``` Semicolons **separate** statements; the one before `end` is optional. `end.` closes the program. ## Operators | | | |---|---| | arithmetic | `+` `-` `*` `/` `div` `mod` | | comparison | `=` `<>` `<` `<=` `>` `>=` | | logical | `and` `or` `not` | | bitwise | `shl` `shr`, and `band` `bor` `bxor` as builtins | `/` is real division, `div` is integer division; mixing them by accident is refused, not rounded. There is no `++`, no `+=`, no `?:` and no assignment inside an expression. ## Comments ``` // to the end of the line ``` That is the only form. `{ }` was removed in 0.2.0: it ended at the first `}`, so a brace inside the comment text turned the rest of the sentence into code. ## Literals ``` 42 -1 1000000 // integers 3.14 1.0e-9 // reals 'a' '\n' '\x41' // chars "text" "line\n" "\x00\xff" // strings true false // bools ``` `\xHH` takes **exactly two** hex digits, unlike C — so `"\x41BC"` is three characters, and a generator can write a byte followed by a literal hex character without ambiguity. ## Names A name starts with a letter and may contain letters, digits, underscores and dots — the dot is an ordinary character, used to group names that belong together: ``` calc.reset; io.puts(STDOUT, "hi"); store.set(k, v); ``` That is a naming convention, not a module system: one flat namespace, and `calc.reset` is simply a name with a dot in it. A part after a dot may start with a digit (`Reading.level.1`); the first part may not. ## What is not here No pointers, no heap, no `new`, no garbage collector. No classes, no inheritance, no interfaces. No generics, no overloading, no operator overloading. No exceptions, no threads, no closures, no lambdas. No implicit conversion between types. Each is a decision, not an omission; `design.md` argues them. Short version: a construct that costs more than it gives is left out, because the reader of this code is usually a generator in a loop, and every extra way to say something is another way to get it wrong. --- # Command line Two arguments and one option to compile; two questions to ask it — the whole interface. ``` wantzel [--debug] wantzel --version wantzel --lib wantzel --lib ``` Short on purpose: a compiler with thirty switches is thirty things to get wrong, and an agent in a loop should not have to choose. Options go **after** the two file names. Run `wantzel` with no arguments (or with one it does not know, such as `--help`) and it prints these lines. ## `--debug` Writes `.wzdbg` beside the binary: which address belongs to which source line, and where every routine, parameter, local and global lives, with its type. ``` wantzel prog.wz bin/prog --debug # bin/prog and bin/prog.wzdbg ``` **A `--debug` build keeps every routine**: a plain build leaves out the routines a program never calls, but the sidecar records addresses as the code is written, so a debug build skips that step and is larger than a plain one -- the same program, with nothing moved. Plain text, one record per line; format in design.md. A library module is named `wantzel/lib/.wz` in it, wherever the compiler is installed; `wantzel --lib ` gives its source. ## `--version` Prints the version and the standard library this compiler carries, then exits. ``` $ wantzel --version wantzel 0.5.0 library built in: 48 modules, 979949 bytes (485040 stored), sha256 5549b4a93a96... ``` The library is part of the compiler file, so the second line is a fact about the file, not about where it is installed: how many modules, their size unpacked and as stored, and the sha256 over their source. Two compilers that print the same line compile every `import` to the same text. A compiler built without its library (only the bootstrap stages are) says `library MISSING`. ## `--lib` Without a name, lists the modules of the library this compiler carries, one per line, in order; with a name, writes that module's source to standard output, byte for byte the file it was built from. ``` $ wantzel --lib acme aead ... $ wantzel --lib io > io.wz # the source of `import io;`, to read or to step into $ wantzel --lib jsn wantzel: no library module 'jsn' in this compiler; wantzel --lib lists them -- did you mean: wantzel --lib json ``` This is the source the compiler actually uses, so an editor showing a definition or a debugger showing a line of a module reads it from here rather than from some other copy. An unknown name is an error on standard error, with exit code 1 and nothing on standard output. ## What has no flag, and why | | why not | |---|---| | **optimisation level** | there is no optimiser; straightforward code generation keeps output recognisable as source, and is why compiling takes milliseconds | | **include paths** | none to set. `import io;` reads the module from the compiler itself; `include "x.wz";` reads the file relative to the file that names it. There is nothing in between to configure | | **a library directory** | none, and no environment variable either: the library is part of the compiler file. A `lib/` directory anywhere changes nothing. To try a changed module, include your copy as a file | | **warnings** | none — something is an error or it is fine | | **debug info in the binary** | every runtime check already carries its file and line, always, in the executable itself; `--debug` adds a separate file for a step-by-step debugger, never bytes in the binary | | **stripping the build path** | nothing to strip — a runtime message carries at most the last two path segments (`src/ui/main.wz:412`), and a library module is always `wantzel/lib/.wz`, so the same source gives the same binary built from any path | | **target** | one output: a static x86-64 Linux ELF. The kernel is reached through `SYSCALL` directly, so there is no second platform to select | | **linking** | no linker, nothing to link: no libc, no runtime, no shared libraries | Each is a decision, not a gap. If one turns out wrong, it changes here, with a count behind it. --- # Library Every module in lib/, and a compact reference for the ones most programs touch directly. The library is part of the compiler: `import io;` reads the module `io` from the compiler you run, never from disk, so a program and a compiler file always build the same executable. `wantzel --lib` lists the modules and `wantzel --lib io` prints the copy inside your compiler, source text included. Read it and step into it; to experiment with a module, copy it and `include` the copy as a file of your own. ## Import and include ``` import io; // a module: a bare name, no quotes, no .wz, no path import json; include "model.wz"; // a file of your own, next to this file include "store/orders.wz"; // a file of your own, in a directory next to this file ``` `import` reads a module from the library inside the compiler; `include` reads a file, relative to the directory of the file that names it. A module never comes from disk and a file never from the library, so a directory called `lib/` or a file of your own that happens to share a module's name changes nothing about an `import`. Both are textual and read once; a module imported by another module is there for the rest of the program too. The complete rules and every error message are in language.md §1b; the mistakes people make with them, and an example of files including each other, in writing-wantzel.md. ## All modules | module | for | |---|---| | `acme` | getting a certificate from a certificate authority (RFC 8555) | | `aead` | ChaCha20-Poly1305 as one operation (RFC 8439 §2.8) | | `aes` | AES-128 and AES-256 (FIPS 197), with GCM (NIST SP 800-38D) and CBC with PKCS#7 padding | | `autocert` | a certificate a server gets and renews by itself, from inside its own event loop (ACME, HTTP-01); `http.https` uses it | | `base64` | base64 encoding and decoding (RFC 4648) | | `certstore` | keeping keys and certificates on disk, beside the binary | | `chacha20` | the ChaCha20 stream cipher (RFC 8439) | | `chain` | does a certificate chain up to something trusted? | | `csv` | reading and writing CSV (RFC 4180), with `,` `;` or a tab between the fields | | `csr` | a certificate signing request, and a self-signed certificate (RFC 2986, 5280) | | `der` | reading DER, and the parts of X.509 a TLS client needs (X.690, RFC 5280) | | `dns` | hostnames to IPv4 and IPv6 addresses: a DNS stub resolver over UDP, and TCP when truncated | | `ed25519` | Ed25519 signatures (RFC 8032) and XEdDSA, signing with an X25519 key (`ed.xsign`, `ed.xverify`), and libsignal's form (`ed.sigverify`) | | `fs` | directories, file metadata and reading, on raw syscalls | | `hash` | open-addressing hash tables on caller-supplied arrays | | `hkdf` | HKDF (RFC 5869) and the TLS 1.3 key schedule (RFC 8446 §7.1) | | `hmac` | HMAC-SHA256 (RFC 2104 / RFC 4231) | | `http` | a non-blocking HTTP/1.1 server on epoll, plain and HTTPS, with automatic certificates | | `io` | output, numbers and little-endian byte packing | | `json` | JSON scanning primitives | | `jsonschema` | validating JSON against a JSON Schema (2020-12), the useful subset | | `jws` | JSON Web Signature with ES256, and the JWK thumbprint (RFC 7515, 7638) | | `kv` | helpers for a small JSON object kept as compact text | | `log` | one JSON line per event on stderr, for journald or a file | | `lz` | LZ compression, packing and unpacking: the format the compiler stores this library in | | `math` | real arithmetic beyond the operators: `exp`, `log`, `pow`, trigonometry, text | | `mcp` | Model Context Protocol over JSON-RPC 2.0 | | `mcphttp` | MCP over HTTP: the endpoint routine for `app.request` | | `money` | amounts as whole cents: reading `1.234,56` or `1,234.56`, writing them back | | `net` | sockets and epoll, straight on top of the system calls; IPv4 and IPv6 (IPv6 on Linux only for now) | | `oauth` | an OAuth 2.1 authorization server for MCP clients | | `openapi` | an OpenAPI 3.1 document and a Swagger UI page, generated from a `tools` block | | `p256` | the NIST P-256 curve and ECDSA verification (FIPS 186-4, SEC 2) | | `p384` | ECDSA over NIST P-384 (secp384r1), verification only | | `pg` | a PostgreSQL client, read-only: connect (SCRAM-SHA-256), run a query, read the rows | | `poly1305` | the Poly1305 one-time authenticator (RFC 8439) | | `proc` | processes: forking, reaping, and knowing how it ended | | `protobuf` | reader for the protobuf wire format | | `rand` | random bytes and numbers from the kernel | | `router` | a few helpers on top of `http.wz` for routing by path | | `rsa` | RSA-2048 signature verification, for certificate chains that are not ECDSA | | `search` | a small search index: words, prefixes, no difference between é and e | | `sha1` | SHA-1 (RFC 3174) -- not a security primitive; kept only for protocols that name it (`websocket.wz`'s handshake) | | `sha256` | SHA-256 (FIPS 180-4) | | `sha384` | SHA-384 and SHA-512 (FIPS 180-4): `sha384.init512`, `sha384.final512`, `sha384.hash512` | | `store` | durable tables of fixed-size records: an append-only log | | `time` | calendar time: dates, ISO weeks, adding months, `dd-mm-yyyy`, Central European Time, running at a fixed time of day | | `tls` | TLS 1.3: a blocking client, and a server that runs inside an event loop | | `tools` | MCP and REST glue for a declared `tools` block | | `toolsmcp` | the MCP half of the glue for a declared `tools` block | | `uuid` | UUID version 4 (RFC 9562) | | `websocket` | a WebSocket server (RFC 6455) that takes over a connection from `http.wz`, on the same port | | `x25519` | X25519 key agreement on Curve25519 (RFC 7748) | | `x509` | reading an X.509 certificate (RFC 5280), on top of `der.wz` | `tools.wz`/`toolsmcp.wz` are the transport for a declared `tools ... end;` block — see language.md §7b. Together `tls.wz`, `x25519.wz`, `chacha20.wz`, `poly1305.wz`, `aead.wz`, `hkdf.wz`, `hmac.wz`, `sha256.wz`, `sha384.wz`, `p256.wz`, `p384.wz`, `rsa.wz`, `der.wz`, `x509.wz`, `chain.wz`, `csr.wz`, `certstore.wz`, `jws.wz`, `acme.wz` are a TLS 1.3 client and server with nothing linked; `autocert.wz` puts them together into a server that keeps its own certificate. The sections below cover the modules most programs touch directly. For the rest, the routine comments in the module text (`wantzel --lib `) are the reference, and writing-wantzel.md already records several sharper pitfalls (`store.*`, `kv.*`, `oauth.*`, `sha256.hex`). ## `io` — output, numbers, byte access `import io;` The lowest layer: no buffering, no allocation, every call a syscall or a few instructions over a buffer you already own. | routine | meaning | |---|---| | `io.write(fd, a, n): int` | raw `write(2)`; returns bytes written, or negative on error | | `io.read(fd, a, n): int` | raw `read(2)`; same contract | | `io.out(fd, a, n)` | write and forget (a procedure, not a function) | | `io.puts(fd, s: str)` | write a string **literal**, not a buffer | | `io.putn(fd, v: int)` | write the decimal form of `v` | | `io.fatal(s: str)` | write `s` and a newline to `STDERR`, then `halt(1)` — never returns | | `io.push(b, at, s: str): int` | append literal `s` to `b` at `at`; returns the new position | | `io.pushnum(b, at, v: int): int` | append the decimal form of `v` | | `io.digits(v: int): int` | render `v` into `io.nbuf`; returns the length | | `io.put32(b, at, v)` | write `v` as 4 bytes, little-endian | | `io.get16/get32/get64(b, at): int` | read 2/4/8 little-endian bytes | | `io.now: int` | monotonic nanoseconds, for elapsed time | | `io.realtime: int` | wall-clock nanoseconds since the epoch | `STDIN`, `STDOUT`, `STDERR` are the fd constants. `io.push`/`io.pushnum` **truncate rather than fail**: a negative `at` passes through unchanged (so a `-1` from a refusing routine elsewhere travels a chain undamaged), and a full buffer gets as much as fits, returning `len(b)`. Compare the returned position to the one passed in to detect either case. `io.now`/`io.realtime` call `io.fatal` if the underlying clock read fails, rather than risk returning a stale or zero time that looks real. Limits: no formatted output beyond decimal integers, no line-oriented reading. ``` import io; var buf: array[0..63] of char; n: int; begin n := io.push(buf, 0, "count = "); n := io.pushnum(buf, n, 42); io.out(STDOUT, addr(buf[0]), n); io.puts(STDOUT, "\n"); end. ``` ## `fs` — directories, file metadata, byte search `import fs;` (pulls in `io`) Read-only: nothing here creates, writes, renames or deletes. `fs.open` is a convenience for the read-only case; writing goes through `sys3(SYS.open, ...)` directly. | routine | meaning | |---|---| | `fs.stat(a: int): bool` | fill `fs.size`, `fs.mode`, `fs.mtime` for the NUL-terminated path at `a`; `false` if unreachable | | `fs.isdir: bool` / `fs.isfile: bool` | read `fs.mode` from the last `fs.stat` | | `fs.open(a: int): int` | open the path read-only; returns the fd, or negative on failure | | `fs.opendir(a: int): int` | open for reading directory entries; returns the fd, or negative (with `fs.err`/`fs.errno`) on failure | | `fs.close(fd)` | close a descriptor, and free its directory slot if it had one | | `fs.next(fd): bool` | step to the next entry (skips `.`/`..`); fills `fs.name`, `fs.namelen`, `fs.type`; `false` when exhausted or on error (see `fs.err`) | | `fs.forget(fd)` | free `fd`'s directory slot WITHOUT closing `fd` — for a caller that owns the fd's lifecycle itself (see below) | | `fs.find(h, from, upto, n, nlen): int` | first occurrence of `n[0..nlen)` in `h[from..upto)`; `-1` if absent | `fs.stat` takes a raw address, not a `str` — build it with `io.push` plus a manual `chr(0)` terminator. `fs.type` is one of the kernel's `DT_*` constants (`DT_DIR`, `DT_REG`, `DT_LNK`). `fs.find` works on any `array of char`, not just file content, and uses the SIMD `scan` builtin for its leading byte. **A recursive walk works**: each `fs.opendir` gets its own read buffer and position, so opening a child directory while a parent's `fs.next` loop is still in progress does not disturb the parent — recursing (using `fs.type = DT_DIR`) is the caller's job, same as before, but nesting it is now safe. Limits: at most 16 directories open at once, process-wide (`FS.MAXOPEN` in `wantzel --lib fs`); a 17th `fs.opendir` fails with `fs.errno = 24` (`EMFILE`) and `fs.err` naming it, until an open one is `fs.close`d. **If you open a directory your own way** (not through `fs.opendir` — your own path resolution or an `openat` wrapper, say) and still read it with `fs.next`, that fd gets a slot the same as `fs.opendir` would give it. Close that fd your own way too, and **call `fs.forget(fd)` first.** Operating systems reuse fd numbers: without `fs.forget`, the next thing opened can get the same number, and `fs.next` on it would find the old slot still claimed and resume reading from the old directory's position instead of starting fresh. A caller that always goes through `fs.opendir`/`fs.close` never needs `fs.forget` — that pair already does this for you. ``` import fs; var path: array[0..15] of char; n, fd, count: int; begin n := io.push(path, 0, "/tmp"); path[n] := chr(0); fd := fs.opendir(addr(path[0])); if fd < 0 then io.fatal("cannot open directory"); count := 0; while fs.next(fd) do count := count + 1; fs.close(fd); io.putn(STDOUT, count); io.puts(STDOUT, "\n"); end. ``` ## `time` — calendar time without a time zone `import time;` (pulls in `io`) A moment is an `int`: seconds since `1970-01-01T00:00:00Z`. Proleptic Gregorian calendar, UTC only, no DST, no leap seconds. Day arithmetic is exact for years 1..9999 and beyond. | routine | meaning | |---|---| | `time.days(y, m, d): int` | days since 1970-01-01 for the civil date | | `time.join(y, m, d, hh, mm, ss): int` | the full moment for a civil date and time | | `time.split(t: int)` | fills `time.year`, `.month` (1..12), `.day`, `.hour`, `.minute`, `.second`, `.weekday` (0=Mon..6=Sun), `.yday` (0=Jan 1) — a procedure, read the fields immediately | | `time.leap(y: int): bool` | is `y` a leap year? | | `time.daysin(y, m: int): int` | days in month `m` of year `y`; `0` outside `1..12` | | `time.iso(dst, at, t: int): int` | write `t` as `YYYY-MM-DDTHH:MM:SSZ`; returns the new position | | `time.parseiso(s: array of char): int` | parse ISO 8601; returns the moment, sets `time.ok` | | `time.pad(dst, at, v, w: int): int` | write `v` as `w` zero-padded digits | | `time.num(s, at, n: int): int` | read `n` decimal digits at `s[at]`; `-1` if not digits | | `time.nowsec: int` | current moment (`io.realtime div 1000000000`) | | `time.weekdayof(t): int` | the weekday of `t`: 0 = Monday .. 6 = Sunday | | `time.isoweek(t): int` | the ISO 8601 week (1..53); `time.isoyear` is the year that week belongs to | | `time.adddays(t, n): int` | `n` days later (earlier when negative) | | `time.addmonths(t, n): int` | `n` months later; a day past the end of the new month becomes its last day (31 January + 1 = 28 or 29 February) | | `time.putdmy(dst, at, t, sep): int` | the date as `dd-mm-yyyy` with `sep` (`-`, `/`, `.`); returns the new position | | `time.parsedmy(s): int` | `d-m-yyyy` (also with `/` or `.`) as midnight UTC; sets `time.ok` | | `time.cet(t): int` | the offset of Central European Time at `t`: 3600, or 7200 in summer (the EU rule: last Sunday of March to last Sunday of October, 01:00 UTC) | | `time.nextat(now, hh, mm, cet: bool): int` | the first moment after `now` whose clock reads `hh:mm`, in UTC or (cet) Amsterdam time | | `time.sleepuntil(t)` | wait until moment `t`; returns at once for a moment that has passed | `time.parseiso` accepts a bare date, no zone (read as UTC), a fractional part (dropped), `Z`, or a `+hh:mm` offset (subtracted to reach UTC); a space is accepted for the `T`, as Postgres writes it. **It signals failure through `time.ok`, not the return value** — `-1` is itself a valid moment, so always check `time.ok`. Limits: no time zones beyond UTC and Central European Time (`time.cet`), no duration/interval type. **Running at a fixed time of day** is a loop: `t := time.nextat(time.nowsec, 9, 0, true); time.sleepuntil(t); ...do the work...` -- 09:00 Amsterdam time, summer and winter. `time.nowsec` shares `io.realtime`'s fatal-on-clock-failure behavior. ``` import time; var s: array[0..15] of char; n: int; t: int; begin n := io.push(s, 0, "2026-09-11"); t := time.parseiso(s[0..n - 1]); if time.ok then io.putn(STDOUT, t) else io.puts(STDOUT, "bad"); io.puts(STDOUT, "\n"); end. ``` ## `csv` — reading and writing CSV `import csv;` (pulls in `io`) Rows of fields (RFC 4180): a field may be quoted, and inside quotes the separator and a line end are text and a quote is written twice. `csv.sep` is the separator -- `,` while you leave it, `;` for a Dutch export (whose decimal mark is `,`), a tab -- and `csv.detect` picks it from the first line. | routine | meaning | |---|---| | `csv.detect(b): char` | the separator of the first line: whichever of `;` `,` tab occurs most outside quotes | | `csv.next(b, at, dst): int` | one field from `b[at..]`, unquoted into `dst[0..csv.len - 1]`; returns the position after it. `csv.eol`: it ended its row; `csv.cut`: it did not fit `dst`; `csv.bad`: a quote was never closed | | `csv.put(dst, at, s): int` | append a field, quoted only when it must be (the separator, a quote, a line end, a space at either end) | | `csv.putn(dst, at, v): int` | append a whole number | | `csv.putsep(dst, at): int` / `csv.puteol(dst, at): int` | between fields / after a row: `\r\n`, or `\n` with `csv.lfonly` | ``` csv.sep := csv.detect(b[0..n - 1]); at := 0; while at < n do begin at := csv.next(b[0..n - 1], at, field); // field[0..csv.len - 1] if csv.eol then ... // the last field of its row end; ``` ## `money` — amounts as whole cents `import money;` (pulls in `io`) Money is never a `real`. An amount is an `int` of cents. `money.parse` reads what people and exports write -- `1.234,56`, `1,234.56`, `12,5`, `-7`, `€ 12,30`, `EUR 1.234,56`, `12,30-` -- and sets `money.ok`; the last `.` or `,` with one or two digits after it is the decimal mark, and a last group of exactly three digits groups thousands (`1.234` is 1234,00). More than two decimals is refused. | routine | meaning | |---|---| | `money.parse(s): int` | cents; `money.ok` false (and 0) when it is no amount | | `money.put(dst, at, cents, dec): int` | cents as text with `dec` (`,` or `.`) as the decimal mark and the other one grouping thousands (not with `money.nogroup`); always two decimals | | `money.percent(cents, pct): int` | a share, rounded half away from zero: `money.percent(1999, 21)` is 420 | ## `search` — a small search index `import search;` (pulls in `io`) For the listings on a website: add each text once under a number of your own; a query finds the numbers whose text has EVERY query word as the start of one of its words (`vrij amst` finds Vrijstaande woning in Amsterdam), without case and without accents (é is e, ç is c, ñ is n, ß is ss). The index lives in fixed global arrays: 1 MB of words, 131072 words, 16384 texts; a query uses at most 8 words. Hits come in the order the texts were added; nothing is ranked. | routine | meaning | |---|---| | `search.clear` | empty the index | | `search.add(doc, text): bool` | add a text under number `doc`; false (and nothing kept) when the index is full | | `search.find(query, out): int` | the matching numbers into `out[0..k - 1]`; returns `k` (an empty query finds nothing) | ## `pg` — a PostgreSQL client, read-only `import pg;` (pulls in `io`, `net`, `dns`, `sha256`, `hmac`, `base64`, `rand`) A running PostgreSQL over TCP, with the simple query protocol: one query string, the rows as text. Signing in: SCRAM-SHA-256 (the default since PostgreSQL 14), a cleartext password, or trust; MD5 is refused. **Right after signing in the session is read-only**: an INSERT, UPDATE or DELETE is refused by the database itself (SQLSTATE 25006). The connection blocks. | routine | meaning | |---|---| | `pg.connect(host, port, user, password, db): bool` | connect, sign in, set read-only; false with `pg.err[0..pg.errn - 1]` (the server's message and its SQLSTATE) | | `pg.query(sql): bool` | run one query; its rows then come with `pg.next`; `pg.ncols` columns | | `pg.next: bool` | the next row; false after the last | | `pg.value(i, dst): int` | the text of column `i`; its length, or -1 for NULL | | `pg.int(i): int` | column `i` as a whole number (0 for NULL) | | `pg.colname(i, dst): int` | the name of column `i` | | `pg.close` | end the session | A message larger than 1 MB (one very wide row) is refused with a clear error. To read a SQLite file instead of a server, there is no module yet. ## `math` — real arithmetic beyond the operators `import math;` (pulls in `json`, and through it `io`) Plain Wantzel over `real`: range reduction plus a series, no C math library. A few ULP of accuracy over the ranges the routines target, not IEEE-754-exact at every corner. Constants: `MATH.PI`, `MATH.E`, `MATH.LN2`, `MATH.HALFPI`, `MATH.TWOPI`. | routine | meaning | |---|---| | `math.abs/max/min(...)` | absolute value, larger, smaller | | `math.floor(x): real → int` / `math.ceil(x)` | round to `int`; `x` must fit in an `int` | | `math.isnan(x): bool` | is `x` NaN? | | `math.pow2i(k: int): real` | `2^k`, exact | | `math.powi(x: real; n: int): real` | `x^n`, integer `n`, by repeated squaring | | `math.pow(x, y: real): real` | `x^y`; NaN if `x < 0` and `y` non-integer | | `math.exp(x): real` | saturates: `+inf`-shaped above `709.7`, `0.0` below `-745.0` | | `math.log/log10/log2(x): real` | NaN for `x < 0`, large negative for `x = 0` | | `math.sin/cos/tan/asin/acos/atan/atan2(...)` | trig; `asin`/`acos` NaN outside `[-1, 1]` | | `math.radians(deg)` / `math.degrees(rad)` | conversions | | `math.fixed(dst, at, x: real, decimals: int): int` | fixed decimals (clamped `0..15`), rounds half away from zero | | `math.text(dst, at, x: real): int` | shortest general form, up to 15 significant digits | | `math.parse(s: array of char): real` | parse a decimal number; sets `math.ok` | **`math.parse` signals failure through `math.ok`**, not the return value — `0.0` is both a legitimate result and the failure value. `math.ok` is also `false` for a partial match: the whole slice must be one number. Limits: no hyperbolic functions, no complex numbers, no statistics, no arbitrary precision. Three routines are internal only — `math.reduce`, `math.sinr`, `math.cosr` — call `math.sin`/ `math.cos` instead; the three give a silently wrong answer outside the first quadrant. ``` import math; var buf: array[0..31] of char; n: int; begin n := math.fixed(buf, 0, math.pow(2.0, 10.0), 2); io.out(STDOUT, addr(buf[0]), n); io.puts(STDOUT, "\n"); end. ``` ## `json` — JSON scanning and writing primitives `import json;` (pulls in `io`) No tree: a value stays in the input buffer, described by `(offset, length)`. The generated `schema` parsers are built on this; reach for it directly only for open-ended input (a proxy, a log line) where a `schema` does not fit. | routine | meaning | |---|---| | `json.ws(b, at, last): int` | skip whitespace; first non-blank position | | `json.skip(b, at, last): int` | step over one whole value, however nested | | `json.string(b, at, last): int` | scan a string; fills `json.sat`/`json.send` | | `json.number(b, at, last): int` | scan a number's integer part into `json.ival` | | `json.real(b, at, last): int` | scan a number as `real` into `json.rval`, up to 18 significant digits | | `json.boolean(b, at, last): int` | scan `true`/`false` into `json.bval` | | `json.isnull(b, at, last): bool` | is the value `null`? (does not consume) | | `json.literal(b, at, last, s: str): int` | does `b` at `at` spell `s`? | | `json.strend(b, at, last): int` | given `at` just past an opening quote, find the closing one | | `json.eq(b, at, last, s: str): bool` | does `b[at..last)` equal `s`? | | `json.escaped(b, from, upto): bool` | does `b[from..upto)` contain a backslash escape? | | `json.unescape(src, from, upto, dst, at): int` | decode a string body into `dst`; `-1` on malformed escape or no room | | `json.copystr(src, from, upto, dst): int` | the same, always from `dst[0]` | | `json.putstr(dst, at, s: str): int` | append `s` as a quoted, escaped string — **truncates** | | `json.putslice(dst, at, src, from, upto): int` | quoted, escaped slice — **refuses** (`-1`), does not truncate | | `json.escslice(dst, at, src, from, upto): int` | escaped, no quotes — truncates | | `json.putb(dst, at, c: char): int` | one raw byte — **refuses** | | `json.putraw(dst, at, src, from, upto): int` | verbatim, no escaping — truncates | | `json.putreal(dst, at, v: real): int` | up to 15 significant digits; NaN/infinity become `null` — truncates | All readers take `(b, at, last)` with `last` exclusive and return `-1` on malformed or truncated input — there is no position to continue from on failure. **Two different write contracts**: `putstr`/`escslice`/`putraw`/`putreal` truncate silently (compare the returned position to detect it, like `io.push`); `putslice`/`putb` refuse and return `-1` — always test their result, because they back the generated `.write`, where a half-written object is a syntax error, not a shorter one. A negative `at` passed to any of them comes back unchanged, so a `-1` can travel a chain of appenders and be tested once at the end. Limits: no tree, no path lookup, no in-place mutation. Field lookup by key is `kv.find`/`kv.first`/`kv.next` in `wantzel --lib kv`; a whole typed object is a `schema` declaration. ``` import json; var buf: array[0..127] of char; n: int; begin n := io.push(buf, 0, "{\"name\":"); n := json.putstr(buf, n, "probe"); n := io.push(buf, n, ",\"count\":"); n := io.pushnum(buf, n, 42); n := io.push(buf, n, "}"); io.out(STDOUT, addr(buf[0]), n); io.puts(STDOUT, "\n"); end. ``` ## `http` — a non-blocking HTTP/1.1 server on epoll `import http;` (pulls in `net`, `json` and `autocert`, which brings the TLS stack and `dns`) One process, one event loop, no threads, no allocation per request. The application defines `procedure app.request`, called once per complete request, reading the request through the module's globals. A request is parsed in place in `http.inbuf`; the reply is built in `http.outbuf`. The same loop terminates TLS itself — see HTTPS below. | routine | meaning | |---|---| | `http.serve(port, workers: int)` | bind and run the event loop; never returns. `workers > 1` forks that many, sharing the port via `SO_REUSEPORT` | | `http.maxbody(cap: int)` | accept request bodies up to `cap` bytes (above `INBUF`, each read into a region of its own connection's); call before `http.serve` | | `http.maxbodymem(total: int)` | what all large bodies being read at once may hold together (256 MB); past it, `503` with `Retry-After` | | `http.maxreply(cap: int)` | allow reply bodies up to `cap` bytes (at least `OUTBUF`); a larger reply becomes `500` | | `http.maxconn(n: int)` | at most `n` connections at once (1..`MAXCONN`); call before `http.serve` | | `http.timeouts(idle, header, body, send: int)` | the deadlines in seconds, `0` keeps the default; call before `http.serve` | | `http.addport(port: int)` | listen on one more port too (listener 1, 2, ... in call order; at most 3, one fewer with `http.redirect`); call before `http.serve` | | `http.bindto(a, b, c, d: int)` | listen on one IPv4 address instead of every address -- `http.bindto(127, 0, 0, 1)` for a server only this machine reaches (behind a reverse proxy); holds for every listener; call before `http.serve` | | `http.https(host, email, store: array of char; staging: bool)` | HTTPS with a certificate from Let's Encrypt, obtained and renewed from this loop; the port given to `http.serve` speaks TLS, and port 80 redirects. Call before `http.serve` | | `http.httpsfiles(certfile, keyfile: array of char): bool` | HTTPS with the chain and key in PEM files; `false` and `http.tlswhy` when they do not load. Call again to replace the certificate while serving | | `http.redirect(port: int)` | the plain port of HTTPS mode (80 unless set): challenges, redirects, never the application | | `http.istls: bool` | the request being served came over TLS | | `http.listen(port, workers: int)` / `http.poll(timeout: int): int` | `http.serve` in two halves, for a program with its own loop: bind (and fork), then one round per call — waits up to `timeout` ms (`-1`: until something happens, at most until the next deadline check) | | `http.method: int` | `HTTP.GET/POST/PUT/DELETE/PATCH/OPTIONS/HEAD`, or `0` | | `http.pathis(s: str): bool` / `http.pathstarts(prefix: str): bool` | path match; the rest is `http.inbuf[http.restat..http.restend)` | | `http.header(name: str): bool` | case-insensitive lookup; value at `http.inbuf[http.hvat..http.hvend)` | | `http.bearer: bool` | the `Authorization: Bearer` token | | `http.param(key: str): bool` | query-string value at `http.pval[0..http.pvlen)` | | `http.form(key: str): bool` | `application/x-www-form-urlencoded` body value | | `http.urldecode(src, from, upto, dst): int` | percent-decode; `-1` if malformed or no room | | `http.add(s: str)` / `.addn(v)` / `.addc(c)` / `.addb(b, at, n)` | append to the reply body | | `http.hdradd(s: str)` / `.hdraddslice(name; b, from, upto)` | add a response header before `finish` | | `http.finish(status: int, ctype: str)` | write status line, headers, body; queue the reply — must be the **last** call for this request | | `http.logging: bool` | set `true` for one JSON line per request on `STDERR` | | `http.fd`, `http.slot`, `http.lis` | the connection being served: its fd, its slot, the listener it came in on | | `http.nopen`, `http.refused`, `http.evicted`, `http.timedout`, `http.queued` | counters: open now, answered `503` (or, on a TLS port, closed) for want of a slot, closed to make room, closed by a deadline, replies that waited for the socket | | `http.tlsrefused`, `http.tlsfailed`, `http.tlskept` | TLS counters: closed at once (no certificate yet, or no session free), sessions that failed (not TLS, a bad record, an alert), reads that decrypted to more than the slot could take at once | `http.param`/`.form`/`.header` share result buffers — a second lookup overwrites the first; copy out what you still need. `http.hdradd`/`.hdraddslice` silently drop a header if the 2 KB header buffer is full. **Large requests.** A body larger than `INBUF` (256 kB) gets `413` unless `http.maxbody(cap)` was called. Then each body up to `cap` is read into a region of its own connection's, mapped when its headers arrive and unmapped as soon as it is answered — several can arrive at once, and an answered one costs nothing. All of them together may hold `http.maxbodymem` (256 MB by default); a body that would go past it is answered `503` with `Retry-After`, and one larger than `cap` (or than that total) `413`. Inside `app.request`, `http.bodyinbig: bool` says whether to read `http.inbuf[http.bodyat..]` or `view(http.bigbase, http.bodylen)`; `mcp.http` and `tool.rest` do this themselves, `http.form` does not. A client that sends `Expect: 100-continue` (curl, for a body over 1 MB) is answered `100 Continue` once its body is accepted, and gets the refusal instead when it is not. **Large replies.** A reply is built in `http.outbuf` until it outgrows it; then it moves, whole, to a region of its own that doubles as it grows, up to `http.maxreply` (`OUTBUF`, 512 kB, by default). Past that the reply becomes a clean `500`. In the clear the region itself becomes the reply's queued output, so a large reply is never copied a second time; over TLS it is sealed into one region that is. What waits for a slow reader counts against `http.pendmax` (128 MB): a connection that would push the total past it is closed, the others are not affected. Code that writes into `http.outbuf` itself (`json.putstr(http.outbuf, http.wpos, ...)`) must stay within its first 512 kB; past that, append with `http.add`/`addb`/`addc`/`addn`. **Limits, per worker process:** | | size | | |---|---|---| | connections at once | `MAXCONN` = 4096 | lower with `http.maxconn`; any fd below `HTTP.MAXFD` = 65536 | | request (headers + body) | `INBUF` = 256 kB | more through `http.maxbody`, a region per connection | | large bodies being read, together | `http.maxbodymem` = 256 MB | past it, `503` with `Retry-After` | | reply body | `OUTBUF` = 512 kB | more through `http.maxreply`; past the limit, `500` | | input every connection owns | `HTTP.SLOTIN` = 16 kB | a larger request borrows a chunk | | shared input chunks | `HTTP.CHUNKS` = 64 of 256 kB | that many large requests can be arriving at once | | unsent reply bytes held | `http.pendmax` = 128 MB | all connections together | Static memory is about 86 MB (4 MB of it the TLS stack), most of it pages that are never touched unless a connection uses them. TLS sessions add a region of 68 MB, mapped on the first TLS connection and likewise touched only as records arrive in pieces. Large bodies and replies take memory only while they exist, within the limits above: at most `http.maxbodymem` (256 MB) of bodies once `http.maxbody` is set, one reply being built (up to `http.maxreply`, and as much again while it is sealed for TLS) and `http.pendmax` (128 MB) of output waiting for slow readers. `http.serve` raises the soft limit on open files to 65536 (never above the hard limit). **Deadlines.** A connection that makes no progress is closed. Defaults, changed with `http.timeouts`: | waiting for | default | counted | |---|---|---| | the request line and headers | 10 s | from the first byte, or from the accept; never extended | | the body | 30 s + 1 s per 16 kB of `Content-Length` | from the end of the headers | | the next keep-alive request | 60 s | from the end of the last reply | | the client to read the reply | 30 s | without progress | **When it is full.** With every slot taken, a new connection closes the quiet connection nearest its deadline (one between requests, or that has not sent anything) and takes its place. If every connection is in the middle of a request, the new one gets `503` with `Retry-After: 1` and is closed. A request that needs a chunk while all 64 are borrowed gets `503` too. Pipelined requests are answered in order, one reply after the other. **Your own loop.** `http.listen(port, workers)` once, then `http.poll(timeout)` repeatedly, with whatever else the program drives in between (`ws.poll`, a timer). The fd-based routines of older loops — `http.accept`, `http.readable(fd)`, `http.flush(fd)`, `http.drop(fd)`, `http.open[fd]` — still work, but only `http.poll` enforces the deadlines. **Any import that brings in `http`** — `tools`, `mcphttp`, `router`, `openapi` — makes `app.request` mandatory, even if `http.serve` is never called; an empty body suffices. A stdio-only MCP server imports `toolsmcp` instead and needs no HTTP at all. **HTTPS.** One call before `http.serve` makes its port speak TLS 1.3 (`tls.wz`); nothing in `app.request` changes: ``` http.https("www.example.com", "admin@example.com", "/var/lib/myapp/certs", false); http.serve(443, 1); ``` `http.https` configures `autocert.wz` and adds a plain listener on port 80 (`http.redirect` moves it). The listeners open, a certificate in the store is installed, and then `http.poll` steps the certificate manager: it wakes at least once a second and calls `autocert.tick`, so the challenge is answered by port 80 while an order is pending. Port 80 answers `/.well-known/acme-challenge/` for the order in progress (`404` otherwise), redirects everything else to `https://[:port]` — `301` for `GET`/`HEAD`, `308` for the rest, which keeps the method — once a certificate is installed, and says `503` with `Retry-After` before that. **It never runs the application.** On the TLS port, before the first certificate, a connection is closed at once. `autocert.*` settings (`setdirectory`, `settiming`, `pin`, `quiet`) can be changed between `http.https` and `http.serve`. `http.httpsfiles(certfile, keyfile)` is the same without an authority: the chain (one or more `CERTIFICATE` blocks, the server's own first) and a P-256 key (`EC PRIVATE KEY` or `PRIVATE KEY`) from PEM files — for development, tests, or a private authority. It may be called again while serving: the next handshake uses the new pair, open connections carry on. With `http.redirect(port)` the plain port redirects to the `Host` the request named. A program that installs its certificate itself with `tls.setcert` sets `http.tlsmain := true` instead. What changes on a TLS connection, and what does not: - the request is decrypted in `http.pull` and the reply sealed in `http.emit`; parsing, pipelining, keep-alive, chunks, the big-body region and every limit above are the same code. A read is at most 32 kB; what it decrypts to beyond the room in the slot is kept for the next read (`http.tlskept`), never dropped — in a region that exists only while it holds bytes, 32 MB for all connections together (past that a connection is closed) - the **header deadline also covers the handshake**: from the accept to the end of the first request's headers, never extended — a client that stops mid-ClientHello is closed by it - a slot's TLS session is freed with the slot; closing sends `close_notify`, except after a reply that did not go out whole - a refused request (`413`, `503`) is not closed at once: the connection lingers up to 5 s, swallowing what the client still sends, so a client busy uploading reads the refusal instead of a reset - no slot free: the connection is closed rather than answered with a plaintext `503` - The `websocket` module works on plain listeners only; on a TLS connection `ws.take` refuses Where the time goes: a new handshake costs milliseconds of CPU (the P-256 signature), so new connections per second are in the low hundreds per core; a request on an established keep-alive connection costs a few times what it does in the clear. Keep connections alive. HTTPS mode runs one worker (`http.serve(443, 1)`): two would each order a certificate. A certificate-manager step blocks the loop for one request to the authority, bounded by its timeout (10 s), a few times per order. Limits: no routing table beyond `router.wz`, no middleware, no chunked REPLIES (every reply carries `Content-Length`), no HTTP/2. A chunked REQUEST body is read and decoded, up to the connection's buffer (256 kB; larger gets 413). ``` import http; procedure app.request; begin if http.pathis("/health") then begin http.add("ok"); http.finish(200, "text/plain"); return; end; http.add("hello from wantzel\n"); http.finish(200, "text/plain"); end; begin http.serve(8080, 1); end. ``` ## `mcp` — an MCP server, and how large its messages may be `import toolsmcp;` for a server over stdio (pulls in `mcp` and `json`); `import tools;` and `import mcphttp;` to serve the same tools over HTTP. The tools themselves are a declared `tools ... end;` block — see language.md §7b. `mcp.stdio` reads one JSON-RPC message per line; `mcp.http`, called from `app.request`, answers one per POST. **Limits.** Three variables, set before the transport starts; `0` keeps the default. | variable | default | bounds | past it | |---|---|---|---| | `mcp.maxin` | 64 MB | one request message: a stdio line, an HTTP body | JSON-RPC error `-32600` (over stdio the line is dropped and the next one read) | | `tool.maxout` | 64 MB | the result JSON of one tool call | JSON-RPC error `-32603`; over REST (`tool.rest`) a 500 | | `mcp.maxout` | 160 MB | one reply message | JSON-RPC error `-32603` | A reply carries a tool result twice — escaped as a text block and as `structuredContent` — so `mcp.maxout` is larger than `tool.maxout`. The error names the limit that was hit, in the message and as `data.limit`, and carries the caller's id when the request had one: ``` {"jsonrpc":"2.0","id":12,"error":{"code":-32603,"message":"the tool result is larger than the maximum of 8388608 bytes","data":{"limit":8388608}}} ``` Nothing is ever cut short: a writer that does not fit makes the whole reply this error. **Memory.** A message that fits the static buffers — `mcp.in` and `mcp.buf`, 4 MB each, and `tool.out`, 1 MB — costs no system call. A larger one moves once into an anonymous mapping as large as its limit; on Linux a page of it costs memory only when written. The request and reply mappings are given back as soon as the message is answered. Tool results are written into a mapping of `tool.maxout` made at the first call and kept for the next, and given back after a result larger than 1 MB; with `tool.maxout` at 1 MB or less, `tool.out` is used and nothing is mapped. Worst case with the defaults, while one maximum call is answered: 64 + 64 + 160 = 288 MB on top of the program's own records; between calls, at most 1 MB more than the static buffers. `mcp.mapped` and `mcp.mappedbytes` say what is held right now. **Over HTTP** the message must fit the HTTP layer too: call `http.maxbody(n)` before `http.serve` to accept requests larger than 256 kB, and `http.maxreply(n)` to send replies larger than 512 kB. A reply larger than `http.maxreply` is answered with the JSON-RPC error for `mcp.maxout`, naming that smaller size. **A transport of your own** calls `n := mcp.handle(b, at, last)`: the reply is `n` bytes at `view(mcp.obase, n)` — in `mcp.buf` whenever it fits there — and `mcp.done` gives a mapping back once they have been sent. A `text`/`json` view field in the output schema is built in `tool.vbuf`, 1 MB; a handler that fills it past that limit is refused with a handler-side failure naming the tool and the limit (`tool : reply exceeds 1048576 bytes`), not a reply built from silently truncated text. For a result that may exceed it, use `text[N]` fields and arrays instead. ## `autocert` — a certificate that a server gets and keeps by itself `import autocert;` (pulls in `tls`, `chain`, `acme`, `certstore`, `dns`) **A server on `http.wz` does not call any of this itself: `http.https` does** (see HTTPS in the `http` section). The routines below are for a program with a loop of its own. A state machine that a server's own event loop steps: it orders a certificate from Let's Encrypt (or any ACME authority) with the HTTP-01 challenge, keeps it on disk, renews it, and hands each new one to the TLS server with `tls.setcert` — no certbot, no cron, no second process, no restart. Every request to the authority is HTTPS whose certificate must chain to a trusted root. | routine | meaning | |---|---| | `autocert.sethost(h: array of char)` | the name the certificate is for (one name) | | `autocert.setemail(e: array of char)` | a contact address for the account (optional) | | `autocert.usestaging: bool` | `true`: Let's Encrypt's staging authority. Default: production | | `autocert.setdirectory(url: array of char)` | another authority's directory URL (`https://` only); overrides staging | | `autocert.setstore(dir: array of char)` | where keys and certificates live; default `certs/` beside the binary | | `autocert.pin(name, a, b, c, d: int): bool` | a fixed IPv4 address for a name, used instead of DNS (the authority's name is otherwise resolved with `dns.resolve`) | | `autocert.settiming(checkms, renewsec, retryms, timeoutms: int)` | check interval (1 day), renew window (30 days), first retry (1 h), one request (10 s); `0` keeps a value | | `autocert.start: bool` | load what is stored, install a usable certificate, check the configuration; no network yet | | `autocert.tick` | call from the loop's timer; does nothing until something is due, then **at most one** request | | `autocert.waitms: int` | milliseconds the loop may sleep before the next `tick` matters | | `autocert.answer(path, dst: array of char): int` | for `GET /.well-known/acme-challenge/`: the key authorization in `dst` and its length, or `-1` (answer 404) | | `autocert.havecert: bool`, `autocert.notafter: int` | a certificate is installed; its expiry in seconds since the epoch | | `autocert.msg[0..autocert.msgn)` | the last log line; `autocert.quiet := true` keeps them off `STDERR` | **What happens, and when.** At `start` a stored certificate that parses, matches its key, names the host and is in date is installed at once, and **not** ordered again while it has more than 30 days left — a restart is free, which matters because Let's Encrypt limits orders per name. The certificate is renewed when fewer than 30 days (or a third of its lifetime) remain. An order is directory → nonce → account (key created once and kept) → new order → authorization → challenge → poll → finalize with a **new** key and CSR → poll → download; the chain is then checked (host, our key, dates, each link signed by the next), saved, and installed — the whole chain, leaf first, since a browser has only the root. A failure keeps the old certificate in service and retries after 1 h, 6 h, then every 24 h — or later if the authority sends `Retry-After` — and that schedule is stored, so a crash-looping service does not hammer the authority either. **The loop's side.** The challenge is fetched by the authority *while the order is pending*, so the loop must keep serving port 80 between steps — which is why each `tick` does one request and returns: ``` if not autocert.start then halt(1); while true do begin wait := autocert.waitms; if wait > 1000 then wait := 1000; nev := net.wait(ep, addr(evs[0]), MAXEV, wait); ... // port 80: n := autocert.answer(path, body); n >= 0: 200 with body[0..n), else 404 ... // port 443: a tls.open session per connection, fed by the loop (as http.wz does) autocert.tick; end; ``` **The store** is one directory (0700, files 0600, every write atomic): `account.key`, `.pem` (the certificate key and chain in one file, so a crash cannot pair a new key with an old certificate) and `.retry`. The keys are PEM `EC PRIVATE KEY`, which openssl reads. A missing or read-only store works — without a cache — and says so. Limits: HTTP-01 only (no wildcards), one name per certificate, no revocation. A step blocks the loop for the length of one HTTPS request: the name lookup (at most 12 s), the connect and every TLS call are each bounded by the request timeout (10 s). `examples/autocertd.wz` is a complete server on `http.https`. ## `websocket` — a WebSocket server on the same port as `http` `import websocket;` (pulls in `net`, `base64`, `http` and `sha1`) RFC 6455, on the SAME listening socket `http.wz` already has. This module never opens a socket of its own; it takes over a connection that `app.request` hands it. | routine | meaning | |---|---| | `ws.take(fd: int): bool` | call from `app.request` after `http.detach := true`; reads `Sec-WebSocket-Key`, sends `101`, returns `true` on success | | `procedure app.wsframe(fd: int)` | (application-defined) once per complete frame; payload at `ws.in[ws.slot * WS.INMAX + ws.at .. +ws.len)`, opcode in `ws.op` | | `procedure app.wsclose(fd: int)` | (application-defined) once per connection end, any reason | | `ws.poll(timeout: int): int` | drive the loop; `timeout` ms, `0` = don't block, `-1` = block; returns connections served | | `ws.sendtext(fd; a: array of char; n: int): bool` | one text frame | | `ws.send(fd; opcode; a; n): bool` | general form: `WS.TEXT`, `WS.BIN`, or a control opcode | | `ws.sendempty(fd; opcode): bool` | a frame with no payload | | `ws.closefd(fd; code: int)` | send a close frame and drop the connection | The takeover: on an upgrade request, set `http.detach := true` and call `ws.take(http.fd)` **before returning from `app.request`** — that is the only point where `http.wz`'s parsed headers still describe the request. If `ws.take` returns `false`, undo the detach and close the fd yourself. Control frames (ping/pong/close) never reach `app.wsframe`. WebSocket runs on plain listeners only: on a TLS connection (`http.istls`) `ws.take` returns `false`. **Connections live in slots, not at their fd** — the same shape `http.wz` uses for its own `MAXCONN` connections. `ws.in` (and every other per-connection table) is sized and indexed by slot, not by fd, so inside `app.wsframe`/`app.wsclose` the base into `ws.in` comes from `ws.slot` (set by this module right before the call), never from `fd` — an fd can be arbitrarily large (up to `WS.MAXFD`, matching `http.wz`'s own fd range) while only `WS.MAXCONN` connections are open at once. Close codes: `1000` normal; `4000`–`4999` reserved by RFC 6455 for application-defined meanings. Limits: `WS.MAXCONN` 256 connections open at once, `WS.MAXFD` 65536 (the fd range `ws.take` accepts — matches `http.wz`'s own), `WS.INMAX` 64 KB buffered input per connection (over that: `ws.closefd(fd, 1009)`), `WS.OUTMAX` 64 KB per outgoing frame; `ws.take` refuses once 256 connections are already open, whatever the fd. `http.serve` cannot also call `ws.poll`, so a program wanting both protocols runs its own loop — `http.listen` once, then `http.poll` and `ws.poll(0)` on every wake — instead of calling `http.serve`. A program that never imports `websocket` is unaffected. ``` import websocket; procedure app.wsframe(fd: int); var base: int; begin base := ws.slot * WS.INMAX; ws.ignored := ws.sendtext(fd, ws.in[base + ws.at .. base + ws.at + ws.len - 1], ws.len); end; procedure app.wsclose(fd: int); begin end; procedure app.request; begin if http.pathis("/ws") and http.header("upgrade") then begin http.detach := true; if not ws.take(http.fd) then begin http.detach := false; net.close(http.fd); end; return; end; http.start; http.add("plain HTTP still works on this port\n"); http.finish(200, "text/plain"); end; ``` ## `dns` — hostnames to IPv4 and IPv6 addresses `import dns;` (pulls in `net` and `rand`) A stub resolver: it asks the nameservers the system is configured with, over UDP, and again over TCP when an answer comes back truncated. `dns.resolve`/`dns.start`/`dns.poll` are A (IPv4) only, unchanged in shape from before: an address is one `int`, `a shl 24 + b shl 16 + c shl 8 + d`. `dns.resolve6`/`dns.start6`/`dns.poll6` are their AAAA (IPv6) counterparts: an address does not fit in one `int` (128 bits against 64), so these take a flat byte buffer instead, 16 bytes per address, network order. A name that is already a literal (a dotted IPv4 quad for the `*4`/plain calls, the colon form for the `*6` ones) comes back as itself, and `localhost` as `127.0.0.1` or `::1`, without a query. | routine | meaning | |---|---| | `dns.resolve(name: array of char; out: array of int): int` | look up A records and wait, at most the lookup's total time (12 s); the number of addresses written to `out`, or a negative `DNS.*` code | | `dns.resolve6(name: array of char; out: array of char): int` | the AAAA counterpart; `out`'s length must be a multiple of 16 | | `dns.connect(name: array of char; port: int): int` | resolve BOTH A and AAAA (concurrently, so one slow record type does not delay the other), then connect to the first address that accepts — IPv6 addresses tried before IPv4, each in the order its records came back; the fd, or a `DNS.*` code (`DNS.NOCONNECT` when it resolved but nothing accepted) | | `dns.start(name: array of char): int` · `dns.start6(name: array of char): int` | begin a lookup without blocking; a query number, or `DNS.BUSY` | | `dns.poll(q: int; out: array of int): int` · `dns.poll6(q: int; out: array of char): int` | never blocks: `DNS.PENDING` (`0`) while running, then the count or a `DNS.*` code; collecting frees `q` (use `dns.poll6` for a query started with `dns.start6`) | | `dns.fd: int` | the resolver's own epoll set — watch it for `EPOLLIN` in yours (shared between A and AAAA queries) | | `dns.wait: int` | milliseconds until the next deadline, for your `epoll_wait`; `-1` when nothing runs | | `dns.cancel(q: int)` | drop a lookup, of either kind | | `dns.error(code: int): str` | a sentence for a `DNS.*` code | | `dns.parseip(s: array of char): int` | a dotted-quad literal as an address, or `-1` | | `dns.ipstr(dst, at, ip: int): int` | append an IPv4 address as text; returns the new position | | `dns.parseip6(s: array of char; out: array of char): bool` | an IPv6 literal (with at most one `::` run) into 16 bytes, network order | | `dns.ipstr6(dst, at: int; ip6: array of char): int` | append an IPv6 address as text, uncompressed (eight groups of four lower-case hex digits); returns the new position | | `dns.loadconf(path: array of char): int` | read nameservers and options from a `resolv.conf`-style file; the count, or a negative errno | | `dns.noservers` · `dns.addserver(a, b, c, d, port: int): bool` | replace the configured servers (at most `DNS.MAXNS`, 3) | | `dns.settimeout(tryms, totalms: int)` | per attempt (5000, as the C library) and per lookup (12000); `0` keeps a value | | `dns.flush` | empty the cache (both A and AAAA entries) | The failures are kept apart, because they mean different things: `DNS.NXDOMAIN` (the name does not exist), `DNS.NODATA` (it exists, without an address of the type asked), `DNS.SERVFAIL`, `DNS.REFUSED`, `DNS.TIMEOUT` (no server answered), `DNS.BADNAME` (not a hostname: a bad label, or a last label of digits only, such as a broken address), `DNS.BADREPLY` (an answer that does not parse, or a CNAME chain that loops), `DNS.NETWORK` (no socket, or nothing listens), `DNS.BUSY`, and from `dns.connect` `DNS.NOCONNECT`. `DNS.NODATA` is type-specific: a name with only AAAA records answers `DNS.NODATA` to an A query and its address to an AAAA one, and the other way round. **Where the servers come from.** `/etc/resolv.conf`, on the first lookup: up to three `nameserver` lines with an IPv4 address, and `options timeout:n attempts:n`. No server at all means `127.0.0.1`, as the C library does. **What a lookup does.** Every attempt has a fresh random id and its own socket on a random source port, connected to the server; a reply counts only when its id and its question (including the type asked, A or AAAA) match, so a forged datagram is ignored rather than believed. A silent server costs one attempt's timeout and the next server is asked; `SERVFAIL`, `REFUSED` and an unreachable port move on at once; `NXDOMAIN` is final. CNAME chains are followed through the answer, under either type — only records on the chain count — and a chain that stops short is asked for under its last name. Compressed names are read with pointers followed only backwards, so a loop cannot hang it. Answers are cached by name AND type (an A and an AAAA lookup of the same name are two cache entries), for their TTL (32 names total); failures are not. Limits: no `search` domains and no `/etc/hosts` beyond `localhost`, so give a full name; `DNS.MAXQ` (8) lookups at once, of either type, sharing the same pool of slots. `dns.connect`'s IPv6 support goes through `net.connect6`. ``` import dns; var addrs: array[0..DNS.MAXADDR - 1] of int; line: array[0..63] of char; n, i, at: int; begin n := dns.resolve("localhost", addrs); if n < 0 then begin io.puts(STDERR, dns.error(n)); io.puts(STDERR, "\n"); halt(1); end; for i := 0 to n - 1 do begin at := dns.ipstr(line, 0, addrs[i]); at := io.push(line, at, "\n"); io.out(STDOUT, addr(line[0]), at); end; end. ``` From an epoll loop, without blocking: start the lookup, put `dns.fd` in your set once, wait no longer than `dns.wait`, and ask `dns.poll` after every wake. `examples/host.wz` is the blocking form as a command-line tool. ``` q := dns.start("example.com"); if not net.watch(ep, EPOLL_ADD, dns.fd, EPOLLIN) then ... while true do begin n := dns.poll(q, addrs); // DNS.PENDING (0), a count, or a DNS.* code if n <> DNS.PENDING then break; r := net.wait(ep, addr(ev[0]), 16, dns.wait); // serve your other fds here too end; ``` ## `tls` — TLS 1.3, a blocking client and an event-loop server `import tls;` (pulls in the crypto modules it needs) TLS 1.3 only, one cipher suite (`TLS_CHACHA20_POLY1305_SHA256`), one group (X25519). The server signs with one P-256 key. No TLS 1.2, no resumption, no client certificates, no HelloRetryRequest, no ALPN (clients then speak HTTP/1.1). **The client** is blocking and handles one connection at a time: | routine | meaning | |---|---| | `tls.connect(fd: int; host: str): bool` | the handshake on a connected socket; `tls.connectb(fd, hb, hn)` takes the name as bytes | | `tls.write(b: array of char; n: int): bool` | send application data | | `tls.read(b: array of char; max: int): int` | receive; `0` at close_notify, `-1` on error | | `tls.close` | send close_notify | | `tls.verified: bool` | the server proved its key and its chain reaches a root in the trust store (`chain.wz`) | | `tls.deadline: int` | milliseconds each call above (and `tls.accept`) may take; `0` means 30 s. Past it the call fails with `tls.err = -110`. The socket is switched to non-blocking mode for this | **The server** never touches a socket. The program's event loop owns each connection, hands its session the bytes that arrived, and sends the bytes it gets back. Nothing blocks, so one thread holds up to `TLS.MAXSESS` (4096) connections, and a client that stalls halfway through its handshake costs its own session and nothing else. | routine | meaning | |---|---| | `tls.setcert(chain: array of char; n: int; key: array of int): bool` | install the chain (DER certificates back to back, leaf first) and the leaf's P-256 private key; refused, keeping the old pair, unless the key matches the leaf | | `tls.open: int` | a session for a new connection; `-1` when no certificate is installed or all are in use | | `tls.feed(s: int; src, app, wire: array of char): int` | the bytes that arrived, `src`; returns how many were taken, `-1` when the session failed. Afterwards `app[0..tls.appn)` is decrypted data and `wire[0..tls.wiren)` must be sent — also on `-1`, when it is the alert | | `tls.seal(s: int; src, wire: array of char): int` | encrypt `src` into `wire`; the number of bytes to send, `-1` when not established or `wire` is too small (it needs `len(src)` + 22 per 16 KB) | | `tls.shut(s: int; wire: array of char): int` | our close_notify into `wire`: its length, or `0` | | `tls.free(s: int)` | give the session back; its keys are wiped | | `tls.state(s: int): int` | `TLS.ST.HANDSHAKE`, `TLS.ST.OPEN`, `TLS.ST.CLOSED` (the peer sent close_notify), `TLS.ST.FAILED`, `TLS.ST.UNUSED` | | `tls.code(s: int): int`, `tls.why: str` | the alert that ended a failed session, and a sentence saying why | | `tls.accept(fd: int): bool` | the blocking form: the whole handshake on one socket, then `tls.read`/`tls.write`/`tls.close` | One step of the loop, when a TLS socket is readable: ``` n := net.recv(fd, addr(raw[0]), len(raw)); used := tls.feed(s, raw[0..n - 1], inbuf[have..len(inbuf) - 1], outbuf); // send outbuf[0..tls.wiren) -- the handshake flight, or the alert when used < 0 have := have + tls.appn; // decrypted request bytes if used < 0 then ... // close the connection after sending if used < n then ... // inbuf is full: keep raw[used..n-1], feed it later ``` - **Partial input is held by the session.** A ClientHello split across TCP segments or records, a record cut in half by a read — `tls.feed` keeps the piece and takes all of `src`. The one exception: a record is not opened until its plaintext fits in `app`, and then `tls.feed` returns less than `len(src)`. An `app` of `TLS.MAXPLAIN` (16384) bytes always has room for one record. - **`wire` needs `TLS.MAXREC` (18432) bytes free while the handshake runs**: the server flight is built whole the moment the ClientHello is complete, and carries the certificate chain. After it, `tls.feed` only writes a KeyUpdate answer or an alert. - **Malformed input fails that session only**: an alert goes into `wire`, `tls.state` says `TLS.ST.FAILED`, and every other session carries on. The caller closes the connection and calls `tls.free`. - **`tls.setcert` is safe while sessions are open.** The next handshake uses the new chain; open sessions keep their keys. - **The client and the server keep separate state**: a blocking `tls.connect` (to an ACME authority, say) can run while server sessions are open. - A deadline is the caller's: a session never times out on its own, so the loop closes a connection that has not finished its handshake or request in time. Memory: 256 bytes per session in a static table, plus 16645 bytes of reassembly each in a region mapped by the first `tls.open` (68 MB of address space for 4096 sessions; only pages that a record arrives into in pieces are ever touched). A program that only uses the client maps nothing. Throughput on one core, measured on an x86-64 laptop: about 150 handshakes a second (ECDSA signing is most of it) and about 30 MB/s of encrypted data. `examples/serve.wz` is the complete server: HTTP and HTTPS on one loop, the ACME challenge, and a certificate picked up from disk while running. `examples/localhttps.wz` is the smallest one, on `tls.accept`. ## `lz` — packing and unpacking `import lz;` (pulls in nothing) The simplest LZ block format: runs of literal bytes and back-references into what was written already, no entropy coding. Source text packs to about half its size; unpacking runs at a few hundred MB/s. It is the format the compiler stores this library in, so it is exercised on every `import`. | routine | what it does | |---|---| | `lz.pack(src, n, dst): int` | `src[0..n)` packed into `dst`; the packed length, or `-1` when `dst` is too small | | `lz.unpack(src, n, dst): int` | `src[0..n)` unpacked into `dst`; the original length, or `-1` when `dst` is too small or `src` is not a whole, valid stream | | `lz.bound(n): int` | the largest result `lz.pack` can give for `n` bytes — size `dst` by it and packing never fails | `lz.unpack` checks every count and offset before it uses it: damaged or hostile input gives `-1`, never a read or write outside a buffer and never a runtime error. The same input always packs to the same bytes. The format itself is described at the top of `wantzel --lib lz`. --- # Writing Wantzel Common mistakes when writing Wantzel, and the idioms that avoid them. For anyone writing Wantzel source, especially an AI agent generating it — most mistakes below produce a compiler message, and this page maps each one back to its fix. Nearly every mistake comes from one of four places: Pascal or C habits the language does not have, the shared case-insensitive namespace, a wrong assumption about a library routine, or a test expectation that was never checked. The specification is language.md; where the two differ, the specification wins and the difference is a finding. ## Pitfalls Each row: what's wrong, what's right, the message that identifies it. Found a new one? Add it under the heading it belongs to. ### Namespace (one shared, case-insensitive namespace) | Wrong | Right | Message | |---|---|---| | `const STORE.SET = 1;` next to `procedure store.set` | Give constants a name no routine has: `STORE.OPSET`, `CURVE.NDAYS`, `HTTP.GET` | `name already used by a variable or constant` / `duplicate global declaration` | | Parameter `N` next to global `n`; type `Out` next to variable `out` | One name per concept; local names that don't collide with globals (`cnt`, `res`) | `name already used by a type` / `duplicate declaration` | | A builtin as a name: `procedure f(len: int)`, `var len: int` | Use `n`, `count`, `nbytes` instead. Builtins: `len addr view scan ord chr real trunc round sqrt pack32 unpack32 slen schar sadr band bor bxor bnot shl shr argc argch halt sys1..sys6` | `that name is built in` | | `tools` as a namespace (`tools.list`) | `tool.list`, `tool.run` — `tools` is a keyword | `variable name expected` | | A type name is also an ordinary identifier: `schema Point` and `var point: ...` collide | `var p: Point` | — | | Assuming `mcp.buf` and `mcp.Buf` differ, or that `a.b` scopes to a module | Dotted names are cosmetic; there is no module scope | — | | Two files each claiming a `run.*` prefix | Grep for a prefix across everything the program can include before claiming it; rename the newer file | `duplicate global declaration`, only where both files are included together | | `if argc < 2` (builtin without brackets) | `argc()` — every builtin used as a value needs its `()` | `missing ( after builtin` | **A prefix is claimed globally**, so two files that each build a `run.*` namespace collide on the first variable of the second one — and only in a program that includes both, so each file compiles alone and the pair does not. ### Types | Wrong | Right | Message | |---|---|---| | `x / 2` on ints | `x div 2` — `/` is for `real` only | `/ divides reals; use div for integers` | | `r := i` with `r: real`, `i: int` | `r := real(i)`; back with `trunc(r)` (toward zero) or `round(r)` | `type error in assignment: expected real, found int` | | `ord(b)` on a bool | `if b then x := 1 else x := 0` | `type error in ord: expected char` | | `if n then` (int as condition) | `if n <> 0 then` — does not exist otherwise | — | | `s = "abc"` | `json.eq(b, at, last, "abc")` (`last` exclusive), `hash.same`, or your own loop | `strings cannot be compared with = or <>` | | Treating `str` as a general text type | `str` is only a literal's type. Text as a value is `array of char` plus a length; use `array of char` in new code — it accepts a literal, an array, a slice and a view | — | | Expecting a 32-bit real, `byte`, `boolean`, `integer` | Only `int char bool real str` and `array` exist | — | | `writeln`/`write` | `io.puts(fd, "..")`, `io.putn(fd, v)` for an int; a real via `json.putreal` into a buffer | `undeclared identifier` on the whole line — check the **outermost** call first | | `1.` or `.5` as literals | `1.0`, `0.5`. A negative real literal in a `const` is fine (`NEG = -2.5`) | — | A smoke test that prints nothing compiles fine and proves nothing about output. ### Arrays, slices, view, records | Wrong | Right | Message | |---|---|---| | A slice as a value: `x := a[1..3]` | Slices and `view(addr, n)` only where an `array of T` argument goes | `a slice can only be passed as an array argument` | | `addr(s[0])` on an empty slice or view | Guard with `if len(s) > 0` first; for an empty range pass `addr(buf[at])` of the source buffer | runtime `array index out of range` | | Treating slices as exclusive | Slices are **inclusive**: `a[lo..hi]` has `hi - lo + 1` elements, `a[3..2]` is empty. A `(at, upto)` exclusive pair becomes `b[at..upto - 1]` | — | | A record as a parameter: `function f(p: Point)` | `procedure f(p: array of Point)`, use `p[0]`; pass the record variable, `items[i..i]`, or `view(address, 1)` | `this parameter needs an array` / `type expected` | | Copying a record field by field in a loop | `a := b` copies the whole record (same type required) | `record types differ in assignment` otherwise | | Wanting `array of array`, a record in a `const`, or in an expression | None of these exist | — | | An empty `array of T` literal | Pass an empty slice: `a[0..-1]` (or `a[i..i-1]`) | — | Other facts worth knowing: a builtin or keyword name is allowed inside a dotted name (`addr.x`, `co.for`, `app.type` are ordinary identifiers). `len(a)` works on array names and `array of T` parameters, not on `str` (that's `slen`) or a constant. `array[lo..hi]` indexing starts at `lo`, which may be nonzero; all arrays are fixed — "growing" means keeping a counter (language.md §10b). Large arrays belong at **global** scope; a local array of megabytes sits on the ~8 MB stack and crashes. ### Routines | Wrong | Right | Message | |---|---|---| | `store.set(...)` as a statement, throwing away the result | `ok := store.set(...)`; or make it a `procedure` | `the value of this function call is not used` | | A `function` missing `return` on some path (including after `while true`) | Every path ends in `return` | runtime `function without return` | | A `procedure` with `return value` | `return;` with no value only | error | | Wanting a `var` parameter | Results write back through an `array of T` argument (a view onto caller storage) or a global — there is no `var` parameter | — | | More than ten argument slots (an array counts as two) | Count slots before writing the signature; pass a slice as the write target, or make repeated arguments state (`q.clear`, `q.setX(...)`, then a no-arg call) | `wrong number of arguments` / `too many arguments in call` | | A missing callback: library declares `procedure app.request; forward;`, application never defines it | Define it; signature must match exactly | `forward declared routine is never defined` / `parameter type differs from the forward declaration` | | Nested routines, function pointers, default arguments, overloading | None exist | — | | `return` in the main program | `halt(code)` instead | — | Counting argument slots is the second most common mistake overall. ### Syntax | Wrong | Right | Message | |---|---|---| | `;` before `else` | `if c then a else b;` | `unexpected else (no ';' may precede it)` | | `var` after `begin`, or any block out of order | Header `;`, then `const`/`var` blocks, then `begin ... end;` | `missing begin in routine body` | | Anything after `end.` | A program ends `end.` (period); a routine ends `end;` | `text after the end of the program` | | `{ }` or `(* *)` comments | `//` only, to end of line. A `{` outside a string is refused | — | | A block comment spanning a JSON literal like `{"a":1}` | Write multi-line comments as several `//` lines (block comments were removed 15-09-2026: they ended at the first `}`) | — | | `'\xHH'` with the wrong digit count | Exactly two hex digits: `'\x1b'`, `"caf\xc3\xa9"`. `"\x41BC"` is three characters, unlike C | `unknown escape sequence` | | `$FF`, `1_000` | `0xFF` only — no Pascal `$`, no digit separators | — | Valid escapes: `'\n' '\t' '\r' '\0' '\\' '\''`, `"\""`, `'\xHH'`. **An `else` binds to the nearest unclosed `if`.** In an if-chain, an arm whose body is itself an `if` without its own `else` swallows everything after it, silently — the result is valid code that means something else: ``` if k = 1 then if v > 0 then out := 1 // still open... else if k = 2 then out := 2 // ...so this else belongs to the INNER if ``` Here `k = 2` and `k = 3` never run, and `k = 1` with `v <= 0` runs the last arm. **Right:** wrap the inner `if` in `begin ... end`. Check for this after rewriting a `case` into a chain — every shape needs its output checked. When a chain is written by a generator, each arm must close bare with only the last carrying `;`, or the emitted code trips this same rule. ### Imports and includes **The rule in one sentence: `import` takes a bare module name — no quotes, no `.wz` — and reads that module from the compiler; `include` takes a file name in quotes, with `.wz`, relative to the file it stands in.** The two never overlap: an import never reads a file, an include never reads the library. `wantzel --lib` lists the modules. | Wrong | Right | |---|---| | `include "io.wz";` for the library | `import io;` — `include` is always a file, so without an `io.wz` next to your file this is refused: `io.wz is not a file next to this one; the library module is imported: import io;` | | `import "io";` or `import "io.wz";` | `import io;` — `import takes a module name, without quotes or .wz: import io;` | | `import io.wz;` | `import io;` — the same message | | `include io;` | `include "model.wz";` for your own file, `import io;` for the library — `include takes a file name in quotes: include "io.wz"; -- for the library module write: import io;` | | `import "model.wz";` for your own file | `include "model.wz";` — `import takes a module name, without quotes or .wz; a file of your own is included: include "model.wz";` | | `import jsn;` | `import json;` — `no library module 'jsn' in this compiler; wantzel --lib lists them -- did you mean: import json;` | | `include "lib/json.wz";` or `include "../../lib/io.wz";` | `import json;` — a path into a `lib/` directory is a file on disk, not the library; the compiler says `the library module is imported: import json;` when it is not there | | `io.puts` with no import | `import io;` at the top — `undeclared identifier: io.puts is declared in the library module io; add: import io;` | | A file of your own named after a module (`store.wz` beside your program, next to the module `store`) | Give it a name no module has (`orders.wz`). `import store;` still reads the library and `include "store.wz";` still reads your file, but every reader — and every tool that opens "store.wz" — has to know which one is meant. This repository refuses such names outside `lib/` for that reason | | A copy of a module in a `lib/` directory, expecting the compiler to use it | Nothing overrides the library. To try a change, copy the module next to your program and include the copy as a file: `include "./tls.wz";` in place of `import tls;` | A program with two imports and a file of its own, and that file in turn naming another file of the project — every include is relative to the file it stands in: ``` app/ main.wz import io; import json; include "orders.wz"; orders.wz include "util/money.wz"; util/money.wz no includes ``` ``` // main.wz import io; import json; include "orders.wz"; // app/orders.wz, next to this file begin orders.show; end. ``` ``` // orders.wz include "util/money.wz"; // app/util/money.wz: relative to orders.wz, not to main.wz procedure orders.show; begin io.puts(STDOUT, "total: "); io.putn(STDOUT, money.euros(1250)); io.puts(STDOUT, " euro\n"); end; ``` ``` // util/money.wz function money.euros(cents: int): int; begin return cents div 100; end; ``` `orders.wz` uses `io.*` without importing it: `main.wz` imported `io` first, and an import is textual, so everything read after it sees it. Import in the file that uses a module when that file is also compiled on its own — a test of `orders.wz`, for instance. Two imports of the same module, or two includes of the same file, read it once. ### Library (wrong assumptions) | Wrong | Right | |---|---| | `sha256.hex(dst, src)` on 16 bytes | It expects exactly 32; use your own hex routine (`oauth.hex16`) for 16 | | `io.puts(fd, s: str)` on a buffer | `io.puts` takes only a literal; use `io.out(fd, addr(b[0]), n)` for a buffer | | `n := io.out(...)` | `io.out` is a procedure, not a function (`a procedure has no value`); call `io.out(fd, addr(b[0]), n);` as a statement, or `sys3(SYS.write, ...)` directly if you need the count | | Assuming `io.push`/`io.pushnum` return a byte count | They return the **new position** | | `json.eq(b, i, i+16, "\"total_matched\"")` | The literal's length including its quotes is 15, so the end is `i + 15` — count it (`printf '%s' '"..."' \| wc -c`), don't estimate. Fails silently: no error, the branch just never runs | | `json.unescape(...)` without checking `len(dst)` | It does not check for you — guard the length yourself | | Hand-computing a record size (`ISIZE = 16 + 8 + ...`) | Measure it: `addr(items[1]) - addr(items[0])`, or `addr(last_field) - addr(first_field) + size` | | `CURVE.DAYS` | The constants are `CURVE.NDAYS`, `CURVE.NHOURS`, `CURVE.DAYLEN` — `CURVE.DAYS` collides with `curve.days` | | Putting unescaped bytes into a `text` field of an output schema | `Out.write` places a `text` view between quotes **raw** — fill it via `json.escslice` (escaped text) or `json.putraw` (only for already-valid JSON) | | Forgetting `import io;` | The compiler names the module: `undeclared identifier: io.puts is declared in the library module io; add: import io;`. A bare message means the name is in no module — check spelling | | Assuming a `str` variable behaves like `array of char` | Only a literal converts implicitly; copy first: `k := io.push(buf, 0, s); ... buf[0..k-1]` | | Calling `parse` on a schema and ignoring what it dropped | An undeclared key is skipped and counted in `json.ignoredn`, `b[json.ignored0..json.ignored1)` gives the first one. Report it — `lib/tools.wz` does (`_meta.ignoredFields`, `X-Ignored-Field`/`X-Ignored-Count`). A key the schema does declare is still strict: a bad value gives `-1`, `json.badkey0..json.badkey1` names it | | Copying a value out of `kv.find`/`kv.first` raw | The value keeps its quotes (`"dryer"` not `dryer`) and every comparison after fails silently. Use `kv.text(b, dst, 0)` — strips quotes, decodes escapes, returns `-1` if not a string | | `http.header("Accept")` expecting case sensitivity | Fixed 15-09-2026: comparison is case-insensitive both sides. On an older compiler, use lower case | | Calling `kv.isobject`/`kv.count`/`kv.match` and assuming the cursor moves | Since 15-09-2026 those three only ask a question and restore the cursor; `kv.scanobj`/`pair`/`first`/`next`/`find` still **position** it — save what you need first | | `arg.strings(arg.buf, 0, arg.n)` after `arg.take` on a list-or-string argument | `arg.take` unwraps a bare string, so `arg.buf` then looks like plain text. Run `arg.strings` on `arg.raw` instead — it handles a list, a string containing a list, and a bare string | | `time.parseiso` — checking the return value for failure | Failure is reported through `time.ok`, not the return: `-1` is ambiguous with one second before the epoch. See `library.md` for accepted shapes | **Two contracts for "it does not fit."** The `lib/` appenders (`io.push`, `io.pushnum`, `json.putraw`, `json.putstr`, `json.escslice`, `json.putreal`) **truncate** at `len(dst)` and hand back a usable position — a shortened message is still a message. A generated `.write`, `json.putslice` and `json.putb` **refuse** with `-1` — a shortened JSON object is a syntax error, not a smaller object. Always test `Out.write`'s result; never assume a chain of `io.push` wrote everything without comparing positions. **Names collide across the constant/routine namespace, case-insensitively**: `STORE.SET` and `store.set` are the same name. Since 15-09-2026 the compiler says so (`duplicate declaration; names are case-insensitive, so STORE.SET is the same name as store.set`) — pick a prefix that isn't already a routine's rather than a spelling that happens to slip past. The module `tools` imports `http` (for `tool.rest`), so a program importing it must define `procedure app.request`, or get `forward declared routine is never defined` on the last line. A stdio-only MCP server imports `toolsmcp` instead — the MCP half without HTTP, needing no `app.request`. ### Working copies (one global record per entity) - **Never pass a field of the working copy into a routine that reloads that working copy.** `uc.winner(dd.k.curve_ref)` reloads `dd.k` while scanning: the slice ends up pointing at changed content, with no error and a partly-correct result. Copy the value into your own buffer first. - A `profile.load(other)` mid-edit overwrites your changes — directly, or indirectly through a scan helper (`ctx.default`, `ctx.owns`, `ctx.selectall`) that touches `dd.*`. **Rule:** new scan/search helpers use their own copy, never `dd.*`; a handler loads what it writes out itself rather than trusting what a helper left behind; handle other rows first, then load/change/save your own. ### Syscalls and memory | Wrong | Right | |---|---| | `sys3(SYS.open, addr(path[0]), flags, mode)` without a terminator | The path must be null-terminated: `path[n] := chr(0)` | | Combining open flags with `+` or `or` | `bor(bor(O_WRONLY, O_CREAT), O_TRUNC)` | | Expecting `peek8`/`peekreal` | They don't exist — `peek(a)` reads one byte. Read an int or real via a typed one-element view: `f(view(a + off, 1))` | | Ignoring `mmap`'s return | Negative means failure (`-errno`); check `p < 0` | | Treating addresses as needing alignment | Ordinary `int`s; `view(p + offset, n)` reads through them — a `char` view is bytes, an `int` view is 8 bytes/element | Everything is static: no `new`, no recursive structures through pointers — use array indices instead (language.md §10b). ### A wrong result that looks like a choice Most pitfalls above announce themselves: a compile error, a crash, a wrong number. The expensive ones don't — they produce output that's plausible, and a plausible result invites tuning instead of debugging. - **If you change an input and the output doesn't change, you're tuning the wrong variable.** Two rounds of this means stop adjusting and start measuring. - **Measure the data, not the rendering.** Print the actual values — maxima, counts, ranges — rather than judging the picture. - **Know the healthy range before you look.** State the expected bound first, then measure. - **Suspect off-by-a-factor before off-by-a-pixel.** A value at 1/8th or 1/64th of what it should be points at an early exit or a mismatched divisor, not a slightly-off formula. A loop that counts and a divisor that disagree is the classic source: ``` // WRONG: break leaves the inner loop, so hits can never exceed n -- // yet the divisor is still n * n. Everything comes out at 1/n of its true value. for i := 0 to n - 1 do for j := 0 to n - 1 do if inside(i, j) then begin hits := hits + 1; break; end; value := hits * 255 div (n * n); ``` The result was usable, just uniformly wrong — nothing ever flagged it. Remove the early exit. ### Spec versus compiler (findings) - `and`/`or` are short-circuit (the spec said otherwise at first; corrected). - Identifiers are case-insensitive (the spec first said case-sensitive; corrected). - After every routine, `nloc` must be 0; a compiler bug here produced stale locals in constant expressions (fixed). --- ## Patterns Everything below compiles with the current `wantzel`. Take the nearest pattern and adapt it; don't invent a variant of something listed here. ### The argument order of the routines you use most 30 of 100 recorded mistakes are library use — a wrong name, or arguments in the wrong order. Two shapes cover almost all of it. **Appenders — `(buffer, at, what)`, returning the new position:** | | | |---|---| | `io.push(b, at, s)` | a string literal, returns the new `at` | | `io.pushnum(b, at, v)` | a number | | `json.putstr(dst, at, s)` | a literal, quoted and escaped | | `json.putslice(dst, at, src, from, upto)` | bytes from another buffer, quoted and escaped | | `json.putraw(dst, at, src, from, upto)` | bytes verbatim, no quotes | | `json.putreal(dst, at, v)` | a real | | `json.putb(dst, at, c)` | one byte | | `kv.text(b, dst, at)` | the cursor's value, decoded — source first, then dst | ``` n := io.push(buf, 0, "{\"name\":"); n := json.putstr(buf, n, "probe"); n := io.push(buf, n, "}"); ``` **Readers — `(buffer, at, upto)`, `upto` exclusive:** | | | |---|---| | `json.ws(b, at, last)` | skip whitespace, first non-blank position | | `json.skip(b, at, last)` | past one whole value | | `json.string(b, at, last)` | past a string; text is `b[json.sat..json.send)` | | `json.eq(b, at, last, s)` | exact match — count the literal including its quotes | | `kv.find(b, at, upto, key)` | key as `array of char`, not `str` | | `kv.first(b, at, upto)` / `kv.next(b, cur, upto)` | walk an object | ### What each routine gives you on failure | kind | on failure | why | |---|---|---| | appenders in `lib/` (`io.push`, `json.put*`) | truncate at `len(dst)`, return a usable position | a shortened message is still a message | | generated `.write`, `json.putslice`, `json.putb` | `-1`, nothing usable written | half a JSON object is a syntax error | | parsers (`json.skip`, `json.string`, `kv.pair`) | `-1` | no position to continue from | | questions (`kv.find`, `json.eq`, `fs.stat`) | `false` | | | generated `.parse` | `-1`, offending key in `json.badkey0..json.badkey1` | a missing/bad field; an unknown key is not a failure — see `json.ignoredn` | | `kv.text` | `-1` | not a string, or doesn't fit | Always test the result of `write` and anything returning `-1`. ### A buffer and its length, together ``` var buf: array[0..1023] of char; buf_n: int; // how much of buf is filled buf_n := io.push(buf, 0, "text"); if buf_n >= len(buf) then ... // it truncated: the position hit the end ``` Pass the filled part as a slice, `use(buf[0..buf_n-1])` — passing `buf` hands over 1024 bytes of mostly zero, and the receiver can't tell where the text stops. ### A whole command-line tool The usage line, the argument copied out with its length guarded, the failing `open` reported, and a distinct exit code for "you used it wrong" (2) versus "it did not work" (1) — nothing here is spare. ``` import io; var path: array[0..1023] of char; buf: array[0..65535] of char; fd, n, i, lines, bytes: int; begin if argc() < 2 then begin io.puts(STDERR, "usage: wc \n"); halt(2); end; i := 0; while argch(1, i) <> chr(0) do begin if i >= len(path) - 1 then begin io.puts(STDERR, "wc: the path is too long\n"); halt(2); end; path[i] := argch(1, i); i := i + 1; end; path[i] := chr(0); // a syscall wants it NUL-terminated fd := sys3(SYS.open, addr(path[0]), O_RDONLY, 0); if fd < 0 then begin io.puts(STDERR, "wc: cannot open that file\n"); halt(1); end; lines := 0; bytes := 0; while true do begin n := io.read(fd, addr(buf[0]), len(buf)); if n <= 0 then break; bytes := bytes + n; for i := 0 to n - 1 do if buf[i] = chr(10) then lines := lines + 1; end; sys1(SYS.close, fd); io.putn(STDOUT, lines); io.puts(STDOUT, " lines, "); io.putn(STDOUT, bytes); io.puts(STDOUT, " bytes\n"); end. ``` ### A whole MCP server **Do not build `tools/list` or a call reply by hand with `mcp.add`.** The `tools` block (language.md §7b) generates the tool table, JSON Schemas, argument parsing, dispatch and result writers from one declared line per tool; only the handler is left to write. Two lines are easy to leave out and neither is optional: ``` import json; type AddArgs = schema a: int "the left operand"; b: int "the right operand"; end; type AddResult = schema sum: int; end; tools add(AddArgs): AddResult "Add two whole numbers." readonly idempotent; end; import toolsmcp; // AFTER the tools block: it reads it function tool.add(a: array of AddArgs; r: array of AddResult): int; begin r[0].sum := a[0].a + a[0].b; return 0; end; begin mcp.stdio; end. ``` That answers `initialize`, `tools/list` and `tools/call` on stdin; `mcp.name`/`mcp.version` are yours to set. To also serve the same table over HTTP, import `tools` (adds `tool.rest`, brings in `http`) and `mcphttp`, define `procedure app.request`, and call `http.serve(port, 1)`; `examples/mcptools.wz` does both. ### Program skeleton with an import ``` import io; // the library: a module name, no quotes, no path const MAX = 10; var i, n: int; buf: array[0..1023] of char; procedure show(s: str; v: int); begin io.puts(STDOUT, s); io.putn(STDOUT, v); io.puts(STDOUT, "\n"); end; begin n := 0; for i := 1 to MAX do n := n + i; show("sum = ", n); end. ``` ### Command-line arguments ``` var port, i: int; c: char; dir: array[0..255] of char; dirn: int; ... port := 0; i := 0; while true do begin c := argch(1, i); if c = chr(0) then break; port := port * 10 + (ord(c) - 48); i := i + 1; end; dirn := 0; while argch(2, dirn) <> chr(0) do begin dir[dirn] := argch(2, dirn); dirn := dirn + 1; end; store.opendir(dir[0..dirn - 1]) ... ``` ### Building text and writing it out ``` n := io.push(buf, 0, "item"); // returns the new position n := io.pushnum(buf, n, 42); buf[n] := chr(0); // only needed for syscalls (paths) io.out(STDOUT, addr(buf[0]), n); // buffer; io.puts is for literals only ``` Comparing text: `json.eq(buf, at, upto, "literal")` (upto exclusive). Passing text along: `f(buf[0..n-1])`; inside `f(s: array of char)`, `len(s)` is the length. ### Passing and storing records ``` type Item = record id: int; value: real; name: array[0..31] of char; name_n: int; end; var it: Item; items: array[0..99] of Item; nitems: int; procedure fill(dst: array of Item; id: int); // dst[0] is "the" record begin dst[0].id := id; dst[0].value := 1.5; end; ... fill(it, 1); // a record variable = a view of one element fill(items[3..3], 4); // one element of an array items[nitems] := it; nitems := nitems + 1; // a whole copy fill(view(store.rowaddr(tab, row), 1), 7); // a row in a store table if not store.set(tab, row, view(store.rowaddr(tab, row), addr(items[1]) - addr(items[0]))) then ... ``` ### A list of unknown length (without a heap) ``` var xs: array[0..9999] of int; nxs: int; // capacity + counter function push(v: int): bool; begin if nxs >= len(xs) then return false; xs[nxs] := v; nxs := nxs + 1; return true; end; ``` Declare large buffers globally; use a `view` on `mmap` memory to grow beyond that (language.md §10b). ### Linked structures without pointers: the index IS the pointer There are no pointers, and for data you don't need them. Where C keeps an address, keep the **index** of the element instead, with `-1` for "none". An index can't dangle, is bounds-checked at every use, and still means the same thing after a write-to-disk and read-back. One array holds every node, a counter says how many are live, every link is an `int` index: ``` type Node = record val, next: int; end; // `next` is an index, not an address var nodes: array[0..1023] of Node; nn, head: int; function alloc(v: int): int; // -1 when full: refuse, never truncate begin if nn >= len(nodes) then return -1; nodes[nn].val := v; nodes[nn].next := -1; nn := nn + 1; return nn - 1; end; ``` **Doubly-linked list, with removal from the middle:** ``` type Node = record val, prev, next: int; end; var nodes: array[0..1023] of Node; nn, head: int; procedure unlink(x: int); begin if nodes[x].prev >= 0 then nodes[nodes[x].prev].next := nodes[x].next else head := nodes[x].next; // x was the head if nodes[x].next >= 0 then nodes[nodes[x].next].prev := nodes[x].prev; end; ``` **A tree, and a graph with cycles** — same record with `left`/`right`, or an edge array. An index can't point at freed memory, so the "use after free" that makes cycles dangerous elsewhere can't happen; just guard the walk against revisiting: ``` procedure walk(x: int); var i: int; begin if seen[x] then return; // the only thing a cycle needs seen[x] := true; i := 0; while i < g[x].ne do begin walk(g[x].edge[i]); i := i + 1; end; end; ``` **Reuse: a free list, not an allocator.** Removing a node doesn't free memory — nothing was allocated. Keep freed indexes in a list of their own (`lib/store.wz` does this with `store.free`). **It survives a restart**: because a link is an index, the whole array is its own file format — write the raw bytes, read them back, every link still points where it did: ``` fd := sys3(SYS.open, addr(path[0]), 577, 420); // O_WRONLY|O_CREAT|O_TRUNC n := sys3(SYS.write, fd, addr(nodes[0]), nn * 24); // 24 = the record's size sys1(SYS.close, fd); ``` **What an index doesn't protect against:** it can point at the wrong row. A zeroed link field means index 0, not "none" — initialise links to `-1` explicitly, or a node becomes its own child and a walk recurses until the stack runs out. The compiler can't see this. ### Reaching memory the program never declared `view(addr, n)` turns any address plus a length into an ordinary, bounds-checked array — the escape hatch for OS memory, a shared segment, a mapped file: ``` base := sys6(SYS.mmap, 0, 4096, bor(PROT_READ, PROT_WRITE), bor(MAP_PRIVATE, MAP_ANONYMOUS), -1, 0); fill(view(base, 4096)); // an ordinary array of char fill(view(base + 10, 100)); // a window at an offset sys2(SYS.munmap, base, 4096); ``` `view` and `sys*` are the unsafe primitives — nothing verifies the address and length are right. Everything inside the view is as safe as any other array. ### List arguments (a small `arg.*` layer on top of json) ``` if not arg.take(a[0].keys_at, a[0].keys_end) then return tool.fail("keys: malformed"); n := arg.strings(arg.raw, 0, a[0].keys_end - a[0].keys_at); // on the RAW view, not arg.buf ``` `arg.take` strips quotes off a bare string; `arg.strings` still needs to see quotes to recognise `"abc"` as one item. On an object already copied elsewhere (say `ctx.pbuf`), the value is still raw: `arg.strings(ctx.pbuf, kv.vat, kv.vend)` directly. ### info filters (kv module): which side decides `kv.match(obj, .., filter, ..)`: every filter key must exist in `obj`; per key, the **stored** side decides the rule. | stored value | filter value | rule | |---|---|---| | not a list | a list | membership: stored value equals one element | | a list | anything | containment: stored list holds the filter value, or all of a filter list | | not a list | not a list | equality (`kv.same`) | So `{"tags":"a"}` matches a stored `["a","b"]`, and a stored list contains itself. An empty **filter** list against a non-list stored value matches nothing; against a stored list it matches everything (containment of nothing is vacuous). `kv.contains`/`kv.haselem` are the containment half standalone. ### Ten argument slots: an array counts double Three arrays (6 slots) leave room for four scalars. A routine with three buffers plus `(from, upto) × 2` plus a write position doesn't fit — drop the write position, pass a slice: `n := kv.merge(b, 0, bn, p, 0, pn, out[at..len(out)-1])`. ### Returning a scalar result without a var parameter ``` procedure co.schedule(...; n: array of int); // n[0] becomes the count var cnt: array[0..0] of int; ... co.schedule(..., cnt); k := cnt[0]; ``` Several results: one record through `array of R`, or one `array of int` with fixed positions. ### Writing a file and mapping it ``` n := io.push(path, 0, "data.bin"); path[n] := chr(0); fd := sys3(SYS.open, addr(path[0]), bor(bor(O_WRONLY, O_CREAT), O_TRUNC), 420); if fd < 0 then io.fatal("open"); if sys3(SYS.write, fd, addr(buf[0]), n) <> n then halt(2); sys1(SYS.close, fd); fd := sys3(SYS.open, addr(path[0]), O_RDONLY, 0); p := sys6(SYS.mmap, 0, bytes, PROT_READ, MAP_SHARED, fd, 0); if p < 0 then halt(3); total := curve.sum(view(p, bytes), 0, 525600); sys2(SYS.munmap, p, bytes); ``` ### HTTP handler (http module + router) ``` procedure app.request; var uid: int; begin if oauth.route then return; // OAuth endpoints first if http.pathis("/protected") then begin uid := oauth.verify; if uid < 0 then begin oauth.challenge; return; end; http.add("{\"user\":"); http.addn(uid); http.add("}"); http.finish(200, "application/json"); return; end; if router.get("/health") then begin router.text(200, "ok"); return; end; router.notfound; end; ... http.serve(port, 1); ``` ### Schema + tools (MCP/REST without the handwork) The `tools` block is the way to expose tools — never build `tools/list` or a call reply by hand with `mcp.add`. See language.md §7b for the full anatomy; "A whole MCP server" above has a complete, runnable program. Shortest form, for reference: ``` type AddArgs = schema a: int "left"; b: int "right"; end; type AddResult = schema sum: int; end; tools add(AddArgs): AddResult "Add two whole numbers." readonly idempotent; end; import tools; function tool.add(a: array of AddArgs; r: array of AddResult): int; begin r[0].sum := a[0].a + a[0].b; return 0; end; ``` ### A list argument (json?) in a tool: arg.strings on the raw view `arg.take(at, upto)` already unwraps a JSON string: `"child"` becomes `child` in `arg.buf`, and `arg.strings(arg.buf, 0, arg.n)` then sees no `"` or `[` and returns -1. Parse lists on `arg.raw` instead: `arg.take(at, upto); n := arg.strings(arg.raw, 0, upto - at)` — covers a list, a JSON string containing a list, and a single string. ### Reading an int or a real at a raw address `peek(a)` is the only read builtin and yields one byte; `peek8`/`peekreal` don't exist. Read eight bytes with a typed view of one element: ``` function db.geti(a: array of int): int; begin return a[0]; end; function db.getr(a: array of real): real; begin return a[0]; end; ... v := db.geti(view(address + offset, 1)); // an int x := db.getr(view(address + offset, 1)); // a real ``` The parameter's type decides what the view reads, not the call site. Measure byte offsets in a record with `addr(rec.field) - addr(rec.first)`, never by hand. --- ## From an error message to its cause The compiler's messages are terse and sometimes point at generated code instead of your own line. Fastest route from symptom to cause; add to it whenever you lose an hour to something not listed here. ### The message points at a file you did not write For example `wantzel: :45: undeclared identifier`, or a line deep inside `refdata.wz` — that's **generated** code (a schema, the `tools` block). | Symptom | Cause | Fix | |---|---|---| | `undeclared identifier` in `` | the module the generator uses isn't imported | `import json;` **before** the schemas — a schema parser calls `json.*` | | `name already used by a type` in generated code | a global shares a name with a schema **field** | rename your global; fields share the namespace inside generated routines | | `forward declared routine is never defined` on the last line | a library-required `app.*` hook is missing | search library headers for `forward` (`app.request`, `app.tools`, `app.authenticate`) | **Fast diagnosis:** don't look at the line number, look at the **name in angle brackets** — that's the schema or tool. Work out which library the generator needs (usually stated in the file's header) and whether a global shares a name with one of its fields. ### `undeclared identifier` on your own line | Symptom | Cause | Fix | |---|---|---| | the name exists, further down the same file | a routine must appear before its caller | move it up, or declare it `forward` | | the name is in another `src/` file | include order | move that file's include earlier | | the name is in a library module | a missing or late `import` | `import ;` above the first use — the message names the module | Quickest check: `grep -n "function |procedure " src/*.wz lib/*.wz`. If it's there, the problem is order, not spelling. ### Runtime, not compile time | Symptom | Cause | Fix | |---|---|---| | `array index out of range` in generated data | a buffer sized too small for what's written into it | the generator computes the size — count in anything you added | | an answer is half right (one group instead of two) | aliasing: a working-copy field passed into a routine that reloads that same working copy | copy the value into your own buffer first | | a test passes alone but fails in the suite | a port clash or shared state | give every test its own port/directory (from the process id) and clean up; otherwise look for a shared global or file two tests both write | | empty slice, `addr(s[0])` blows up | `n = 0` | guard with `if n > 0` before `addr` | ### Before you compile 1. **Count the argument slots.** Ten is the maximum, an array counts as two — four text buffers plus a scalar is already twelve. Make search keys state with setters (see `ref.q.*`) instead of parameters. 2. Check whether a name already exists: `grep -rn "\bname\b" lib/ src/` — the namespace is shared, case-insensitive, and includes schema **fields**. The costliest mistakes are namespace and ordering errors, and cost time because the message points at generated code. "What name is in the angle brackets?" gets you there in one step. --- # How-to Short recipes for specific tasks: a goal, the steps, and the pitfalls. Task-shaped, not reference-shaped: you want to build one specific thing, and each section says how. `design.md` explains why the language and compiler work the way they do; `writing-wantzel.md` is the reference for pitfalls and idioms. There is no toolkit underneath any of this — the binary is kilobytes and nothing installs, and the price is that somebody has to know what the platform demands. That knowledge is below, so it only has to be paid once. ## Getting the compiler The compiler comes with Wantzel Studio: every studio has it, and an agent in the studio uses it as `wantzel`; the binary is also offered on wantzel.com/download. The file is the compiler and the whole standard library: `import io;` works with nothing beside it, `wantzel --lib` lists the modules and `wantzel --lib io` prints one; `wantzel --version` says which version it is. ## Background work with fork `fork` needs no compiler change: `sys1`/`sys2`/etc. are builtins that translate straight to the SYSCALL instruction (two bytes: `0x0F 0x05`), and `SYS.fork`/`SYS.wait4` are ordinary constants in `lib/io.wz`. ``` pid := sys1(SYS.fork, 0); ``` | | in the parent | in the child | |---|---|---| | return value | the child's pid (`> 0`) | `0` | | on failure | `< 0` (`-errno`) | — | `fork` copies the whole process: code, data, open file descriptors, program counter. From there the two diverge. **What goes wrong, roughly by frequency:** | problem | fix | |---|---| | the child dies without writing anything (segfault, `halt`, OOM) | give every task a status that separates "started" from "finished"; treat a non-zero exit as failed | | nobody reaps the child | reap periodically with `wait4(..., WNOHANG)` inside your existing event loop | | two processes write the same store | strict division: the child writes only its own row, created by the parent before forking | | the child inherits open connections | close what you do not need, first thing in the child | | the task does not survive a restart | mark every "busy" task "aborted" at startup | **Limits:** | limit | value | symptom | |---|---|---| | processes per user | `ulimit -u` | `fork` returns `-EAGAIN` | | memory | copy-on-write | OOM killer | | file descriptors | inherited, `ulimit -n` | `accept` fails | | zombies | until the process table is full | `fork` returns `-EAGAIN` | The practical limit is almost always simultaneous task count, not memory — cap it and refuse cleanly above the cap; five lines. `http.serve(port, workers)` also forks, but statically: n processes made once at startup, each with its own listening socket on `SO_REUSEPORT`, the kernel distributing connections. That does not conflict with per-task forking, but **each worker reaps only its own children** — a task started in worker 3 can only be reaped there, so an answer read by worker 1 must come from disk, not memory. **Killing a background process is not the same as knowing it is gone:** - `kill` is a request, not a confirmation — follow with `wait` (only works on your own children), then poll `kill -0`, then `kill -KILL` if still alive. - Track a list of pids, not one variable — an overwritten variable silently forgets the first. - A shell `trap` does not fire if the shell dies under a timeout, or the child outlives the shell; a cleanup keyed to something unique to the run (e.g. its temp directory) catches what the trap misses. - PPID tells you how an orphan got there: PPID 1 means started as a daemon; the session leader's PPID means adopted after the real parent died. - "Memory does not come back" has several causes — split `Cached` (reclaimable) from `AnonPages` (not) in `/proc/meminfo`, and sum `ps -eo rss=,comm=` per command to find the real owner. Not here, deliberately: threads (a language change, and unneeded — processes with no shared state do the same job); `posix_spawn`/`vfork`/`clone` (reachable through `sysN` if ever needed, `fork` is the one that was measured); signals (a child that must stop cleanly is killed and its row marked, there is no handler). ## Serving HTTPS Three situations, three different answers — work out which one you are in before reaching for a certificate. | where your program runs | what it needs | |---|---| | own machine, reached as `localhost` | a self-signed certificate, or no TLS at all | | behind a tunnel (ngrok, Cloudflare, tailscale) | nothing — the tunnel terminates TLS | | public address, real name | Let's Encrypt, and the program gets it itself | **1. Localhost.** Let's Encrypt cannot issue for `localhost`, `127.0.0.1`, `.local`, `.internal`, or private ranges (10.x, 192.168.x, 172.16–31.x) — ACME proves control of a public name, and none of those are one. Traffic on loopback never leaves the machine, so not using TLS is often the right answer there. Otherwise, self-sign in-process with `lib/csr.wz`: ``` import csr; import tls; p256.setup; hn := io.push(host, 0, "localhost"); if not p256.genkey(d, qx, qy) then ... // a fresh key csr.setvalidity(io.realtime div 1000000000, 10); // ten years if not csr.selfsign(host, hn, d, qx, qy) then ... if not tls.setcert(csr.buf, csr.n, d) then ... // and serve with it ``` Ten years, because nothing re-checks this and an expiry only creates an outage on a machine talking to itself. Browsers will still warn — nothing vouches for a self-signed certificate, correctly. `examples/localhttps.wz` is the full working version. **2. Behind a tunnel.** TLS ends at the tunnel edge; your program never sees it: ``` browser --TLS (provider's certificate)--> tunnel edge --plain--> your program ``` No certificate needed, at the cost of the provider seeing your traffic in the clear and an address that usually changes per session. A tunnel is a development tool, not a production answer — except one configured to re-encrypt to you (Cloudflare "Full (strict)"), which puts you back in case 3. **3. A public address.** Real name, A record, ports 80 and 443 reachable — ACME works and the program does it itself: no certbot, no cron, no reverse proxy, no second process. A server on `lib/http.wz` needs one call more than a plain one: ``` import http; procedure app.request; begin http.add("hello over https\n"); http.finish(200, "text/plain"); end; begin // the name, a contact address, where the certificate is kept, staging or not http.https("www.example.com", "you@example.com", "/var/lib/hello/certs", true); http.serve(443, 1); end. ``` ``` ./bin/wantzel hello.wz hello sudo ./hello # ports 80 and 443; or grant the binary cap_net_bind_service ``` Try it against **staging** first (the `true`) — its certificates are not trusted by browsers, but its rate limits are generous; change it to `false` for a real certificate. The authority's address is found through DNS (`lib/dns.wz`, with the machine's nameservers). `examples/autocertd.wz` is the same server with command-line options for every setting. What the program does, in one loop: - **At start** it opens both ports, then reads the store (here `/var/lib/hello/certs`; `""` means `certs/` beside the binary). A certificate that is for this host, matches its key and has more than 30 days left is served at once and **not** ordered again — so restarts are free, which matters: Let's Encrypt limits certificates per name per week. - **Port 80** never serves the application. It answers the authority's `/.well-known/acme-challenge/` from memory while an order is pending, redirects everything else to `https://` (`301`, or `308` for a method other than GET and HEAD) once a certificate is installed, and says `503` before that. - **The order** runs one HTTPS request per loop turn, so port 80 keeps answering between them — the authority fetches the token while the order is pending. - **The certificate** gets a new key, is checked before use (right host, our key, in date, each link of the chain signed by the next), saved with its key in one atomic write, and installed with its intermediate. The next handshake uses it; nothing restarts. - **Renewal** is checked daily and happens when fewer than 30 days are left. - **When the authority fails** the current certificate stays in service, and the next attempt comes after 1 h, 6 h, then every 24 h. The schedule is stored, so a restart does not retry early. TLS terminates in that same loop, for many clients at once: each connection's session is fed the bytes that arrived, so a handshake advances as its records come in, and a client that stalls halfway holds only its own connection until the header deadline closes it. A renewed certificate goes in while connections are open: the next handshake uses it, open connections carry on. For development without an authority, `http.httpsfiles("cert.pem", "key.pem")` serves a certificate from files on any port; the details are in library.md. The log is one line per step on stderr, and says what is served and when the next attempt is: ``` autocert: www.example.com: certificate loaded from the store, valid until 2026-12-01 (68 days left) autocert: www.example.com: renewing: 29 days left autocert: www.example.com: order failed: the authority did not answer in time; the current certificate stays in use; next attempt in 1 h ``` **HTTP-01, not DNS-01** — DNS-01 needs API credentials for your DNS provider (a dependency this stack avoids) and is only required for a wildcard certificate. **Every request to the authority is verified HTTPS.** `autocert.start` fills the trust store with `ch.addsystem` when the program has not put roots in it: the machine's bundle (`/etc/ssl/certs/ca-certificates.crt`), or two built-in roots (ISRG Root X1/X2) on a scratch container with none. A connection whose certificate does not chain to one of them is refused, not used. For a private ACME authority, give its root with `--ca ` — then that root is the whole trust store. `lib/tls.wz` never fills the store itself, so including it trusts nothing quietly. ## Reaching a server by name `net.connect` takes four numbers; `lib/dns.wz` turns a name into them. Pick the form by where the call sits: | where | call | |---|---| | a command-line tool, or start-up | `fd := dns.connect("example.com", 443)` — resolve, then connect to the first address that accepts | | you want the addresses | `n := dns.resolve("example.com", addrs)` — blocks for at most the lookup's total time, 5 s | | inside an event loop | `q := dns.start(name)`, `dns.fd` in your epoll set, `dns.poll(q, addrs)` after each wake — nothing blocks | In an event loop a blocking lookup stops every other connection for as long as the nameserver takes, so a server that looks something up while it serves uses the third form — unless the lookup is rare and its stop bounded: `lib/autocert.wz` resolves the authority with `dns.resolve` for each of the handful of requests an order makes, every two months or so. `dns.wait` is the timeout to hand `epoll_wait`, so a lookup whose server went silent still moves on in time: ``` if not net.watch(ep, EPOLL_ADD, dns.fd, EPOLLIN) then ... // once q := dns.start("acme-v02.api.letsencrypt.org"); ... n := net.wait(ep, addr(ev[0]), 64, dns.wait); // -1 when nothing is pending n := dns.poll(q, addrs); // DNS.PENDING (0), a count, or a DNS.* code ``` Pitfalls: - **The connect after the lookup is a separate step.** `dns.connect` resolves and then calls `net.connect`, which blocks; in an event loop, resolve with `dns.start`/`dns.poll` and connect however your loop connects. - **Say which failure it was.** `dns.error(code)` gives a sentence; `DNS.NXDOMAIN` (no such name) and `DNS.TIMEOUT` (nobody answered) are different problems with different fixes. - **Test against a server of your own.** `dns.noservers` and `dns.addserver(127, 0, 0, 1, port)` replace the system's nameservers, so a test never depends on the network — the same pattern works for any test of yours. --- # Conventions Agreements that give direction without being compiler rules. Nothing here is enforced by the compiler; a program that ignores all of it is still a correct Wantzel program. For what the language means, see language.md, which is binding. For how it's written, see syntax.md. For how to write it well, see writing-wantzel.md. This file is the softest of the four, on purpose. ## How a convention arrives, and how it leaves ``` an idea -> a convention -> used in practice -> measured -> maybe the language ``` A convention is a proposal that has been written down and is being tried. It earns its place by being followed, and keeps its place by staying useful. **Promotion.** A convention followed long enough that departing from it is surprising, with a count behind it, is a candidate for language.md or the compiler — the same evidence bar any language change needs; popularity alone isn't it. **Removal.** A convention nobody follows gets deleted, not left standing. If it has drifted from what the code does, the code wins: describe reality or change it, but don't let the two disagree in silence. ## 1. The entry point of an application is `main.wz` **A program made of several files names its entry point `main.wz`.** Nothing else marks which file is the program: a `.wz` file is a program when it has a top-level `begin ... end.`, and that difference appears nowhere in the name. An editor that compiles the file you have open guesses wrong constantly — in one measured application, 21 of 22 source files were not the entry point, so `F9` on the wrong one answers: ``` wantzel: src/theme.wz:1: missing begin of the main program ``` **The rule, in three parts:** 1. An application of several files names its entry point `main.wz`. 2. A single-file program is named after itself: `hello.wz`, `cat.wz`, `httpd.wz` — a directory of eleven examples all called `main.wz` would be unusable, which is why this is a convention and not a compiler rule. 3. Several programs in one project each get their own directory with a `main.wz` in it. Every multi-file application measured already follows it, with one exception: **the compiler's own source file**, named throughout scripts and docs — renaming it is risk with no return, so it stays. New applications follow the rule; existing ones are left alone. **Not a compiler requirement**: `wantzel anything.wz out` works regardless, and the compiler never searches for a `main.wz` on your behalf — that would guess wrong the moment a directory holds two programs. ## 2. A module prefixes its names **Names that belong to one file share a prefix: `tree.paint`, `doc.save`, `run.go`.** The language has no namespaces, and `import` and `include` are textual — everything lands in one flat scope. The prefix is the namespace: it says where a name comes from, keeps two modules from colliding, and makes a name greppable to exactly one file. Already universal in `lib/` (`json.parse`, `io.puts`, `http.get`) and in every application of size. The prefix matches the file: `tree.wz` defines `tree.*`. Where a file is one obvious noun, the short form is fine — `doc.wz` defines `doc.*`, not `document.*`. ## 3. Constants are `SCREAMING_CASE` **A compile-time constant is written in capitals with underscores: `DOCMAX`, `FILE_SHARE_READ`, `TH_KEYWORD`.** Case doesn't distinguish names in Wantzel, so this carries no meaning to the compiler — it's for the reader, reliable enough that a tool can act on it: an editor can colour an all-capitals name without a symbol table and be right essentially always. That's the test for a convention worth keeping. ## 4. `agent-permissions:` says what a file is for **A comment in the first lines of a file says how it's meant to be treated:** ``` // agent-permissions: read ``` ``` # agent-permissions: none ``` ``` (an HTML comment containing) agent-permissions: read ``` Three values, and no others: | value | meaning | |---|---| | `read` | read it, do not change it | | `none` | do not open it at all | | (absent) | ordinary: read and write | **There is no `write` and no `delete`.** Writing is the default, so a marker for it would say nothing; whoever may write may in practice also replace a file with an empty one, so `delete` would promise a distinction that doesn't exist. **It's a hint, not a guard** — nothing enforces it, exactly as nothing enforces `.gitignore` or an SPDX line. Those are followed because they're unambiguous and cost nothing to read; an enforced permission would need a mechanism, storage and an override path. **Why a comment and not a filename rule.** The one thing that can't be inferred from a file is intent — reference material, a scratch file, or generated output never to hand-edit. A comment says it where a reader is already looking, at no runtime cost. **Where it's binding: the first 512 bytes, and nowhere else.** A marker halfway down a file is one you can hide, and it would allow two contradictory markers in one file. Reading only the head is also cheap enough to do for every file in a directory listing. **What precedes the marker doesn't matter** — `// `, `# ` and an HTML comment opener all lead to the same three words, which is what makes it work across languages without a comment parser per language. **An unknown value reads as ordinary.** A misspelling restricts nothing rather than restricting everything — it should never be possible to lock a file by typing it wrong. **Where it earns its place:** generated files first (a file a tool rewrites should say so, since a hand edit is lost on the next run), then reference material meant to be read rather than adapted. Not yet settled: whether the same vocabulary belongs on other kinds of notes, and whether a generator should emit the marker itself. ## 5. An error names the fact first and the repair after a dash **The shape is fixed, so the reader always knows which half is which:** ``` wantzel: t.wz:6: undeclared identifier: prnit -- did you mean print? wantzel: t.wz:9: a schema is declared as 'type X = schema ... end;' wantzel: t.wz:3: { } is not a comment; use // to the end of the line ``` **Before the dash: what's wrong, as a fact. After it: what to do, when the compiler can work that out.** The reader needs to know `prnit` doesn't exist first; the nearest name is help, not the finding. **A repair is offered only when it can be derived, never guessed.** The spelling of a declaration is derived; "check your spelling" is not — a wrong suggestion costs more than none, because the reader stops trusting the next one. | level | what it adds | when | |---|---|---| | the fact | file, line, what is wrong | always | | the parameter | which argument, when a call has several | when the message alone is ambiguous | | the repair | `-- did you mean X?`, `; use Y`, `, for example "Z"` | only when derivable | Most messages need only the fact — `identifier too long` is complete as it stands, padding it is noise. The parameter earns its place where a call has several arguments: "the result type of a tool is a schema" beats "an argument has the wrong type". This is a convention and not a rule because the compiler can't check it; the rest is judgement. ## 6. Keep the levels apart: a helper file carries the vocabulary **Code that reads like pseudocode is a consequence of putting the words in one file and the decisions in another**, not a style choice. Before, a routine that opens a file for reading: ``` fd := sys3(SYS.open, addr(path[0]), O_RDONLY, 0); if fd < 0 then begin io.puts(STDERR, "cannot open that file\n"); return false; end; base := sys6(SYS.mmap, 0, size, PROT_READ, MAP_SHARED, fd, 0); sys1(SYS.close, fd); if base < 0 then begin io.puts(STDERR, "cannot map that file\n"); return false; end; ``` This says "syscall 2, syscall 9, syscall 3"; it means "map this file". After: ``` base := fs.map(addr(path[0])); if base < 0 then begin io.puts(STDERR, "cannot map that file\n"); return false; end; ``` Not shorter for its own sake: the `close` now lives in one place, including the part every caller gets wrong (a mapping keeps its own reference, so the descriptor is done the moment `mmap` returns — a caller who doesn't know that leaks one per file). **The test for a mixed level:** read one routine and ask what it's about. If the answer needs two sentences — "it maps a file, and also mmap takes six arguments in this order" — the levels are mixed. A routine that mentions a syscall number is at the bottom level, and almost nothing else should be. **Where the vocabulary goes:** a file of its own, named after what it's about. `fs.wz` is about files; `fs.map`, `fs.open`, `fs.stat` are the words it contributes. The caller imports it and never reaches past it to a syscall. Same technique as §1, from the other side: that section says where files go, this one says why the result reads better. **Not a wrapper for everything.** A routine that only renames a syscall (`fs.read` calling `sys3(SYS.read, ...)` with the same arguments) adds a name and no meaning. The ones worth having combine steps (`fs.map` is a stat, an open, an mmap and a close) or remove a decision the caller shouldn't have to make — and don't hide what they cost: `fs.map` maps a whole file, and a caller mapping a gigabyte should see that from the name. ## Candidates, not yet conventions Written down so they aren't lost, marked clearly so nobody mistakes them for settled. - **Where tests live and what they're called.** Each repository decides for itself; worth an agreement once there are more repositories than the ones here. - **How an application lays out its directories.** `src/`, `docs/`, `bin/` is what everything does — not the same as having been agreed. Add to this list freely: the cost of a bad candidate is one paragraph, a bad language rule is everything downstream of it. --- # Design Why the language is the way it is, and how the compiler works underneath it. What the language is belongs in language.md, which is binding. This page argues why: the yardstick every change is weighed against, the choices that follow from it, and — in the second half — how the compiler itself is built. The case for the compiler, stated plainly by the maintainer: **it is strict, fast and verbose, has no dependencies, and gives errors an agent can act on.** Everything below explains that sentence. It does not argue it against other languages — no benchmarks, no essays — because the sentence is the argument. ## The yardstick > **A compact, fast, strict language, fit to be used in iterations by AI when agents write > code.** | word | what it decides | |---|---| | compact | the whole specification is readable in one sitting; an agent does not spend its attention re-reading it | | fast | a build is short enough to run on every change, which is what makes iteration possible at all | | strict | what the compiler refuses is what nobody has to review by hand | | in iterations | the unit of work is a cycle that runs many times, and every signal is read by a machine before a person sees it | Weigh a proposal against those four. Where it does not decide, say so instead of guessing — a yardstick used to justify a guess is worse than none, because the guess then arrives with a citation. ## The main choices, and their one-line reason | choice | reason | |---|---| | **No pointers, no heap, no garbage collector** | an index into an array cannot dangle, is bounds-checked at every use, and survives being written to disk — every linked structure normally built with pointers has been built this way instead | | **Strict typing, no implicit conversions** | what the compiler refuses is a review question nobody has to ask; a wrong call is a compile error, not a silent bug that ships | | **One way per concept** | a second way is a second thing to learn, review, and get wrong; `bool` and no `boolean`, `import` for the library and `include` for a file and no module system, errors and no warnings | | **No dependencies** | the standard library is part of the compiler, changes with it, and travels inside the compiler file; there is no package manager, so there is no mechanism by which a dependency could be added | | **No optimiser** | the generated code stays recognisable as the source you reviewed, and it is most of why compiling takes milliseconds | | **No warnings, only errors** | a grey zone between "wrong" and "fine, but" does not survive contact with time — it piles up and gets ignored; exit 0 has to mean correct | | **Schemas are compiled, not configured** | a `schema` or `tools` block is the parser, the writer and the JSON Schema — nothing to keep in sync, and a wrong field is a compile error instead of a run-time surprise | **`tools` deserves a separate mention.** It is the language construct that turns a group of routines into an MCP tool table, argument parsing, dispatch, REST route and OpenAPI document — all from one declaration. See language.md §7b. It solves problem 3 below in its sharpest form: a contract that would otherwise be five things that can disagree becomes one thing that cannot. ### Why a generator makes this stricter, not more permissive A hundred recorded mistakes in generated Wantzel put library misuse (wrong routine, wrong argument order) at 30 of 100 — bigger than syntax and namespace mistakes combined. A permissive language answers a wrong call by doing something; a strict one refuses. For a generator running in a loop, a refusal is a message the next iteration can act on; a silent wrong answer is a bug that ships unnoticed. That asymmetry is why the language stays narrow rather than growing convenience features. ### Three problems, answered once each | problem | what happens without an answer | what Wantzel does | |---|---|---| | **Dependencies** | a tree you did not plant, and "will this still build in five years" has no answer | zero dependencies as an invariant; one static binary, nothing to install on the target | | **Stacks that grew** | a build becomes compiler + bundler + transpiler + package manager + linter, each with its own way of failing | one binary, one pass, no AST, no intermediate representation; `hello.wz` compiles in ~3 ms | | **Errors that surface too late** | a mistake a compiler could catch instead ships to a user or a log nobody reads | declarations mandatory, signatures checked, every array index and division checked at run time with file and line | ### Where the idea does not hold No ecosystem — every library you need, you write. No debugger, no profiler, no IDE support beyond `printf` and tests. Almost nobody knows this language. No proven memory safety, only "no pointers" plus a test suite. Every compiler bug is your bug. A small language says no often, including to reasonable requests. That is a real price, and the honest answer for a lot of other work is to use something else. --- ## How the compiler works One pass, no AST, no intermediate representation: the compiler reads source once and emits machine code as it goes, backpatching forward jumps where they stand. It compiles itself in milliseconds. ### One file: the standard library travels in the compiler `wantzel` is one file. The standard library is part of it: `import io;` compiles the `io` of this compiler, read from the compiler's own file, and from nowhere else — no library directory on disk, no environment variable, no flag, no search order. The same source and the same compiler file give the same executable wherever either of them sits, and two compilers whose `--version` lines agree compile every `import` to the same text. **Why nothing overrides it.** A copy of a module on disk that wins over the built-in one is a second place an `import` can be answered from, and then the question "which copy did this build use?" has no answer in the output. Experimenting with a module is still one line: copy it and include the copy as a file (`include "./tls.wz";`) — a file is always a file. **Why it is not precompiled.** The library is kept as source, not as compiled tables: the compiler is one pass with no intermediate form, so "precompiled" would mean a snapshot of every symbol table, in a second format that has to change with every table. Measured, it would save at most 7% of a large program's compile time; parsing the library is not where the time goes. ### Unused routines never reach the executable Every module a program imports compiles in full, but only the routines it actually calls end up in the file: after parsing, the compiler builds the call graph from its own relocations and drops everything unreachable from the main block, before writing the ELF image. **How.** A routine's code is one contiguous range, and every reference to it (a call, a jump to data, a jump to bss) is a relocation recorded at the offset it sits at. A relocation's kind already says whether it is a call to a routine (as opposed to a data or bss reference), so the call graph does not need a separate pass over the source: a routine's own relocations, read back, are exactly the routines it calls. The main block and the entry code are the two roots; everything not reachable from them is dead. Live routines are then copied down over the gaps the dead ones leave, and every surviving relocation is shifted by however much its own bytes moved. **Always on, no flag**, because the output has to stay deterministic and a flag would be a second code path nothing exercises by default. **Skipped under `--debug`**: the debug sidecar's addresses are written into it as the code is generated, line by line, long before this pass knows what will move where. Rewriting the sidecar afterwards would be a second serialisation of every address; instead a `--debug` build keeps every routine, so its addresses are exactly the ones the sidecar recorded. It is therefore larger than a plain build, and not byte-identical to it. Measured: `hello.wz` with an unused `import tls;` — nothing from `tls` is called, so none of it survives — drops from 348 KB to 56 KB. A program with most of its imported modules genuinely in use shrinks by a smaller fraction: `examples/mcpfiles.wz` goes from 847 KB with 0.4.0 to 707 KB; `examples/serve.wz`, which uses less of what it imports, from 500 KB to 257 KB. ### One syscall layer Library code names no operating-system function. `lib/io.wz` writes a file with one line: ``` return sys3(SYS.write, fd, a, n); ``` and `sys3` emits the `SYSCALL` instruction, literally — the kernel, nothing in between. The call number is an ordinary constant in `lib/io.wz`, so the compiler knows no syscall by name and anything the kernel offers is reachable from source. There is no import table: the ELF this compiler writes has no dynamic section, no `PT_INTERP`, nothing for `ld.so` to do. A shared library that was not there at build time cannot be called — calling it as a **process** (`lib/proc.wz`, `fork`+`execve`, ~300 µs) is the answer that needs no compiler change. Events arrive on a descriptor and the program reads them in its own loop; nothing ever calls into the program, so the language needs no function pointer and has none. ### Memory: an index is the pointer No heap, no pointers, no garbage collector. Instead: 1. **All memory is an array with an upper bound.** A global array costs nothing until touched: pages are 4 KB each. Local arrays live on the stack and are zeroed. 2. **An index replaces a pointer.** It cannot dangle, is bounds-checked at every use, and still means the same thing after being written to disk and read back. 3. **Anything larger than the program wants to pin down comes through `mmap` and `view`**, which turns an address and a length into an ordinary, bounds-checked array — the capacity is disk or virtual memory, not RAM. | situation | pattern | |---|---| | a growable list | array + counter, `-1` past the bound | | text of unknown length | one arena array + `(offset, length)` slices | | linked structures (list, tree, graph) | index fields, `-1` for none, a free list for reuse | | key → value | open-addressed hash table on two parallel arrays | | bigger than RAM, or must survive a restart | `mmap` on a file; the array is the file format | The one thing an index cannot replace is the **address of a routine** — `addr(myroutine)` is rejected, because that is a function pointer, and nothing checks one. ### Debug info: the `.wzdbg` sidecar `--debug` writes the executable -- with every routine kept, since unused routines are only dropped from a plain build (see above) -- plus a text sidecar, `.wzdbg`, that a debugger reads to map addresses to source and memory to variables. Own format, not DWARF: the only reader is a debugger for this compiler, and DWARF needs a library on both ends that this project does not carry. | record | fields | holds | |---|---|---| | `wzdbg` | version | format version (this page describes `1`) | | `target` / `base` / `text` / `data` / `bss` / `entry` | address(+length) | where each image section lives — addresses in the file are process addresses directly, no relocation | | `file` | id, path | source file table; a path starting `<` has no source (generated runtime code) | | `line` | address, file, line, kind | maps code to source; `kind` is `s` (statement start — the only safe breakpoint), `p` (prologue), or `e` (implicit return) | | `func` / `main` | name, start, end, file, line, framesize, result type | one routine's code range and frame size, followed by its `param`/`local` records | | `param` / `local` / `global` | name, location, type, array flag, bounds | where a variable lives and how to read it | | `record` / `field` | name, size, fields | a record type's layout, for typed variables of that type | Frames are found through the **rbp chain**: `[rbp]` is the caller's saved rbp, `[rbp+8]` the return address. At a statement-start (`s`) line, `rsp = rbp - framesize` and every variable is in memory — nothing is kept in a register across statements, so that is always a safe stopping point. ### Calling convention | what | how | |---|---| | up to 10 parameters | `rdi, rsi, rdx, rcx, r8, r9, r12, r13, r14, r15` — the first six are the System V order, so the syscall builtins share the code | | array parameter | two registers: base pointer, then length | | result | `rax` (a `real` result is its IEEE-754 bit pattern in `rax`) | | frame set-up | `push rbp; mov rbp, rsp; sub rsp, framesize`, locals zeroed | ### Types, in memory | type | size | layout | |---|---|---| | `int` | 8 | two's-complement, little-endian | | `char` | 1 | one byte | | `bool` | 1 | `0` or `1` | | `real` | 8 | IEEE-754 double | | `str` | 8 | address of the first character; length is the 8 bytes before it; NUL-terminated; `0` is the empty string | | `record` | sum of fields | declaration order; `char`/`bool` fields 1-aligned, everything else 8-aligned, size rounded up to a multiple of 8 | ### What already works, beyond the standard library `sysN` emits the raw syscall instruction and the compiler knows no call by name, so anything the kernel offers is reachable without a compiler change — including things once assumed impossible: | capability | status | |---|---| | `fork`, copy-on-write, exit codes and signals, non-blocking reap | works, measured | | shared memory and locking between processes | works, measured | | several worker processes behind one listening socket | works (`http.serve`) | | durable storage with crash recovery | works (`lib/store.wz`: append-only log + snapshot) | | multiple writers to one store | not done — a deliberate choice, not a limit; the primitives (`MAP_SHARED`, `flock`) are already present | | TLS | no — handled by a process in front; the one honest exception to "no dependencies" | | threads | no, and stays no — separate processes do the same job without shared state | ### Measurements the design rests on Same machine (Intel Core Ultra 7 258V, 6 cores, 16 GB), same job where stated: counting a pattern in a 2 GB file, every implementation giving the identical answer. | measure | value | what it means | |---|---|---| | compile speed | a 189-line program: 3 ms. The compiler itself, 6,495 lines: ~9 ms (median of ten, 721,666 lines/s) | one pass, no optimiser — compiling is not a step you wait for | | binary size | 22,786 bytes, static, vs. 2,040,611 for the same program in Go | a factor of 89.6 with no runtime hidden on either side | | run time | 0.72 s (`mmap` + scan) vs. 0.82 s for the same approach in Go | no optimiser does not cost speed for this class of program; the gap against Go's `read()`-based version (1.17 s) is larger but is an I/O-strategy difference, not code generation | | generated-code mistakes | 30 of 100 recorded errors are library misuse, more than syntax and namespace combined | the strictness and the whole-program examples in writing-wantzel.md answer this category specifically | These numbers are narrow by design: one machine, one class of program (a tight byte loop, exactly where an optimiser has least to find), and the run-time lead is mostly `mmap`, not code generation. They are not a claim against other languages in general — only evidence for the specific trade made above. ### No pointers: what was tried before trusting it Nine linked structures were built without pointers — list, tree, graph with cycles, a heterogeneous AST, a free list, zero-copy views, arbitrary OS memory as an array — and all nine work, with two actively better off: a cyclic graph needs no ownership tracking (nothing to free), and an index-based structure written to disk and read back has every link still valid with no fix-up pass. The one place this does not extend is a **function** address, and the language has none. --- ## Influences Two languages shaped this one. **Turbo Pascal**, for what a short compile-run loop feels like, and for syntax that reads close to pseudocode. **Go**, for what a deployable artifact should be: a static binary, fast builds, a capable standard library, a deliberately small language. Wantzel pushes both further than either did: dependencies are not merely few but structurally impossible, and an API contract is a language construct rather than a generator run as a build step. ## What it comes down to One binary you can copy instead of a stack you have to install. A compiler that answers in milliseconds and says plainly what cannot work. One declaration where there were five descriptions that could disagree. The language had to be new to get there; that was a consequence, not the goal. --- # Routine index Every public routine of every module that is built into the compiler, generated from the library itself (`wantzel --lib ` prints the source of one). A routine marked `local` is private to its module and is not listed. The reference above explains the modules a program normally uses; this index is complete. ## acme ``` procedure acme.fail(m: str) function acme.find(b: array of char function acme.copy(b: array of char function acme.get(b: array of char function acme.readdir(b: array of char function acme.lower(c: char): int function acme.readnonce(b: array of char function acme.readstatus(b: array of char procedure acme.acctbody(email: array of char procedure acme.orderbody(host: array of char function acme.csrbody(d: array of int function acme.readorder(b: array of char function acme.getin(b: array of char function acme.ishttp01(b: array of char function acme.readauthz(b: array of char ``` ## aead ``` procedure ae.setkey(key: array of char) procedure ae.setnonce(nonce: array of char) procedure ae.tag(aad: array of char procedure ae.seal(aad: array of char function ae.open(aad: array of char ``` ## aes ``` function aes.xt(a: int): int function aes.mul(a: int function aes.rotl8(x: int procedure aes.setup procedure aes.addrk(r: int) procedure aes.shiftrows procedure aes.invshiftrows procedure aes.mixcolumns procedure aes.invmixcolumns procedure aes.encst procedure aes.decst procedure aes.encrypt(b: array of char procedure aes.decrypt(b: array of char function aes.setkey(key: array of char procedure aes.gmul procedure aes.ghash(b: array of char procedure aes.setiv(iv: array of char) procedure aes.gcminit procedure aes.inc32 procedure aes.ctrxor(buf: array of char procedure aes.gcmtag(aad: array of char procedure aes.gcmseal(aad: array of char function aes.gcmopen(aad: array of char function aes.cbcenc(iv: array of char function aes.cbcdec(iv: array of char ``` ## autocert ``` procedure autocert.logs(s: str) procedure autocert.logb(b: array of char procedure autocert.logn(v: int) procedure autocert.logbegin procedure autocert.logend procedure autocert.say(s: str) procedure autocert.logdur(ms: int) procedure autocert.logdate(t: int) function autocert.nowms: int function autocert.nowsec: int procedure autocert.defaults procedure autocert.copyin(dst: array of char procedure autocert.sethost(h: array of char) procedure autocert.setemail(e: array of char) procedure autocert.setdirectory(u: array of char) procedure autocert.setstore(p: array of char) procedure autocert.settiming(checkms: int function autocert.pin(name: array of char function autocert.lower(c: char): int function autocert.starts(b: array of char function autocert.statis(s: str): bool function autocert.parseurl(u: array of char function autocert.dotted(h: array of char function autocert.resolve(h: array of char procedure autocert.settimeout(fd: int function autocert.dial: int function autocert.readable(fd: int function autocert.header(name: str function autocert.number(b: array of char function autocert.findend(n: int): int function autocert.complete(n: int): bool procedure autocert.readhead function autocert.takebody: bool function autocert.fetch(url: array of char function autocert.readstat: bool procedure autocert.readproblem function autocert.problemis(t: str): bool function autocert.pemblock(b: array of char function autocert.pemput(dst: array of char procedure autocert.keyder(d: array of int function autocert.same(a: array of char function autocert.readkey(b: array of char procedure autocert.setname(suffix: str) function autocert.save(name: array of char procedure autocert.saveretry(at: int) function autocert.readchain(b: array of char function autocert.check(qx: array of int function autocert.install(d: array of int procedure autocert.loadaccount procedure autocert.loadcert procedure autocert.loadretry function autocert.start: bool procedure autocert.moveto(st: int procedure autocert.failorder procedure autocert.failreply(what: str) function autocert.post(url: array of char function autocert.postget(url: array of char function autocert.retrynonce: bool function autocert.nextpoll: int procedure autocert.stepdir procedure autocert.stepnonce procedure autocert.stepaccount procedure autocert.follow procedure autocert.steporder procedure autocert.stepauthz procedure autocert.stepchallenge procedure autocert.finddetail(from: int procedure autocert.steppollauthz procedure autocert.stepfinalize procedure autocert.steppollorder procedure autocert.stepdownload procedure autocert.checkdue procedure autocert.tick function autocert.waitms: int function autocert.answer(path: array of char ``` ## base64 ``` function base64.sym(v: int function base64.enc(dst: array of char function base64.encode(dst: array of char function base64.urlencode(dst: array of char function base64.val(c: char): int function base64.decode(src: array of char ``` ## certstore ``` procedure cs.fail(m: str) function cs.exedir(dst: array of char): int function cs.open: bool function cs.opendir(path: array of char procedure cs.setname(name: array of char function cs.read(name: array of char function cs.write(name: array of char ``` ## chacha20 ``` function cc.rotl(v: int procedure cc.qr(a: int procedure cc.block(key: array of char procedure cc.xor(key: array of char ``` ## chain ``` procedure ch.fail(m: str) procedure ch.clearroots function ch.addroot(b: array of char procedure ch.takekey procedure ch.takekeyfrom(b: array of char) function ch.ecpoint(b: array of char function ch.ecpoint384(b: array of char function ch.rsakey(b: array of char function ch.derint(r: array of int function ch.derint384(r: array of int function ch.checksig(b: array of char function ch.setcert(i: int procedure ch.reset function ch.isca: bool function ch.samebytes(b: array of char function ch.issuedas(b: array of char): bool function ch.verify(b: array of char function ch.b64val(c: char): int function ch.b64(b: array of char function ch.starts(b: array of char function ch.addpem(b: array of char procedure ch.setupbaked function ch.addbaked: int function ch.addfile(path: str): int function ch.addsystem: int ``` ## csr ``` procedure csr.oid(which: int function csr.dersig(b: array of char procedure csr.digest(b: array of char function csr.algid(b: array of char function csr.spki(b: array of char function csr.name(b: array of char function csr.san(b: array of char function csr.make(host: array of char function csr.leap(y: int): bool function csr.mdays(y: int procedure csr.two(b: array of char function csr.puttime(b: array of char procedure csr.setvalidity(now: int function csr.selfsign(host: array of char ``` ## csv ``` function csv.s: char function csv.detect(b: array of char): char function csv.next(b: array of char function csv.needsquote(s: array of char): bool function csv.put(dst: array of char function csv.putn(dst: array of char function csv.putsep(dst: array of char function csv.puteol(dst: array of char ``` ## der ``` function der.next(b: array of char function der.enter(b: array of char function der.find(b: array of char function der.skip(b: array of char function der.oidis(b: array of char function der.readint(b: array of char function der.opentag(b: array of char function der.open(b: array of char function der.putlen(b: array of char function der.close(b: array of char function der.put(b: array of char function der.putoid(b: array of char function der.putnull(b: array of char function der.putint(b: array of char function der.putbits(b: array of char function der.putstr(b: array of char function der.putstrb(b: array of char ``` ## dns ``` function dns.parseip(s: array of char): int function dns.ipstr(dst: array of char function dns.parseip6(s: array of char function dns.ipstr6(dst: array of char function dns.error(code: int): str procedure dns.noservers function dns.addserver(a: int procedure dns.settimeout(tryms: int function dns.loadconf(path: array of char): int function dns.fd: int procedure dns.flush function dns.start(name: array of char): int function dns.start6(name: array of char): int function dns.poll(q: int function dns.poll6(q: int procedure dns.cancel(q: int) function dns.wait: int function dns.resolve(name: array of char function dns.resolve6(name: array of char function dns.connect(name: array of char ``` ## ed25519 ``` procedure ed.set(r: array of int procedure ed.pow(r: array of int function ed.isneg(a: array of int): int function ed.feq(a: array of int procedure ed.get(r: array of int procedure ed.put(p: array of int procedure ed.pzero(p: array of int) procedure ed.pcopy(r: array of int procedure ed.pcswap(p: array of int procedure ed.padd(r: array of int procedure ed.pneg(p: array of int) procedure ed.smul(r: array of int procedure ed.encode(out: array of char function ed.decode(p: array of int procedure ed.setup function ed.geL: bool procedure ed.subL procedure ed.modl(out: array of char procedure ed.muladd(out: array of char function ed.ltL(s: array of char): bool procedure ed.hashmodl(out: array of char) procedure ed.clamp procedure ed.public(pub: array of char procedure ed.sign(sig: array of char function ed.verify(pub: array of char function ed.frommont(pub: array of char procedure ed.negmodl(s: array of char) procedure ed.xsign(sig: array of char function ed.xverify(u: array of char function ed.sigverify(u: array of char ``` ## fs ``` procedure fs.dinit function fs.dfind(fd: int): int function fs.dslotfor(fd: int): int function fs.stat(a: int): bool function fs.isdir: bool function fs.isfile: bool procedure fs.forget(fd: int) procedure fs.close(fd: int) function fs.open(a: int): int function fs.opendir(a: int): int function fs.map(a: int): int procedure fs.unmap(base: int function fs.next(fd: int): bool function fs.find(h: array of char ``` ## hash ``` function hash.fnv(key: array of char): int procedure hash.clear(rows: array of int) function hash.count(rows: array of int): int function hash.same(keys: array of char function hash.slot(keys: array of char function hash.get(keys: array of char function hash.put(keys: array of char function hash.del(keys: array of char procedure hash.padkey(klen: int function hash.getpad(keys: array of char function hash.putpad(keys: array of char function hash.delpad(keys: array of char ``` ## hkdf ``` procedure hk.feed(b: array of char procedure hk.mac(key: array of char procedure hkdf.extract(salt: array of char procedure hkdf.expand(prk: array of char procedure tls.expandlabel(secret: array of char procedure tls.derivesecret(secret: array of char ``` ## hmac ``` procedure hmac.sha256(key: array of char ``` ## http ``` procedure app.request procedure http.maxbody(cap: int) procedure http.maxbodymem(total: int) procedure http.maxreply(cap: int) procedure http.movew(d: array of int procedure http.moveb(d: array of char procedure http.move(dst: int procedure http.xfree function http.rbuf: int function http.room(upto: int): bool function http.replyroom: int procedure http.append(src: int procedure http.start procedure http.hdradd(s: str) procedure http.hdraddslice(name: str procedure http.log(status: int procedure http.add(s: str) procedure http.addn(v: int) procedure http.addc(c: char) procedure http.addb(b: array of char procedure http.finish(status: int function http.hdreq(at: int function http.unchunk(at: int function http.parse(base: int function http.header(name: str): bool function http.bearer: bool function http.hexval(c: char): int function http.urldecode(src: array of char function http.lookup(b: array of char function http.param(key: str): bool function http.form(key: str): bool function http.pathis(s: str): bool function http.pathstarts(prefix: str): bool procedure http.init procedure http.maxconn(n: int) procedure http.timeouts(idle: int procedure http.addport(port: int) procedure http.bindto(a: int procedure http.redirect(port: int) procedure http.https(host: array of char function http.readfile(path: array of char): int function http.httpsfiles(certfile: array of char procedure http.nofile function http.slotfd(fd: int): int procedure http.slotfree(s: int) function http.slotopen(fd: int procedure http.tunkeep(s: int) procedure http.bfree(s: int) procedure http.closeslot(s: int) procedure http.drop(fd: int) procedure http.dropdetach(fd: int) function http.mask(s: int function http.waitout(s: int): bool function http.sendq(s: int function http.sendown(s: int function http.emit(s: int function http.emitreply(s: int): bool function http.tlsin(s: int function http.tkeep(s: int function http.pull(s: int procedure http.linger(s: int) procedure http.endslot(s: int) procedure http.swallow(s: int) procedure http.input(s: int) procedure http.rest(s: int) procedure http.drain(s: int) procedure http.flush(fd: int) function http.grow(s: int): bool procedure http.consume(s: int procedure http.plain(status: int procedure http.redirected function http.answer(s: int procedure http.refuse(s: int procedure http.bodystart(s: int) function http.bopen(s: int): bool procedure http.readbig(s: int) procedure http.readable(fd: int) function http.evict: bool procedure http.shed(fd: int) procedure http.pause procedure http.resume procedure http.acceptfd(lfd: int procedure http.accept procedure http.sweep function http.listener(fd: int): int procedure http.listen(port: int function http.poll(timeout: int): int procedure http.serve(port: int ``` ## io ``` function io.write(fd: int function io.read(fd: int procedure io.out(fd: int procedure io.puts(fd: int function io.digits(v: int): int procedure io.putn(fd: int procedure io.fatal(s: str) function io.pushnum(b: array of char function io.push(b: array of char function io.argstr(k: int function io.argn(k: int): int procedure io.put32(b: array of char procedure io.put64(b: array of char function io.get64(b: array of char function io.get16(b: array of char function io.get32(b: array of char function io.now: int function io.realtime: int ``` ## json ``` function json.ws(b: array of char function json.strend(b: array of char function json.string(b: array of char function json.number(b: array of char function json.literal(b: array of char function json.boolean(b: array of char function json.skip(b: array of char function json.eq(b: array of char function json.escaped(b: array of char function json.unescape(src: array of char function json.putstr(dst: array of char function json.escslice(dst: array of char function json.putslice(dst: array of char function json.isnull(b: array of char function json.real(b: array of char function json.putreal(dst: array of char function json.copystr(src: array of char function json.putb(dst: array of char function json.putraw(dst: array of char function json.indent(dst: array of char function json.pretty(src: array of char function json.wellformed(b: array of char ``` ## jsonschema ``` procedure sch.pushname(b: array of char procedure sch.pushindex(k: int) procedure sch.fail(m: str) function sch.kind(b: array of char function sch.member(b: array of char function sch.same(a: array of char function sch.milli(b: array of char function sch.whole(b: array of char function sch.strlen(b: array of char function sch.typename(b: array of char function sch.checktype(sc: array of char function sch.checkenum(sc: array of char function sch.checkrange(sc: array of char function sch.memberb(b: array of char function sch.propschema(sc: array of char procedure sch.notice(sc: array of char function sch.check(sc: array of char function sch.valid(sc: array of char ``` ## jws ``` function jws.b64(dst: array of char procedure jws.feed(b: array of char function jws.push(dst: array of char function jws.jwk(dst: array of char procedure jws.thumbprint(qx: array of int function jws.keyauth(dst: array of char procedure jws.setkey(d: array of int procedure jws.setnonce(b: array of char procedure jws.setkid(b: array of char function jws.signb(url: array of char function jws.keyauthb(dst: array of char function jws.sign(nonce: str ``` ## kv ``` function kv.vstart(c: char): bool function kv.pair(b: array of char function kv.scanobj(b: array of char function kv.isobject(b: array of char function kv.first(b: array of char function kv.next(b: array of char function kv.count(b: array of char function kv.text(b: array of char function kv.bytes(a: array of char function kv.find(b: array of char function kv.rtrim(b: array of char function kv.hasfrac(b: array of char function kv.same(a: array of char function kv.anyof(obj: array of char function kv.isarray(b: array of char function kv.haselem(a: array of char function kv.contains(o: array of char function kv.match(obj: array of char function kv.putc(dst: array of char function kv.put(dst: array of char function kv.putkey(dst: array of char function kv.merge(base: array of char ``` ## log ``` procedure log.begin procedure log.kv(key: str procedure log.kvs(key: str procedure log.emit(level: str procedure log.info(msg: str) procedure log.warn(msg: str) procedure log.error(msg: str) ``` ## lz ``` function lz.bound(n: int): int function lz.more(dst: array of char function lz.pack(src: array of char function lz.unpack(src: array of char ``` ## math ``` function math.abs(x: real): real function math.max(a: real function math.min(a: real function math.floor(x: real): int function math.ceil(x: real): int function math.isnan(x: real): bool function math.pow2i(k: int): real function math.exp(x: real): real function math.log(x: real): real function math.log10(x: real): real function math.log2(x: real): real function math.powi(x: real function math.pow(x: real procedure math.reduce(x: real) function math.sinr(r: real): real function math.cosr(r: real): real function math.sin(x: real): real function math.cos(x: real): real function math.tan(x: real): real function math.atan(x: real): real function math.atan2(y: real function math.asin(x: real): real function math.acos(x: real): real function math.radians(deg: real): real function math.degrees(rad: real): real function math.fixed(dst: array of char function math.text(dst: array of char function math.parse(s: array of char): real ``` ## mcp ``` function mcp.map(n: int): int procedure mcp.unmap(p: int procedure mcp.movew(d: array of int procedure mcp.moveb(d: array of char procedure mcp.move(dst: int function mcp.inlimit: int function mcp.outlimit: int procedure mcp.done function mcp.room(n: int): bool function mcp.esclen(b: array of char procedure mcp.add(s: str) procedure mcp.addn(v: int) procedure mcp.addq(s: str) procedure mcp.copyin(s: array of char) procedure mcp.addslice(b: array of char procedure mcp.addqslice(b: array of char procedure mcp.addesc(b: array of char procedure mcp.startreply procedure mcp.begin_result(b: array of char) procedure mcp.end_result procedure mcp.error(b: array of char procedure mcp.text(s: str) procedure mcp.textslice(b: array of char procedure mcp.textbytes(b: array of char procedure mcp.textnum(v: int) procedure app.tools procedure app.call(b: array of char function mcp.dispatch(b: array of char function mcp.limiterror(b: array of char): int function mcp.handle(b: array of char procedure mcp.sendline(fd: int procedure mcp.istatic procedure mcp.igrow function mcp.lines(b: array of char): bool procedure mcp.ishrink procedure mcp.stdio ``` ## mcphttp ``` procedure mcp.pvheader function mcp.httproom: int procedure mcp.httpanswer(b: array of char procedure mcp.http ``` ## money ``` function money.parse(s: array of char): int function money.put(dst: array of char function money.percent(cents: int ``` ## net ``` function net.socket: int function net.socket6: int procedure net.close(fd: int) procedure net.setopt(fd: int procedure net.setoptlevel(fd: int function net.nonblock(fd: int): bool function net.listenat(a: int function net.listen(port: int function net.listen6(port: int function net.connect(a: int function net.connect6(addr6: array of char function net.accept(lfd: int): int function net.send(fd: int function net.recv(fd: int procedure net.shutdown(fd: int function net.epoll: int function net.watch(ep: int function net.wait(ep: int ``` ## oauth ``` function app.authenticate(user: array of char function app.machinetoken(token: array of char): int procedure oauth.fail(m: str) procedure oauth.configure(issuer: array of char) procedure oauth.tables procedure oauth.rdclient(src: array of OClient) procedure oauth.wrclient(dst: array of OClient) procedure oauth.rdcode(src: array of OCode) procedure oauth.wrcode(dst: array of OCode) procedure oauth.rdtoken(src: array of OToken) procedure oauth.wrtoken(dst: array of OToken) procedure oauth.loadclient(row: int) procedure oauth.loadcode(row: int) procedure oauth.loadtoken(row: int) procedure oauth.rdres(src: array of OResource) procedure oauth.wrres(dst: array of OResource) procedure oauth.loadres(row: int) function oauth.saveclient(row: int): bool function oauth.savecode(row: int): bool function oauth.savetoken(row: int): bool procedure oauth.load function oauth.resput(h: array of char function oauth.resget(h: array of char procedure oauth.hex16(src: array of char procedure oauth.newsecret procedure oauth.hashtext(t: array of char) function oauth.sameid(a: array of char function oauth.samedigest(a: array of char function oauth.findclient(id: array of char): int function oauth.urlsafe(b: array of char function oauth.uriallowed(b: array of char procedure oauth.addissuer procedure oauth.metadata_resource procedure oauth.metadata_server procedure oauth.register function oauth.take(dst: array of char): bool function oauth.goodresource: bool function oauth.readrequest(fromform: bool): bool procedure oauth.addtext(b: array of char procedure oauth.hidden(name: str procedure oauth.authorize procedure oauth.login procedure oauth.issue(uid: int procedure oauth.tokenerror(status: int function oauth.clientauth: bool function oauth.tokenresource: bool procedure oauth.token procedure oauth.take_slice(b: array of char function oauth.readcookie: bool function oauth.bykind(kind: int): int function oauth.verify: int function oauth.wantshtml: bool procedure oauth.tologin procedure oauth.challenge function oauth.istls: bool function oauth.newsession(uid: int): bool function oauth.safenext(n: int): bool procedure oauth.loginpage(failed: bool) procedure oauth.dologin procedure oauth.logout function oauth.route: bool procedure oauth.expire ``` ## openapi ``` procedure oapi.scan function oapi.valueend(at: int): int function oapi.field(from: int procedure oapi.addslice(at: int function oapi.isrequired(nat: int function oapi.sub(from: int function oapi.istext(at: int function oapi.nameends(nat: int function oapi.nameis(nat: int function app.worthshowing(nat: int function oapi.worthshowing(nat: int function app.exarray(nat: int procedure oapi.exarray(nat: int procedure oapi.exuntyped(nat: int procedure oapi.exint(nat: int function oapi.namedvalue(nat: int function app.exstring(nat: int procedure oapi.exstring(nat: int procedure oapi.exvalue(sat: int procedure oapi.example(sat: int procedure oapi.spec(prefix: str procedure oapi.page(title: str) ``` ## p256 ``` procedure p256.zero(r: array of int) procedure p256.copy(dst: array of int procedure p256.cswap(a: array of int procedure p256.csel(r: array of int function p256.iszerobit(v: int): int function p256.iszero(a: array of int): bool function p256.isone(a: array of int): bool function p256.shr1(a: array of int): int function p256.cmp(a: array of int function p256.addc(r: array of int function p256.subb(r: array of int procedure p256.addm(r: array of int procedure p256.subm(r: array of int procedure p256.mul512(a: array of int procedure p256.redc(r: array of int function p256.pw(k: int): int procedure p256.sterm(w7: int procedure p256.accadd(r: array of int) procedure p256.accsub(r: array of int) procedure p256.redfast(r: array of int) procedure p256.mulm(r: array of int procedure p256.powm(r: array of int procedure p256.invm(r: array of int procedure p256.hexto(r: array of int procedure p256.setup procedure p256.dbl procedure p256.addgeneral procedure p256.addct(keep: int) procedure p256.add procedure p256.affine function p256.inrange(v: array of int): bool function p256.oncurve(x: array of int procedure p256.mulpoint(px: array of int procedure p256.mulacc(px: array of int procedure p256.combbase(px: array of int procedure p256.combbuild procedure p256.comblookup(digit: int) function p256.digitat(k: array of int procedure p256.mulbase(k: array of int) function p256.verify(qx: array of int procedure p256.frombytes(r: array of int function p256.sign(d: array of int function p256.genkey(d: array of int procedure p256.tobytes(b: array of char ``` ## p384 ``` procedure p384.zero(r: array of int) procedure p384.copy(dst: array of int function p384.iszero(a: array of int): bool function p384.cmp(a: array of int function p384.addc(r: array of int function p384.subb(r: array of int procedure p384.addm(r: array of int procedure p384.subm(r: array of int procedure p384.mul512(a: array of int procedure p384.redc(r: array of int function p384.pw(k: int): int procedure p384.term procedure p384.wihi(a: int procedure p384.wilo(g: int procedure p384.accadd(r: array of int) procedure p384.accsub(r: array of int) procedure p384.redfast(r: array of int) procedure p384.mulm(r: array of int procedure p384.powm(r: array of int procedure p384.invm(r: array of int procedure p384.hexto(r: array of int procedure p384.setup procedure p384.dbl procedure p384.add procedure p384.affine function p384.inrange(v: array of int): bool function p384.oncurve(x: array of int procedure p384.mulpoint(px: array of int procedure p384.mulacc(px: array of int function p384.verify(qx: array of int procedure p384.frombytes(r: array of int function p384.sign(d: array of int function p384.genkey(d: array of int procedure p384.tobytes(b: array of char ``` ## pg ``` procedure pg.seterr(s: str) function pg.be32(b: array of char function pg.be16(b: array of char procedure pg.put32(b: array of char function pg.wtext(at: int function pg.wzero(at: int): int function pg.send(n: int): bool function pg.fill(n: int): bool function pg.read: bool procedure pg.errfrom function pg.ready: bool procedure pg.hi(pw: array of char function pg.attr(b: array of char function pg.same(at: int function pg.scram(pw: array of char): bool procedure pg.close function pg.connect(host: array of char function pg.query(sql: array of char): bool function pg.next: bool function pg.value(i: int function pg.int(i: int): int function pg.colname(i: int ``` ## poly1305 ``` procedure po.init(key: array of char) procedure po.block(b: array of char procedure po.update(b: array of char procedure po.final(tag: array of char) procedure po.auth(key: array of char ``` ## proc ``` function proc.fork: int function proc.wait(pid: int): int function proc.reap: int function proc.exited(status: int): bool function proc.exitcode(status: int): int function proc.signal(status: int): int function proc.ok(status: int): bool function proc.kill(pid: int function proc.alive(pid: int): bool function proc.self: int procedure proc.arg0(path: array of char) procedure proc.arg(a: array of char) function proc.exec(path: array of char): int ``` ## protobuf ``` function pb.varint(b: array of char function pb.next(b: array of char function pb.i64(v: int): int function pb.zigzag(v: int): int function pb.i32(v: int): int function pb.f32(b: array of char function pb.asreal(a: array of real): real function pb.f64(b: array of char function pb.packn(b: array of char function pb.packnext(b: array of char function pb.find(b: array of char function pb.wellformed(b: array of char function pb.iseq(b: array of char ``` ## rand ``` procedure rand.bytes(b: array of char) function rand.int: int function rand.below(n: int): int ``` ## router ``` function router.get(path: str): bool function router.post(path: str): bool function router.post_under(prefix: str): bool function router.get_under(prefix: str): bool procedure router.error(status: int procedure router.notfound procedure router.methodnotallowed procedure router.json(status: int procedure router.text(status: int procedure router.cors ``` ## rsa ``` procedure rsa.fail(m: str) procedure rsa.zero(r: array of int procedure rsa.copy(dst: array of int function rsa.cmpn(a: array of int function rsa.subn(r: array of int procedure rsa.mul(a: array of int function rsa.shiftin(r: array of int function rsa.submul(r: array of int function rsa.addback(r: array of int function rsa.fits(r: array of int procedure rsa.redmod(r: array of int procedure rsa.mulmod(r: array of int procedure rsa.powmod(r: array of int function rsa.frombytes(r: array of int function rsa.bytelen(m: array of int): int function rsa.byteat(r: array of int procedure rsa.setdi function rsa.checkpkcs1(r: array of int function rsa.setkey(nb: array of char function rsa.verify(sb: array of char function rsa.hlen(hid: int): int procedure rsa.hash(hid: int procedure rsa.mgf1(hid: int function rsa.checkpss(hid: int function rsa.verifypss(hid: int function rsa.pssblock(hid: int ``` ## search ``` procedure search.clear function search.isword(c: char): bool function search.fold(c2: int): char function search.word(s: array of char function search.add(doc: int function search.find(query: array of char ``` ## sha1 ``` function sha1.rotl(x: int procedure sha1.block(b: array of char procedure sha1.hash(b: array of char function sha1.hex(digest: array of char ``` ## sha256 ``` procedure sha256.setup function sha256.rotr(x: int procedure sha256.block procedure sha256.init procedure sha256.update(b: array of char) procedure sha256.updateat(b: array of char procedure sha256.final(digest: array of char) procedure sha256.hash(b: array of char function sha256.hex(digest: array of char ``` ## sha384 ``` function sha384.shr(x: int function sha384.rotr(x: int procedure sha384.setup procedure sha384.block procedure sha384.init procedure sha384.runbuf procedure sha384.updateat(b: array of char procedure sha384.update(b: array of char) procedure sha384.pad procedure sha384.emit(digest: array of char procedure sha384.final(digest: array of char) procedure sha384.init512 procedure sha384.final512(digest: array of char) procedure sha384.hash512(b: array of char procedure sha384.hash(b: array of char ``` ## store ``` procedure store.crcsetup function store.crc(b: array of char function store.table(name: str procedure store.layout(t: int procedure store.upgrade(t: int procedure store.carry(t: int function store.upgraded(t: int): int function store.size(t: int function store.missing(t: int function store.convert(t: int function store.rowaddr(t: int function store.live(t: int function store.count(t: int): int function store.high(t: int): int function store.alloc(t: int): int procedure store.mkpath(suffix: str) procedure store.putu32(b: array of char function store.mapzero(bytes: int): int function store.putname(at: int procedure store.saylayout(t: int procedure store.sayversion(t: int procedure store.saysnapsize(t: int function store.apply(t: int function store.replay(from: int): int function store.loadsnap: int procedure store.setupconvert function store.opendir(dir: array of char): bool function store.write(t: int function store.set(t: int function store.del(t: int function store.snapshot: bool procedure store.close ``` ## time ``` function time.leap(y: int): bool function time.daysin(y, m: int): int function time.days(y, m, d: int): int function time.join(y, m, d, hh, mm, ss: int): int procedure time.split(t: int) function time.pad(dst: array of char function time.iso(dst: array of char function time.num(s: array of char function time.parseiso(s: array of char): int function time.weekdayof(t: int): int function time.isoweek(t: int): int function time.adddays(t: int function time.addmonths(t: int function time.putdmy(dst: array of char function time.parsedmy(s: array of char): int function time.cet(t: int): int function time.nowsec: int function time.nextat(now: int procedure time.sleepuntil(t: int) ``` ## tls ``` procedure tls.fail(code: int procedure tls.tradd(b: array of char procedure tls.trhash(out: array of char) procedure tls.copyin(dst: array of char function tls.derwrite(dst: array of char procedure tls.put8(b: array of char procedure tls.put16(b: array of char procedure tls.put24(b: array of char function tls.get16(b: array of char function tls.get24(b: array of char procedure tls.startclock function tls.arm(fd: int): bool function tls.waitfor(events: int): bool function tls.sendall(b: array of char function tls.recvall(b: array of char procedure tls.mknonce(iv: array of char function tls.sendrec(ctype: int function tls.readrec: bool procedure tls.keys(secret: array of char procedure tls.schedule procedure tls.finishedmac(dst: array of char) function tls.hello(host: str): bool function tls.readsh(b: array of char procedure tls.recordchain(b: array of char function tls.readcert(b: array of char function tls.readcv(b: array of char procedure tls.cvdigestfor(server: bool) procedure tls.cvdigest function tls.derint(r: array of int function tls.checkchain: bool function tls.rsakeyload: bool procedure tls.cvdigest384 function tls.derint384(r: array of int function tls.checkcvpss(hid: int): bool function tls.checkcv384: bool function tls.checkcv: bool function tls.readhs(out: array of char): int function tls.connect(fd: int function tls.connectb(fd: int function tls.write(b: array of char function tls.read(b: array of char procedure tls.close procedure tls.zero32(b: array of char) function tls.setcert(cert: array of char function tls.pool: bool function tls.valid(s: int): bool function tls.open: int procedure tls.free(s: int) function tls.state(s: int): int function tls.code(s: int): int procedure tls.enoncefor(iv: array of char function tls.putplain(ctype: int function tls.putsealed(s: int procedure tls.sfail(s: int function tls.hdrok(s: int function tls.fits(s: int function tls.unseal(s: int procedure tls.peeralert(s: int procedure tls.posthello(s: int function tls.onerec(s: int function tls.parsech(s: int procedure tls.ehash(b: array of char procedure tls.emac(key: array of char procedure tls.ecvdigest(th: array of char function tls.flight(s: int function tls.hello1(s: int procedure tls.hold(s: int function tls.feedin(s: int function tls.feed(s: int function tls.seal(s: int function tls.shut(s: int function tls.accept(fd: int): bool ``` ## tools ``` function tool.cutfieldsin(o: array of char function tool.solelistin(o: array of char procedure tool.restbodyof(o: array of char function tool.cutfields(from: int function tool.solelist(n: int): int procedure tool.restbody(n: int) function tool.runin(idx: int function tool.rest(prefix: str): bool ``` ## toolsmcp ``` procedure app.tools function tool.outlimit: int function tool.outready: bool procedure tool.outdone(n: int) procedure tool.takeignored(b: array of char) procedure app.call(b: array of char ``` ## uuid ``` procedure uuid.v4(u: array of char) function uuid.text(u: array of char function uuid.hexval(c: char): int function uuid.parse(s: array of char ``` ## websocket ``` procedure ws.setup function ws.slotfd(fd: int): int procedure app.wsframe(fd: int) procedure app.wsclose(fd: int) function ws.take(fd: int): bool function ws.send(fd: int function ws.sendempty(fd: int function ws.sendtext(fd: int procedure ws.dropconn(s: int) procedure ws.consume(s: int procedure ws.consumeframe(s: int) procedure ws.closeslot(s: int procedure ws.closefd(fd: int function ws.read(s: int): bool procedure ws.readframes(s: int) function ws.poll(timeout: int): int ``` ## x25519 ``` procedure x.zero(r: array of int) procedure x.copy(dst: array of int procedure x.one(r: array of int) procedure x.carry(r: array of int) procedure x.add(r: array of int procedure x.sub(r: array of int procedure x.mul(r: array of int procedure x.sq(r: array of int procedure x.mulsmall(r: array of int procedure x.invert(r: array of int procedure x.unpack(r: array of int procedure x.pack(b: array of char procedure x.cswap(a: array of int procedure x.scalarmult(out: array of char procedure x.base(out: array of char ``` ## x509 ``` procedure x509.fail(m: str) procedure x509.setoid(s: str) function x509.leap(y: int): bool function x509.mdays(y: int function x509.epoch(y: int function x509.digits(b: array of char function x509.time(b: array of char procedure x509.altnames(b: array of char procedure x509.basic(b: array of char function x509.parse(b: array of char function x509.lower(c: char): int function x509.hostmatch(b: array of char function x509.hostmatchb(b: array of char function x509.matchesb(b: array of char function x509.matches(b: array of char ```