Skip to content

Language Reference

A single-page, exhaustive summary of Weld's syntax and semantics. The Language Guide explains each feature with worked examples; this page is the terse, complete reference — every keyword, operator, declaration, and statement form. For the standard functions, see Builtins & Globals.

Lexical structure

  • Comments start with # and run to end of line. There are no block comments.
  • Identifiers are [A-Za-z_][A-Za-z0-9_]*. Type names are conventionally CapitalCase; values and fields lower_case.
  • Integer literals are base-10 (42, -7). Float literals have a dot (3.14, 0.0). Bool literals are true / false. null is the empty optional.
  • String literals use double quotes and support the escapes \n \t \r \" \\ \0 and \u{HEX} (a Unicode codepoint, e.g. "\u{2603}" → ☃), plus {expr} interpolation. A doubled brace (two { or two }) is a literal brace. Interpolation holes may contain nested string literals without escaping the quotes: "url={env("BASE")}".
  • Whitespace and newlines are insignificant except as token separators; statements are not terminated by semicolons or newlines — the grammar is brace- and keyword-delimited.
weld
# a line comment
route GET "/x/:a" (a: int) {
    respond "value = {a}, escaped brace = {{literal}}\n"   # {{ -> a literal {
}

Keywords

route · type · enum · interface · fn · state · decorator · hub · database · upstream · grpc · service · import · test · let · var · require · if · else · for · in · match · return · try · fail · assert · transaction · every · listen · on · host · topic · metric · startup · worker · every · respond · status · type · header · cookie · emit · serve · connect · disconnect · method · injects · query · port · driver · body · rpc · stream · mut · self · true · false · null.

HTTP method tokens used by route / call: GET POST PUT PATCH DELETE.

Types

SyntaxMeaning
int float bool string bytesPrimitive scalar types.
T? · Optional<T>Optional — a T or null.
[T] · List<T>Mutable list of T.
[K: V] · Map<K, V>Map from key K (string or int) to V.
Stream<T>A lazy, consume-once sequence of T, walked by a for loop (server-streaming gRPC, streaming DB results). No .len / indexing.
NameA named record, enum, or sum type.
Name<A, B>A generic type instantiated with type arguments (usually inferred).
T!Fallible: a T or an error (built-in Error, or a declared type).
T ! EFallible with an explicit typed error E (a sum type).
SelfThe implementing type, inside an interface/method.

The capitalized constructors List<T>, Map<K, V>, Optional<T> are exact aliases for the [T] / [K: V] / T? sugars — use whichever reads better. Stream<T> is the lazy sibling of List<T>: both are consumed by for x in xs, so swapping one for the other changes how the sequence is produced (materialized vs one-at-a-time) without touching the loop.

Optionals, lists, maps, and fallibles compose: [User]?, [string: [int]], User ! LookupError, Stream<Order>.

Declarations

Top-level items. Order is not significant; forward references resolve.

Records and sum types

weld
type User  = { id: int, name: string, email: string? }        # record
type Shape = Circle(float) | Rect(float, float) | Empty       # sum type

