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 conventionallyCapitalCase; values and fieldslower_case. - Integer literals are base-10 (
42,-7). Float literals have a dot (3.14,0.0). Bool literals aretrue/false.nullis the empty optional. - String literals use double quotes and support the escapes
\n \t \r \" \\ \0and\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.
# 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
| Syntax | Meaning |
|---|---|
int float bool string bytes | Primitive 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. |
Name | A 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 ! E | Fallible with an explicit typed error E (a sum type). |
Self | The 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
type User = { id: int, name: string, email: string? } # record
type Shape = Circle(float) | Rect(float, float) | Empty # sum typeA 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
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
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 selfpasses the receiver by reference (mutations persist); plainselfis by value; noselfmakes it a static method called asType.name(…).- A type with a
next(mut self) -> T?method is iterable byfor. mut selfpasses the receiver by reference (mutations persist); plainselfis by value; noselfmakes it a static method called asType.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
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
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 ShowType 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
:nameare typed parameters; a trailing*nameis a wildcard capturing the rest of the path (bound asstring). - A param is a path param by default, a query param with the
queryprefix, or the JSON request body with thebodyprefix. Query params may have= default. -> Tdeclares the response type (flows into the generated client). See Routes & Params.
State
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
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:
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
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
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
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:
# 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:
import "money.weld" as global
route GET "/p/:u" (u: int) -> string { respond "{quote(u, Currency.Usd).cents}" } # no prefixas 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
let siteName = "Weld Demo"
let pageSize: int = 20
let maxPageSize = pageSize * 5 # a later `let` may reference an earlier oneA 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
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
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:
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
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
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:
| Level | Operators | Notes |
|---|---|---|
| 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. |
| 7 | try ! - (unary) | Prefix: propagate-fallible, logical not, numeric negate. |
| 8 | f(…) 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 ( … ).
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 failureLiterals & constructors
| Form | Example |
|---|---|
| Record literal | User { id: 1, name: "Ada", email: null } |
| List literal | [1, 2, 3], [] (empty; element type from context) |
| Map literal | ["a": 1, "b": 2], [:] (empty) |
| Sum variant | Shape.Circle(2.0), Shape.Empty |
| Enum variant | Role.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
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.
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:
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 divergeSee Error Handling.
Statements
Full list with links to details:
- Binding:
let/var, reassignmentx = 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:
assertinsidetest— see Getting Started.
Semantic rules at a glance
- Static typing, no coercion.
intandfloatdon't mix implicitly; usestr/parseInt/intto convert. - Optionals are explicit. A
Tis nevernull; onlyT?can be, and you must handle it (??orelse) to reach theT. - Fallibility is in the type. Only
T!values needtry/else; resource calls (db/upstream/grpc/jwt_verify) returnT!. - Exhaustiveness. A sum-type
matchmust 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.