A type after = is a record if it opens with {, otherwise a sum type (a |-separated list of variants with positional payloads; a leading | is allowed, and nullary variants like Empty need no parentheses). See Enums & Sum Types.

Record fields carrying @N are protobuf field numbers, used by grpc messages: type HelloRequest = { name: string @1, times: int @2 }.

Enums

weld
enum Role {
    Admin = 1 : "admin",     # number (required) : string label (optional)
    User  = 2,               # label defaults to the variant name
}

Proto-style: a hidden Unspecified = 0 always exists and is the fallback for unknown numbers/strings. Trailing commas are required between variants.

Functions and methods

weld
fn double(x: int) -> int { return x * 2 }         # free function
fn greet(name: string) { }                        # void return (no `-> T`)

fn Counter.inc(mut self) { self.n = self.n + 1 }  # method, pointer receiver
fn Counter.value(self) -> int { return self.n }   # method, value receiver
fn HashMap.new(cap: int) -> HashMap { … }         # static method (no self)
  • mut self passes the receiver by reference (mutations persist); plain self is by value; no self makes it a static method called as Type.name(…).
  • A type with a next(mut self) -> T? method is iterable by for.
  • mut self passes the receiver by reference (mutations persist); plain self is by value; no self makes it a static method called as Type.name(…).
  • A free function that reads request data (request_header, request_cookie, client_ip, form_field, upload, upload_filename) is request-scoped: the request context is threaded into it automatically, so you can factor auth checks and header parsing out of handlers. Such a function may only be called from a route handler, a decorator, or another request-scoped function — not from startup or a background task.

Function values

weld
fn double(n: int) -> int { return n * 2 }
fn apply(f: fn(int) -> int, x: int) -> int { return f(x) }   # takes a function, calls it

fn deny() -> bool { return false }
state auth { var check: fn() -> bool = deny }                # a callback stored in state
fn configure(c: fn() -> bool) { auth.check = c }             # installed once…
startup { configure(my_auth) }                               # …at startup

route POST "/admin" -> string {
    if !auth.check() { fail 403 "no" }                       # …and called per request
    respond "ok\n"
}

A function type is written fn(P…) -> R (drop -> R for a void callback). Reference a function by its bare name to get a value — pass it as an argument, store it in a let or a state field, and call it later. This is how a generic module states a contract (the parameter and return types a callback must have) and lets the caller supply the implementation — e.g. a custom authorization check for @weld/admin.

A function value may be called from any scope — a route handler, a fn, startup, a worker, an every task, a hub connect/disconnect/method handler, or a gRPC rpc. When called during an HTTP request the callback sees the live request; when called anywhere else it runs against an empty request, so any request builtins it uses (request_header, …) simply read nothing and return null. Storing or passing a callback is unrestricted too (that's what configure does from startup).

Generics and interfaces

weld
type Box<T> = { value: T }
fn Box<T>.get(self) -> T { return self.value }

interface Show { fn show(self) -> string }        # a named set of method signatures
type Labeled<T: Show> = { item: T }               # T constrained to Show

Type parameters are <T> (or several: <A, B>); a constraint <T: Iface> requires the argument to structurally satisfy an interface. Everything monomorphizes to Zig comptime generics — no runtime cost. See Generics & Interfaces.

Routes

route METHOD "path" (params)? ('->' Type)? { statements }
param := 'query'? 'body'? NAME ':' Type ('=' default)?
  • Path segments :name are typed parameters; a trailing *name is a wildcard capturing the rest of the path (bound as string).
  • A param is a path param by default, a query param with the query prefix, or the JSON request body with the body prefix. Query params may have = default.
  • -> T declares the response type (flows into the generated client). See Routes & Params.

State

weld
state db {
    var users: [User] = []
    var nextId: int = 1
}

Server-global mutable fields, accessed as db.users. Persisted into a durable allocator and guarded by a read/write lock. See State.

Decorators

weld
decorator require_role(role: string) injects (user: Claims) {
    let auth = request_header("Authorization") else { fail 401 "no token" }
    user = try jwt_verify(Claims, trimPrefix(auth, "Bearer "), env("JWT_SECRET"))
    if user.role != role { fail 403 "forbidden" }
}

@require_role("admin")
route GET "/admin" -> Claims { respond user }

A decorator runs before the handler (inlined, so no indirection); it can fail to reject, and its injects (…) bindings become typed variables in the handler. @name(args) attributes stack top-to-bottom. See Auth.

Resource blocks

Declarative external-service clients, each called as name.member(args) and fallible:

weld
database db  { url env("DATABASE_URL"); query findUser(id: int) "SELECT …" -> User }
upstream api { base env("API_URL"); header "Accept" "application/json"
               call getUser(id: int) GET "/users/{id}" -> User }
grpc Svc     { addr env("GRPC_ADDR"); rpc SayHello(Req) -> Reply }

See Databases, Upstreams, gRPC.

Hubs

weld
hub Chat {
    connect (query token: string) injects (user: Claims) { … }
    disconnect { … }                                  # runs when the socket drops
    method say(text: string) { … }
    method echo(text: string) -> string { return text }
}

A WebSocket endpoint at /<HubName>: connect runs once per connection (query params carry auth), the optional disconnect block runs when a socket drops (injected bindings in scope, may broadcast but cannot reply), methods are id-correlated request/response calls, and any method can push events via Chat.clients.filter(…).send(…) or count them with Chat.clients.count(). See Hubs.

Tests

weld
test "clamp bounds a value" {
    assert clamp(5, 0, 10) == 5
}

Top-level test blocks run by weld test (or --emit test); they compile away in the server binary. Inside a test, mock fakes any resource call. See Testing & Mocking.

Imports

weld
import "models.weld"            # flat: merge the file's declarations into one namespace
import "money.weld" as money    # namespaced module: reference members as `money.<name>`

A plain import merges another file's declarations into one program. Each file is included once (diamonds and cycles are safe); there is one global namespace. Use it to split routes, types, and helpers across files.

An aliased import "x" as ns loads a namespaced module. Such a module may declare fn, let, type, and enum (not routes or runtime resources), and they are reachable from the importer only as ns.name (a bare name does not resolve) — so two modules can define the same name without clashing. Inside the module, its own declarations are referenced unqualified; from the importer they are qualified:

weld
# money.weld
let cents_per_unit = 100
enum Currency { Usd = 1 : "USD" }
type Price = { cents: int, currency: Currency }
fn quote(u: int, c: Currency) -> Price { return Price { cents: u * cents_per_unit, currency: c } }

# app.weld
import "money.weld" as money
fn describe(p: money.Price) -> string { return money.show(p) }   # module type in a signature
route GET "/p/:u" (u: int) -> string {
    respond "{money.quote(u, money.Currency.Usd).cents}"          # fn + qualified enum variant
}

The qualified forms are ns.fn(args), ns.constant, ns.Enum.Variant, ns.Enum(x) (reinterpretation), ns.Type (in a type position), and ns.Type { … } (a record literal).

To pull a library into the root namespace instead — so its members are referenced without a prefix — import it as the reserved alias global:

weld
import "money.weld" as global
route GET "/p/:u" (u: int) -> string { respond "{quote(u, Currency.Usd).cents}" }   # no prefix

as global merges the library's declarations into the root, exactly like a plain import "x" does, but additionally asserts the file is a pure library (no routes or runtime resources). Multiple as global imports are allowed; because they share the root namespace, a name that collides with the root (or another global import) is a name clash — namespace one of them with a regular as alias instead.

Top-level constants

weld
let siteName = "Weld Demo"
let pageSize: int = 20
let maxPageSize = pageSize * 5     # a later `let` may reference an earlier one

A top-level let is a compile-time constant, usable by bare name from any handler, fn, or hub body. Values are evaluated at build time (so they're constant expressions); a local binding of the same name may shadow one. Forward references, duplicates, and assignment to a top-level let are errors.

Required environment

weld
require "JWT_SECRET", "DATABASE_URL"

Declares environment variables that must be set (non-empty) at startup. If any is missing the server prints a clear message and exits rather than handing handlers an empty env(…) string. Multiple names may be comma-separated, and require may appear more than once.

Reloadable config

weld
config from env("CONFIG_FILE") {
    greeting:    string = "hello"     # typed keys with compile-time defaults
    maintenance: bool   = false
}

config limits from env("LIMITS_FILE") {   # a second, named block, read as `limits.<key>`
    max_items: int = 20
}

route GET "/items" -> string {
    if config.maintenance { status 503  respond "unavailable\n" }
    respond "{config.greeting} — up to {limits.max_items}\n"
}

config [name] from <file> { … } declares typed configuration loaded from a JSON file. Keys may be int, float, bool, or string, each with a default used when the file omits (or mistypes) it. Read values in any handler as <name>.<key> — already typed, no parsing. The name is optional and defaults to config; you may declare several blocks (each from its own file) as long as each has a distinct name.

Each file is read once at startup and re-read on SIGHUP (kill -HUP <pid>): the new values are validated and swapped in atomically while the server keeps running — no restart, no dropped requests. If a new file is missing or invalid, that block's running config is left untouched and the error is logged. A from path is any string expression (typically env("…")); if a file is absent at startup, its keys take their defaults.

Calling reload_config() triggers the same reload from code — the primitive behind an HTTP reload endpoint. Import the built-in admin module to get one:

weld
import "@weld/admin"    # adds POST /admin/reload (guarded by ADMIN_TOKEN), /admin/healthz, /admin/readyz

@weld/admin is plain Weld embedded in the compiler — no file needs to exist on disk. It guards /admin/reload with an authorization callback that defaults to an ADMIN_TOKEN bearer-token check; install your own from startup with admin_authorize(fn() -> bool) — a function value — to authorize however you like. Or copy lib/admin.weld into your project and edit it directly; your version is in no way less capable than the built-in one.

Embedded files

weld
embed home from "assets/index.html"     # read at compile time, relative to this source
embed logo from "assets/logo.svg"

route GET "/"         -> bytes { type "text/html"      respond home }
route GET "/logo.svg" -> bytes { type "image/svg+xml"  respond logo }

embed <name> from "<path>" reads a file when weld compiles and bakes its contents into the binary as a bytes value, referenced by its bare name. The path is a string literal resolved relative to the .weld source. Because the data travels inside the binary, no file need exist on disk at runtime — the server is fully self-contained. Serve it by responding the value (set the content type with type), or use it anywhere a bytes value is expected. Each embed name must be unique. Unlike serve, which reads a file from disk per request, embed costs one compile-time read and adds the bytes to the binary.

Background tasks

weld
every 1m {
    let now = now_ms()
    # …sweep expired sessions, drop stale entries, etc.
}

every N<unit> { … } runs its body on a fixed interval off the request path (units: ms, s, m, h). It runs on its own thread and may read/write state, call functions, try/else, and broadcast to hubs — but has no request or response (no respond/status/header/fail). Not supported with the --green runtime yet.

Expressions

Operators, by precedence

From lowest (binds loosest) to highest:

LevelOperatorsNotes
1??Null-coalescing: a ?? b yields a unwrapped, or b if a is null.
2||Logical or (short-circuits).
3&&Logical and (short-circuits).
4== != < <= > >=Comparisons (non-associative).
5+ -Addition/subtraction; + also concatenates strings.
6* / %Multiply, integer/float divide, modulo.
7try ! - (unary)Prefix: propagate-fallible, logical not, numeric negate.
8f(…) x[i] x.field x.method(…)Postfix: call, index, field access, method call.

Integer arithmetic wraps on overflow (it never traps and aborts the server). Integer division/modulo by zero is guarded. Parenthesize with ( … ).

weld
let ok = a > 0 && a < 100          # comparison then &&
let name = maybe ?? "anonymous"     # coalesce
let greeting = "Hello, " + name     # string concat
let neg = -x                        # unary
let n = try db.count()              # propagate on failure

Literals & constructors

FormExample
Record literalUser { id: 1, name: "Ada", email: null }
List literal[1, 2, 3], [] (empty; element type from context)
Map literal["a": 1, "b": 2], [:] (empty)
Sum variantShape.Circle(2.0), Shape.Empty
Enum variantRole.Admin
Interpolated string"user {id} has {len(xs)} items"

Empty collections ([], [:], null) and generic record literals infer their types from the expected type (a let x: T, a -> T return, a field type, a filled element, or the left side of a ??maybe ?? []), so you rarely annotate them.

Field access, indexing, calls

weld
u.name                 # record field
xs[i]                  # list index (bounds-checked -> recoverable error)
m[k]                   # map lookup -> V? (null if absent)
counter.inc()          # method call
Shape.Circle(3.0)      # variant constructor (qualified)

match

An expression yielding a value; every arm must have the same type.

weld
let label = match role {
    Role.Admin => "admin",
    Role.User  => "user",
    _          => "other",       # `_` required for enums/ints/strings
}

let a = match shape {            # over a sum type: exhaustive, binds payloads
    Circle(r)  => 3.14159 * r * r,
    Rect(w, h) => w * h,
    Empty      => 0.0,           # no `_` needed when all variants are covered
}

match works over enums, ints, strings (each needs a _ default) and sum types (exhaustive, with payload binding — no _ if every variant is listed).

try / else

try e unwraps a fallible, propagating failure. let x = e else … { } handles a failure or unwraps an optional, with a diverging block:

weld
respond try upstream.call(id)                       # propagate (route -> error response)

let u = db.findUser(id) else e { status 404 respond e.message }   # handle Error
let v = maybe else { fail 400 "required" }          # unwrap optional or diverge

See Error Handling.

Statements

Full list with links to details:

  • Binding: let / var, reassignment x = e, field/index/map assignment.
  • Response: respond, status, type, header, cookie, emit, serve, fail — see Routes and Builtins.
  • Control flow: if / else if / else, for x in, for i, x in (indexed), for k, v in (map), match (expression), return — see Control Flow.
  • Fallible/optional: try, … else e { }, … else { } — see Errors.
  • Data: transaction db { } — see Databases.
  • Testing: assert inside test — see Getting Started.

Semantic rules at a glance

  • Static typing, no coercion. int and float don't mix implicitly; use str / parseInt / int to convert.
  • Optionals are explicit. A T is never null; only T? can be, and you must handle it (?? or else) to reach the T.
  • Fallibility is in the type. Only T! values need try/else; resource calls (db/upstream/grpc/jwt_verify) return T!.
  • Exhaustiveness. A sum-type match must cover every variant (or add _).
  • JSON is automatic. Return a record/list/sum → application/json; return a string → text. You never hand-write serialization.
  • No runtime cost for abstraction. Generics monomorphize; methods and interface dispatch lower to Zig pub fns inside comptime-generic structs.