Builtins
A builtin is a function the language already knows. You do not import it, declare it or define it — you call it. There are exactly one hundred, and the wcl CLI adds three more of wdoc's own, which is where the 103 in this chapter comes from.
The bulk of this chapter is a catalogue. Each entry carries one builtin's signature, its parameters, its return value, and an example whose printed answer came from a real run.
The five sections before the catalogue are the part you cannot get by looking one name up: how a call behaves, how to ask the language what it has, and which of the near-identical pairs to reach for.
§ 1One call, one answer
A builtin call is an ordinary call expression. Expressions and operators covers the syntax; four rules govern what a builtin does with it.
- Arity is fixed. Every builtin but one takes an exact number of arguments. Supplying the wrong count is an error at the call site, not a partially applied function — there is no currying in WCL.
- Arguments are positional. WCL has no keyword arguments and no defaults. A builtin that wants named options takes a record instead, so the option names are written at the call site.
- Arguments are values. Each one is evaluated before the call, left to right. A function argument — the fn(x: i64) -> i64 { … } you hand to map — is a value like any other; see Functions.
- A failure aborts the field. A builtin that cannot answer reports an error rather than returning a sentinel, and that error propagates out through whichever field forced it. try body catch e => fallback recovers one; see Control flow.
The one exception to fixed arity is format, which takes a template plus as many arguments as the template has {} slots.
Builtins are the last place the resolver looks
A name in call position is resolved against the document first — let bindings, fn items, fields — and only then against the builtin registry. So a fn len(xs: list<utf8>) -> u32 { … } of your own shadows the builtin inside its scope. The flip side is the error message: a name that resolves to nothing callable is reported as unknown built-in 'box', because the registry is where the search ended, not where it began.
§ 2A file that uses seven of them
Type this one and run it. It is a three-service fleet that reports on itself, and it calls map, sort, unique, max_by, sum, len and format to do it.
# release.wcl — one fleet, seven builtins.
@block("service")
type Service {
@inline(0) name: utf8
region: utf8
replicas: u32
}
@document
type Release {
@children("service") services: list<Service>
names: list<utf8>
regions: list<utf8>
busiest: utf8
replicas: u32
summary: utf8
}
names = sort(map(services, fn(s: Service) -> utf8 { s.name }))
regions = unique(map(services, fn(s: Service) -> utf8 { s.region }))
busiest = { let s = max_by(services, fn(s: Service) -> u32 { s.replicas }); s.name }
replicas = sum(map(services, fn(s: Service) -> u32 { s.replicas }))
summary = format("{} services over {} regions, {} replicas, busiest is {}",
len(services), len(regions), replicas, busiest)
service "web" { region = "us-east-1" replicas = 4u32 }
service "db" { region = "us-east-1" replicas = 2u32 }
service "cache" { region = "eu-west-1" replicas = 1u32 }
$ wcl check release.wcl
OK
$ wcl get release.wcl names
["cache", "db", "web"]
$ wcl get release.wcl regions
["us-east-1", "eu-west-1"]
$ wcl get release.wcl busiest
"web"
$ wcl get release.wcl replicas
7u32
$ wcl get release.wcl summary
"3 services over 2 regions, 7 replicas, busiest is web"
Three things in that file are worth naming before the catalogue starts. A gather field is a list<Service>, so every list builtin works on it directly. unique keeps first-seen order, which is why us-east-1 leads. And busiest wraps its call in a block expression: max_by returns a record-shaped value, and reading a field straight off a call — max_by(…).name — fails with expected a reference, got call. Bind it with a let first. That rule bites hardest in Reflection, where almost every builtin answers a record.
§ 3Asking the language what it has
The catalogue below was generated from the language itself, and you can regenerate it. builtin_names() answers every registered name, sorted, and fn_signature(name) answers that builtin's documentation as a record.
@document
type Catalogue {
count: usize
first: list<utf8>
sig: utf8
}
count = len(builtin_names())
first = take(builtin_names(), 3)
sig = { let s = fn_signature("map"); s.signature }
$ wcl get catalogue.wcl count
102
$ wcl get catalogue.wcl first
["__wdoc_slot", "abs", "acos"]
$ wcl get catalogue.wcl sig
"fn(xs: [T], f: fn (T) -> U) -> [U]"
The count is 102 rather than 100, and the first name is one you did not write, because the environment a builtin lives in belongs to the host, not to the language. The wcl CLI is a wdoc-aware host: it registers wdoc's two builtins into every document it opens, whether or not the document imports wdoc. The bare language registers one hundred. wcl repl shows both, because a REPL started with a file opens it the way the CLI opens any document, and a REPL started without one evaluates against an empty document in the plain language environment:
$ echo 'len(builtin_names())' | wcl repl
100
$ echo 'len(builtin_names())' | wcl repl release.wcl
102
The hundred are registered by five modules of wcl_lang, and this chapter groups them by what they are for rather than by which module holds them.
| Module | Registers | Covered in |
|---|---|---|
| collections | 54 | Lists, Records, Strings, Tensors, Failing on purpose |
| math | 29 | Math |
| reflect | 12 | Reflection |
| paths | 3 | Paths and globs |
| units | 2 | Units |
| wdoc, via the host | 2 | Builtins the host adds |
§ 4Choosing between the near-twins
Several builtins answer nearly the same question, and picking the wrong one of a pair is the most common way to be surprised. This is the table you cannot build by reading either entry on its own.
| When you want | Reach for | Not | Because |
|---|---|---|---|
| A substring test | contains(s, needle) | list_contains | contains is strings only and fails on a list; list_contains accepts a list or a tensor and answers false for anything else, silently |
| The matching element | find(xs, pred) | index_of / list_contains | find answers the element or none, index_of answers a position or -1, list_contains answers a bool. Three questions, three shapes |
| A sub-list by position | slice(xs, a, b) | take + drop | slice clamps both bounds and also reads a string; take/drop are the one-sided forms and are lists only |
| The smallest of a list | min_by(xs, key) | min(a, b) | min compares exactly two numbers. min_by walks a list and answers an element, or none when it is empty |
| A sorted list | sort(xs) | sort_by(xs, key) | sort orders numbers or strings directly and fails on a mixed list; sort_by is for everything else, and is stable |
| A string built from parts | format(t, …) | concat / join | format renders any value into a {} slot; concat and join demand strings. The interpolated $"…${x}…" literal renders the same way format does — see Values and primitives |
| To stop on a bad value | assert(cond, msg) | error / panic | assert is the guard form; error and panic abort unconditionally. All three fail the same way, and try/catch recovers all three |
| A byte size rendered | format_unit(v, type, unit) | format_unit_value | format_unit reads the factor from the type's @unit decorator, so it survives a change to the factor. format_unit_value takes the factor from you and checks nothing |
| To know if a path is covered | path_contains(p, c) | glob_match / glob_overlaps | path_contains is a segment-aware prefix test between two concrete paths. glob_match tests one path against a pattern; glob_overlaps tests two patterns against each other |
One pair deserves its own sentence. error and panic are the same function under two names — identical implementations, identical error: <msg> output, and both equally catchable by try/catch. Nothing about panic is more final than error. Choose between them for the reader, not for the runtime.
§ 5How to read an entry
Every entry below carries four things: the signature, a one-line description, a table of parameters and the return, and an example. The signature is the one the language reports for itself through fn_signature, which uses an informal notation — it is documentation, not a type you may write.
| In a signature | Means | Legal as a field type | Legal as a parameter type |
|---|---|---|---|
| [T] | A list of any one element type | No — write list<T>, see Lists, tensors and records | No — write list<T> |
| T / U / K / A / B | A type variable: whatever you pass, or whatever your callback returns | No — WCL has no generic declarations | No |
| number | Any numeric value, widened to f64 on the way in | No — write i64, u32, f64, …, see Values and primitives | Yes |
| record | A record value, or a union variant with a record body | No — declare a type of your own, see Lists, tensors and records | Yes |
| &T | A reference to a declaration — a type, field, block or variant named in an expression | Yes — see Types | Yes |
| any | Any value at all | No | No |
| never | The call does not return | No | No |
Two consequences follow. Most of that notation is for reading, not for writing. A field declared record fails with unknown type 'record'. So the result of a builtin that answers a record either stays inside an expression, which is where most of them belong, or you copy it into a type you declared. A callback parameter is looser: it may be typed record, number or &T. That is why the reflection examples below write fn(f: record) -> utf8 { … }.
And [T] says nothing about the element type. sort and sum check theirs at run time and fail on a mixed list, because the signature could not.
The example line ends in a # → … comment carrying the answer wcl repl prints for it. That is the display form: abs(-7.5) is 7.5 and not 7, because every math builtin answers an f64.
§ 6Lists
Twenty-eight builtins over list<T>, the collection everything else turns into. A gather field is one, a table's rows are one, and split, range, keys and chars all answer one. Lists, tensors and records covers the value; this section covers what you can do to it.
They divide by job. Build one with range. Read one with len, at, head, tail, take, drop, slice, index_of and list_contains. Transform one with map, filter, fold, flatten, zip, enumerate, reverse and unique. Search one with any, all and find. Order one with sort, sort_by, sort_connected, min_by, max_by and group_by. And sum reduces a numeric one.
Nine of them also accept a tensor, reading its flat row-major data: at, fold, head, index_of, len, list_contains, map, sum and tail. Only map keeps the shape; the rest answer a plain value or a plain list. The other nineteen refuse a tensor, and filter says why — dropping elements would invalidate the shape.
Two accept a string: len, which counts characters, and slice. Everything else that reads a string lives under Strings.
§ 6.1all
fn(xs: [T], pred: fn (T) -> bool) -> bool
true when the predicate holds for every element (short-circuits; true for an empty list).
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to test. |
| pred | fn (T) -> bool | Predicate applied to each element. |
| Returns | bool | true if every element satisfies the predicate. |
all([2, 4, 6], fn(x: i64) -> bool { x % 2 == 0 }) # → true
§ 6.2any
fn(xs: [T], pred: fn (T) -> bool) -> bool
true when the predicate holds for at least one element (short-circuits).
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to test. |
| pred | fn (T) -> bool | Predicate applied to each element. |
| Returns | bool | true if any element satisfies the predicate. |
any([1, 2, 3], fn(x: i64) -> bool { x > 2 }) # → true
§ 6.3at
fn(xs: [T], i: i64) -> T
The element at a zero-based index; errors if out of bounds or negative.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to index. |
| i | i64 | The zero-based index. |
| Returns | T | The element at i. |
at([10, 20, 30], 1) # → 20
Out of range is a failure, not a none: at([1, 2], 5) reports 'at': at: index 5 out of bounds. Reach for head or a guarded slice when the index may not be there. Also accepts a tensor, indexing its flat data.
§ 6.4drop
fn(xs: [T], n: i64) -> [T]
Every element of a list after the first n.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to drop from. |
| n | i64 | How many leading elements to skip. |
| Returns | [T] | The elements after the first n. |
drop([1, 2, 3, 4], 2) # → [3, 4]
§ 6.5enumerate
fn(xs: [T]) -> [[i64, T]]
Pair every element with its zero-based index, as [index, element] pairs.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to enumerate. |
| Returns | [[i64, T]] | [index, element] pairs. |
enumerate(["a", "b"]) # → [[0, "a"], [1, "b"]]
The pairs are two-element lists, not records: read the index with at(pair, 0) and the element with at(pair, 1).
§ 6.6filter
fn(xs: [T], pred: fn (T) -> bool) -> [T]
Keep only the list elements for which the predicate returns true.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to filter. |
| pred | fn (T) -> bool | Predicate deciding whether to keep an element. |
| Returns | [T] | The elements for which the predicate returned true. |
filter([1, 2, 3, 4], fn(x: i64) -> bool { x % 2 == 0 }) # → [2, 4]
Refuses a tensor outright — dropping elements would invalidate the shape. The message says so: filter: tensors are not supported (would invalidate shape); use map or convert to a list.
§ 6.7find
fn(xs: [T], pred: fn (T) -> bool) -> T
The first element for which the predicate returns true, or none.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to search. |
| pred | fn (T) -> bool | Predicate applied to each element. |
| Returns | T | The first matching element, or none. |
find([1, 2, 3], fn(x: i64) -> bool { x > 1 }) # → 2
§ 6.8flatten
fn(xss: [[T]]) -> [T]
Concatenate a list of lists into a single list, one level deep.
| Parameter | Type | Meaning |
|---|---|---|
| xss | [[T]] | A list whose elements are themselves lists. |
| Returns | [T] | The inner lists concatenated, one level deep. |
flatten([[1, 2], [3]]) # → [1, 2, 3]
§ 6.9fold
fn(xs: [T], init: U, f: fn (U, T) -> U) -> U
Reduce a list or tensor to a single value by repeatedly combining the accumulator with each element.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list or tensor to reduce. |
| init | U | The initial accumulator value. |
| f | fn (U, T) -> U | Combines the accumulator with the next element. |
| Returns | U | The final accumulator value. |
fold([1, 2, 3], 0, fn(acc: i64, x: i64) -> i64 { acc + x }) # → 6
Accepts a tensor, reading its flat row-major data.
§ 6.10group_by
fn(xs: [T], key: fn (T) -> K) -> [record]
Group elements by a key function into { key, items } records, in first-seen key order.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to group. |
| key | fn (T) -> K | Maps each element to its group key. |
| Returns | [record] | One { key, items } record per distinct key. |
group_by([1, 2, 3, 4], fn(x: i64) -> i64 { x % 2 }) # → [ { items: [1, 3], key: 1 }, { items: [2, 4], key: 0 }]
Groups appear in first-seen key order, not sorted key order — grouping [1, 2, 3, 4] by x % 2 puts the odd group first, because 1 came first.
§ 6.11head
fn(xs: [T]) -> T
The first element of a list or tensor (none when empty).
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | A list or tensor. |
| Returns | T | The first element, or none if empty. |
head([1, 2, 3]) # → 1
The one list reader that answers none instead of failing on an empty list. It also accepts a tensor, and reads the first element of its flat data.
§ 6.12index_of
fn(xs: [T], needle: T) -> i64
The index of the first element equal to needle, or -1 if absent.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to search. |
| needle | T | The value to look for. |
| Returns | i64 | The zero-based index, or -1 if not found. |
index_of([10, 20, 30], 20) # → 1
Accepts a list or a tensor, and answers -1 for anything else — including a string, which it never searches.
§ 6.13len
fn(xs: [T]) -> usize
The number of elements in a list or tensor, or characters in a string.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | A list, tensor, or string. |
| Returns | usize | The number of elements (or characters). |
len([10, 20, 30]) # → 3
Three shapes, one question. A list answers with its element count, a tensor with the product of its dimensions, and a string with its character count — len("héllo") is 5, not the byte length.
§ 6.14list_contains
fn(xs: [T], needle: T) -> bool
Whether a list contains a value equal to needle.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to search. |
| needle | T | The value to look for. |
| Returns | bool | true if an equal element is present. |
list_contains([1, 2, 3], 2) # → true
Accepts a list or a tensor. Anything else answers false rather than failing, so a mis-typed argument is silent — see Choosing between the near-twins.
§ 6.15map
fn(xs: [T], f: fn (T) -> U) -> [U]
Apply a function to every element of a list or tensor, returning the transformed collection.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list or tensor to transform. |
| f | fn (T) -> U | Function applied to each element. |
| Returns | [U] | A new collection of the transformed elements. |
map([1, 2, 3], fn(x: i64) -> i64 { x * 2 }) # → [2, 4, 6]
The only iterator that keeps a tensor's shape: map over a tensor[2x2] answers a tensor[2x2]. Every other transformer either refuses a tensor or flattens it.
§ 6.16max_by
fn(xs: [T], key: fn (T) -> K) -> T
The element with the largest key, or none for an empty list.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to search. |
| key | fn (T) -> K | Maps each element to its comparison key. |
| Returns | T | The element with the largest key, or none. |
max_by(["a", "abc", "ab"], fn(s: utf8) -> i64 { len(s) }) # → "abc"
§ 6.17min_by
fn(xs: [T], key: fn (T) -> K) -> T
The element with the smallest key, or none for an empty list.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to search. |
| key | fn (T) -> K | Maps each element to its comparison key. |
| Returns | T | The element with the smallest key, or none. |
min_by(["abc", "a", "ab"], fn(s: utf8) -> i64 { len(s) }) # → "a"
§ 6.18range
fn(start: i64, end: i64) -> [i64]
The half-open integer range [start, end) as a list.
| Parameter | Type | Meaning |
|---|---|---|
| start | i64 | Inclusive lower bound. |
| end | i64 | Exclusive upper bound; must be >= start. |
| Returns | [i64] | The integers from start up to (but excluding) end. |
range(0, 4) # → [0, 1, 2, 3]
§ 6.19reverse
fn(xs: [T]) -> [T]
Reverse the order of a list's elements.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to reverse. |
| Returns | [T] | The list in reverse order. |
reverse([1, 2, 3]) # → [3, 2, 1]
§ 6.20slice
fn(xs: utf8 | [T], start: i64, end: i64) -> utf8 | [T]
The half-open range [start, end) of a string's characters or a list's elements (bounds are clamped).
| Parameter | Type | Meaning |
|---|---|---|
| xs | utf8 | [T] | The string or list to slice. |
| start | i64 | Inclusive start index (clamped to the length). |
| end | i64 | Exclusive end index (clamped to the length). |
| Returns | utf8 | [T] | The sub-string / sub-list. |
slice([10, 20, 30, 40], 1, 3) # → [20, 30]
The one slicer that reads a string as well as a list. Both bounds are clamped, so slice([1, 2, 3], 0, 99) is the whole list rather than an error.
§ 6.21sort
fn(xs: [T]) -> [T]
Sort a list — numerically for all-numeric lists, lexicographically for all-string lists.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | An all-numeric or all-string list. |
| Returns | [T] | The sorted list. |
sort([3, 1, 2]) # → [1, 2, 3]
Mixed lists fail loudly rather than inventing an order: sort([1, "a"]) reports list must be all numeric or all strings, found mixed. Tensors are refused; sort tensor_data(t) instead.
§ 6.22sort_by
fn(xs: [T], key: fn (T) -> K) -> [T]
Sort a list by a key function (stable). Keys must be all numeric or all strings.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to sort. |
| key | fn (T) -> K | Maps each element to its sort key. |
| Returns | [T] | The elements ordered by ascending key. |
sort_by(["abc", "a", "ab"], fn(s: utf8) -> i64 { len(s) }) # → ["a", "ab", "abc"]
§ 6.23sort_connected
fn(items: [T], edges: [{source, destination, ...}]) -> [T]
Reorder a list so that items joined by edges cluster together (recursing into children).
| Parameter | Type | Meaning |
|---|---|---|
| items | [T] | Items identified by an id field (possibly nested via children). |
| edges | [{source, destination, ...}] | Edge records linking item ids. |
| Returns | [T] | The reordered list, connected items adjacent. |
sort_connected([{ id: "a" }, { id: "b" }, { id: "c" }],
[{ source: "a", destination: "c" }]) # → [ { id: "a" }, { id: "c" }, { id: "b" }]
Written for diagram layout: it reads an id field off each item, recurses into a children field, and reorders so that edge-joined items sit together. Items no edge names keep their relative order.
§ 6.24sum
fn(xs: [number]) -> number
Add together every element of a non-empty homogeneous numeric list or tensor.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [number] | A non-empty list or tensor of one numeric type. |
| Returns | number | The total, in the element's numeric type. |
sum([1, 2, 3, 4]) # → 10
An empty list is a failure — sum([]) reports 'sum': sum: empty list — because there is no zero to return without knowing the element type. Also accepts a tensor.
§ 6.25tail
fn(xs: [T]) -> [T]
Every element of a list or tensor except the first.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | A list or tensor. |
| Returns | [T] | The elements after the first. |
tail([1, 2, 3]) # → [2, 3]
Accepts a tensor, and drops the shape: the answer is always a plain list.
§ 6.26take
fn(xs: [T], n: i64) -> [T]
The first n elements of a list (fewer if the list is shorter).
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to take from. |
| n | i64 | How many leading elements to keep. |
| Returns | [T] | The first n elements. |
take([1, 2, 3, 4], 2) # → [1, 2]
§ 6.27unique
fn(xs: [T]) -> [T]
Remove duplicate elements from a list, keeping first-seen order.
| Parameter | Type | Meaning |
|---|---|---|
| xs | [T] | The list to deduplicate. |
| Returns | [T] | The list with duplicates removed. |
unique([1, 2, 2, 3, 1]) # → [1, 2, 3]
§ 6.28zip
fn(a: [A], b: [B]) -> [(A, B)]
Pair up elements of two lists by index, stopping at the shorter length.
| Parameter | Type | Meaning |
|---|---|---|
| a | [A] | The first list. |
| b | [B] | The second list. |
| Returns | [(A, B)] | Index-paired [a, b] lists, up to the shorter length. |
zip([1, 2, 3], ["a", "b"]) # → [[1, "a"], [2, "b"]]
The pairs are two-element lists, exactly as enumerate produces.
§ 7Records
Four builtins over the record — the { name: value, … } literal covered in Lists, tensors and records. A record has no declared type and no field order, which is why keys sorts.
§ 7.1keys
fn(r: record) -> [utf8]
The field names of a record, in deterministic (sorted) order.
| Parameter | Type | Meaning |
|---|---|---|
| r | record | A record value (or a union variant with a record body). |
| Returns | [utf8] | The field names. |
keys({ name: "Rex", age: 4 }) # → ["age", "name"]
Sorted, always — a record's field order is not preserved, so keys gives you a deterministic one instead.
§ 7.2map_values
fn(r: record, f: fn (T) -> U) -> record
Apply a function to every field value of a record, keeping the keys.
| Parameter | Type | Meaning |
|---|---|---|
| r | record | The record to transform. |
| f | fn (T) -> U | Function applied to each field value. |
| Returns | record | A record with the same keys and transformed values. |
map_values({ low: 1, high: 9 }, fn(x: i64) -> i64 { x * 2 }) # → { high: 18, low: 2 }
§ 7.3merge
fn(a: record, b: record) -> record
Combine two records into one; fields of b win on a name clash.
| Parameter | Type | Meaning |
|---|---|---|
| a | record | The base record. |
| b | record | The overriding record. |
| Returns | record | A record with the union of both field sets. |
merge({ host: "localhost", port: 80 }, { port: 8080 }) # → { host: "localhost", port: 8080 }
§ 7.4values
fn(r: record) -> [T]
The field values of a record, in the same order as keys.
| Parameter | Type | Meaning |
|---|---|---|
| r | record | A record value (or a union variant with a record body). |
| Returns | [T] | The field values. |
values({ name: "Rex", age: 4 }) # → [4, "Rex"]
In the same order as keys, which is sorted by field name. values({ name: "Rex", age: 4 }) is [4, "Rex"], not ["Rex", 4].
§ 8Strings
Fifteen builtins over utf8. Every one of them counts in characters, not bytes: chars, pad_start and pad_end all treat a multi-byte character as one unit.
len and slice also read strings, but they are list builtins that happen to accept one, so they are catalogued under Lists. format_unit and format_unit_value build strings too, and are under Units because that is what they are about.
§ 8.1chars
fn(s: utf8) -> [utf8]
The characters of a string as a list of one-character strings.
| Parameter | Type | Meaning |
|---|---|---|
| s | utf8 | The string to split into characters. |
| Returns | [utf8] | One string per character. |
chars("abc") # → ["a", "b", "c"]
Characters, not bytes: chars("héllo") is five one-character strings.
§ 8.2concat
fn(a: utf8, b: utf8) -> utf8
Concatenate two strings into one.
| Parameter | Type | Meaning |
|---|---|---|
| a | utf8 | The left-hand string. |
| b | utf8 | The string appended after a. |
| Returns | utf8 | The two strings joined together. |
concat("foo", "bar") # → "foobar"
§ 8.3contains
fn(s: utf8, needle: utf8) -> bool
Whether a string contains a substring.
| Parameter | Type | Meaning |
|---|---|---|
| s | utf8 | The string to search. |
| needle | utf8 | The substring to look for. |
| Returns | bool | true if the substring is present. |
contains("hello", "ell") # → true
Strings only. Its list-shaped twin is list_contains, and passing a list here fails with 'contains': expected utf8 string, found list.
§ 8.4ends_with
fn(s: utf8, suffix: utf8) -> bool
Whether a string ends with a suffix.
| Parameter | Type | Meaning |
|---|---|---|
| s | utf8 | The string to test. |
| suffix | utf8 | The suffix to look for. |
| Returns | bool | true if the string ends with the suffix. |
ends_with("hello", "lo") # → true
§ 8.5format
fn (utf8, ...args) -> utf8
Substitute trailing arguments into a template's {} placeholders ({{/}} are literal braces).
| Parameter | Type | Meaning |
|---|---|---|
| template | utf8 | Template string with {} placeholders. |
| Returns | utf8 | The template with placeholders substituted. |
format("{} = {}", "x", 42) # → "x = 42"
The one variadic builtin. {} takes the next argument in order; {{ and }} are literal braces. A slot renders the value the way a person reads it — a u32 loses its suffix — which is the same rendering an interpolated $"…${x}…" string performs. See Values and primitives.
§ 8.6join
fn(parts: [utf8], sep: utf8) -> utf8
Join a list of strings into one, inserting a separator between each.
| Parameter | Type | Meaning |
|---|---|---|
| parts | [utf8] | The strings to join. |
| sep | utf8 | The separator inserted between parts. |
| Returns | utf8 | The joined string. |
join(["a", "b", "c"], "-") # → "a-b-c"
§ 8.7pad_end
fn(s: utf8, width: i64, pad: utf8) -> utf8
Right-pad a string with a fill pattern until it is width characters long.
| Parameter | Type | Meaning |
|---|---|---|
| s | utf8 | The string to pad. |
| width | i64 | The target character count. |
| pad | utf8 | The fill pattern (repeated / truncated as needed). |
| Returns | utf8 | The padded string (unchanged if already wide enough). |
pad_end("42", 5, "0") # → "42000"
Same fill rule as pad_start, applied on the right.
§ 8.8pad_start
fn(s: utf8, width: i64, pad: utf8) -> utf8
Left-pad a string with a fill pattern until it is width characters long.
| Parameter | Type | Meaning |
|---|---|---|
| s | utf8 | The string to pad. |
| width | i64 | The target character count. |
| pad | utf8 | The fill pattern (repeated / truncated as needed). |
| Returns | utf8 | The padded string (unchanged if already wide enough). |
pad_start("42", 5, "0") # → "00042"
The pad pattern is repeated and truncated to fill the gap, so a multi-character pattern may end mid-pattern. A string already width wide comes back unchanged; nothing is ever cut.
§ 8.9repeat
fn(s: utf8, n: i64) -> utf8
A string repeated n times (empty for n <= 0).
| Parameter | Type | Meaning |
|---|---|---|
| s | utf8 | The string to repeat. |
| n | i64 | How many copies to concatenate. |
| Returns | utf8 | n copies of s. |
repeat("ab", 3) # → "ababab"
n <= 0 answers the empty string rather than failing.
§ 8.10replace
fn(s: utf8, old: utf8, new: utf8) -> utf8
Replace every occurrence of a substring with another.
| Parameter | Type | Meaning |
|---|---|---|
| s | utf8 | The string to search. |
| old | utf8 | The substring to find. |
| new | utf8 | The replacement substring. |
| Returns | utf8 | The string with every match replaced. |
replace("hello world", "world", "there") # → "hello there"
§ 8.11split
fn(s: utf8, sep: utf8) -> [utf8]
Split a string on every occurrence of a separator into a list of pieces.
| Parameter | Type | Meaning |
|---|---|---|
| s | utf8 | The string to split. |
| sep | utf8 | The separator to split on. |
| Returns | [utf8] | The pieces between separators. |
split("a,b,c", ",") # → ["a", "b", "c"]
§ 8.12starts_with
fn(s: utf8, prefix: utf8) -> bool
Whether a string begins with a prefix.
| Parameter | Type | Meaning |
|---|---|---|
| s | utf8 | The string to test. |
| prefix | utf8 | The prefix to look for. |
| Returns | bool | true if the string starts with the prefix. |
starts_with("hello", "he") # → true
§ 8.13to_lower
fn(s: utf8) -> utf8
Lowercase every character of a string.
| Parameter | Type | Meaning |
|---|---|---|
| s | utf8 | The string to lowercase. |
| Returns | utf8 | The lowercased string. |
to_lower("AbC") # → "abc"
§ 8.14to_upper
fn(s: utf8) -> utf8
Uppercase every character of a string.
| Parameter | Type | Meaning |
|---|---|---|
| s | utf8 | The string to uppercase. |
| Returns | utf8 | The uppercased string. |
to_upper("abc") # → "ABC"
§ 8.15trim
fn(s: utf8) -> utf8
Remove leading and trailing whitespace from a string.
| Parameter | Type | Meaning |
|---|---|---|
| s | utf8 | The string to trim. |
| Returns | utf8 | The string without leading/trailing whitespace. |
trim(" hi ") # → "hi"
§ 9Tensors
One constructor and three accessors. A tensor is a flat, row-major buffer plus a shape — see Lists, tensors and records. These four are the only builtins that know about the shape. The nine list builtins that accept a tensor all read its flat data, and only map gives a tensor back.
§ 9.1tensor
fn(data: [number], shape: [usize]) -> tensor<T>
Build a tensor from flat row-major data and a shape; the data length must equal the product of the dimensions.
| Parameter | Type | Meaning |
|---|---|---|
| data | [number] | Flat, row-major element data. |
| shape | [usize] | The dimension sizes. |
| Returns | tensor<T> | The constructed tensor. |
tensor([1, 2, 3, 4], [2, 2]) # → tensor[2x2](1, 2, 3, 4)
The data length must equal the product of the dimensions. tensor([1, 2, 3], [2, 2]) reports tensor: data length 3 does not match shape product 4.
§ 9.2tensor_data
fn(t: tensor<T>) -> [T]
The flat row-major element data of a tensor as a list.
| Parameter | Type | Meaning |
|---|---|---|
| t | tensor<T> | The tensor to read. |
| Returns | [T] | The tensor's flat, row-major element data. |
tensor_data(tensor([1, 2, 3, 4], [2, 2])) # → [1, 2, 3, 4]
§ 9.3tensor_reshape
fn(t: tensor<T>, shape: [usize]) -> tensor<T>
Reinterpret a tensor's data under a new shape; the element count must be unchanged.
| Parameter | Type | Meaning |
|---|---|---|
| t | tensor<T> | The tensor to reshape. |
| shape | [usize] | The new dimension sizes. |
| Returns | tensor<T> | The same data under the new shape. |
tensor_reshape(tensor([1, 2, 3, 4], [2, 2]), [4]) # → tensor[4](1, 2, 3, 4)
The element count must be unchanged; the data is not copied or reordered, only re-read.
§ 9.4tensor_shape
fn(t: tensor<T>) -> [usize]
The dimension sizes of a tensor as a list.
| Parameter | Type | Meaning |
|---|---|---|
| t | tensor<T> | The tensor to read. |
| Returns | [usize] | The tensor's dimension sizes. |
tensor_shape(tensor([1, 2, 3, 4], [2, 2])) # → [2, 2]
§ 10Failing on purpose
Three ways to stop. All three are implemented as the same abort: evaluation of the field that forced them ends, and the message is printed after error:. All three are recoverable by try body catch e => fallback — see Control flow.
The difference is the shape of the call, not the severity. assert takes a condition and does nothing when it holds. error and panic take no condition and always abort. They are literally the same function registered twice.
§ 10.1assert
fn(cond: bool, msg: utf8) -> none
Return none when cond is true, otherwise abort with msg.
| Parameter | Type | Meaning |
|---|---|---|
| cond | bool | The condition that must hold. |
| msg | utf8 | The error message reported when cond is false. |
| Returns | none | none when the assertion holds (otherwise aborts). |
assert(1 + 1 == 2, "math is broken") # → none
The return value is none, which is rarely what you want on the right of an =. Use it as a guard inside a block expression: { let _ = assert(self.port > 1024u32, "port must be above 1024"); 3u32 }.
§ 10.2error
fn(msg: utf8) -> never
Abort evaluation with an error message.
| Parameter | Type | Meaning |
|---|---|---|
| msg | utf8 | The error message to report. |
| Returns | never | Never returns — aborts evaluation. |
@block("service")
type Service {
@inline(0) name: utf8
port: u32
checked: u32
}
@document
type Cfg {
@children("service") services: list<Service>
}
service "web" {
port = 80u32
checked = if self.port > 1024u32 { self.port } else { error("port must be above 1024") }
}
$ wcl get guard.wcl services.web.checked
wcl::eval::user_error
× error: port must be above 1024
The failure aborts the field that called it, and the message is printed verbatim after error:. try … catch e => … recovers it — see Control flow.
§ 10.3panic
fn(msg: utf8) -> never
Abort evaluation with an unrecoverable failure message.
| Parameter | Type | Meaning |
|---|---|---|
| msg | utf8 | The failure message to report. |
| Returns | never | Never returns — aborts evaluation. |
panic("unreachable: two roots") # → aborts: error: unreachable: two roots
Identical to error in every observable way: same abort, same error: <msg> output, and equally catchable by try/catch. The two names differ in intent only.
§ 11Math
Twenty-nine builtins, and one rule that covers all of them: arguments widen to f64 and results are f64. sqrt(144) answers 12.0, not 12. That is why the signatures say number going in and f64 coming out — an i64, a u32 and an f64 are all accepted, and none of them survives the call as itself.
Nothing here is a constraint check. sqrt(-1), ln(0) and 0.0 / 0.0 produce the IEEE answers (NaN, -inf) rather than errors, exactly as the floating-point arithmetic in Expressions and operators does.
The trigonometric functions work in radians. radians and degrees convert; pi, tau and e are the three nullary constants.
§ 11.1abs
fn(x: number) -> f64
Absolute value.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
abs(-7.5) # → 7.5
§ 11.2acos
fn(x: number) -> f64
Arccosine, in radians, of a value in [-1, 1].
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
acos(1.0) # → 0.0
§ 11.3asin
fn(x: number) -> f64
Arcsine, in radians, of a value in [-1, 1].
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
asin(1.0) # → 1.5707963267948966
§ 11.4atan
fn(x: number) -> f64
Arctangent, in radians.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
atan(1.0) # → 0.7853981633974483
§ 11.5atan2
fn(a: number, b: number) -> f64
Arctangent of a/b in radians, using the signs of both to pick the quadrant.
| Parameter | Type | Meaning |
|---|---|---|
| a | number | The first operand. |
| b | number | The second operand. |
| Returns | f64 | The result, as an f64. |
atan2(1.0, 1.0) # → 0.7853981633974483
§ 11.6cbrt
fn(x: number) -> f64
Cube root.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
cbrt(27) # → 3.0
§ 11.7ceil
fn(x: number) -> f64
Round up to the nearest integer.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
ceil(3.1) # → 4.0
§ 11.8clamp
fn(x: number, lo: number, hi: number) -> f64
Constrain x to the range [lo, hi].
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The value to clamp. |
| lo | number | The lower bound. |
| hi | number | The upper bound. |
| Returns | f64 | x limited to [lo, hi], as an f64. |
clamp(12.0, 0.0, 10.0) # → 10.0
No check that lo <= hi. When they are the wrong way round the answer is lo.
§ 11.9cos
fn(x: number) -> f64
Cosine of an angle in radians.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
cos(0.0) # → 1.0
§ 11.10degrees
fn(x: number) -> f64
Convert an angle from radians to degrees.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
degrees(pi()) # → 180.0
§ 11.11e
fn() -> f64
Euler's number e (≈ 2.71828).
| Parameter | Type | Meaning |
|---|---|---|
| Returns | f64 | The value of e. |
e() # → 2.718281828459045
§ 11.12exp
fn(x: number) -> f64
e raised to the power x.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
exp(0.0) # → 1.0
§ 11.13floor
fn(x: number) -> f64
Round down to the nearest integer.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
floor(3.9) # → 3.0
§ 11.14hypot
fn(a: number, b: number) -> f64
Length of the hypotenuse sqrt(a² + b²).
| Parameter | Type | Meaning |
|---|---|---|
| a | number | The first operand. |
| b | number | The second operand. |
| Returns | f64 | The result, as an f64. |
hypot(3, 4) # → 5.0
§ 11.15ln
fn(x: number) -> f64
Natural (base-e) logarithm.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
ln(1.0) # → 0.0
§ 11.16log10
fn(x: number) -> f64
Base-10 logarithm.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
log10(1000.0) # → 3.0
§ 11.17log2
fn(x: number) -> f64
Base-2 logarithm.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
log2(8.0) # → 3.0
§ 11.18max
fn(a: number, b: number) -> f64
The larger of two numbers.
| Parameter | Type | Meaning |
|---|---|---|
| a | number | The first operand. |
| b | number | The second operand. |
| Returns | f64 | The result, as an f64. |
max(3, 7.5) # → 7.5
Two numbers, not a list. The list form is max_by.
§ 11.19min
fn(a: number, b: number) -> f64
The smaller of two numbers.
| Parameter | Type | Meaning |
|---|---|---|
| a | number | The first operand. |
| b | number | The second operand. |
| Returns | f64 | The result, as an f64. |
min(3, 7.5) # → 3.0
Two numbers, not a list. The list form is min_by.
§ 11.20pi
fn() -> f64
The constant π (≈ 3.14159).
| Parameter | Type | Meaning |
|---|---|---|
| Returns | f64 | The value of π. |
pi() # → 3.141592653589793
§ 11.21pow
fn(a: number, b: number) -> f64
Raise a to the power b.
| Parameter | Type | Meaning |
|---|---|---|
| a | number | The first operand. |
| b | number | The second operand. |
| Returns | f64 | The result, as an f64. |
pow(2, 10) # → 1024.0
§ 11.22radians
fn(x: number) -> f64
Convert an angle from degrees to radians.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
radians(180.0) # → 3.141592653589793
§ 11.23round
fn(x: number) -> f64
Round to the nearest integer (ties away from zero).
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
round(2.5) # → 3.0
Ties go away from zero: round(2.5) is 3.0 and round(-2.5) is -3.0.
§ 11.24sign
fn(x: number) -> f64
The sign of x: 1, -1, or 0.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
sign(-3.0) # → -1.0
Zero answers 0.0, so the result is one of exactly three values.
§ 11.25sin
fn(x: number) -> f64
Sine of an angle in radians.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
sin(0.0) # → 0.0
§ 11.26sqrt
fn(x: number) -> f64
Square root.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
sqrt(144) # → 12.0
§ 11.27tan
fn(x: number) -> f64
Tangent of an angle in radians.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
tan(0.0) # → 0.0
§ 11.28tau
fn() -> f64
The constant τ = 2π (≈ 6.28319).
| Parameter | Type | Meaning |
|---|---|---|
| Returns | f64 | The value of τ (2π). |
tau() # → 6.283185307179586
§ 11.29trunc
fn(x: number) -> f64
Discard the fractional part, rounding toward zero.
| Parameter | Type | Meaning |
|---|---|---|
| x | number | The input value (any number, widened to f64). |
| Returns | f64 | The result, as an f64. |
trunc(3.9) # → 3.0
§ 12Reflection
Twelve builtins that read the document rather than its data: what a type declares, what decorates it, what a namespace holds, what the source said. They exist so a document can generate its own reference — which is how the catalogue in this chapter was produced.
Most of them take a reference — the declaration's bare name, written in expression position; the rest take a name as a string. Every example below is a field of one small file, so you can see what each answers against the same schema:
# schema.wcl — a tiny schema that documents itself.
namespace app
# The edge service: terminates TLS and forwards to the app tier.
# Owned by the platform team.
@block("service")
type Service {
@inline(0) name: identifier
port: u32
@child("limits") limits: Limits?
}
@block("limits")
type Limits {
cpu: f64
}
@document
type Inventory {
@children("service") services: list<Service>
decorators: list<utf8>
block_kind: utf8
fields: list<utf8>
children: list<utf8>
declarations: list<utf8>
legal_on: usize
kind: utf8
doc: utf8
source: utf8
map_sig: utf8
builtins: usize
doubled: i64
}
decorators = decorator_names(Service)
block_kind = decorator_arg(Service, "block", "name")
fields = map(type_fields(Service), fn(f: record) -> utf8 { f.name })
children = map(child_types(Inventory), fn(t: &Service) -> utf8 { { let d = decl_info(t); d.name } })
declarations = map(namespace_decls("app"), fn(t: &Service) -> utf8 { { let d = decl_info(t); d.name } })
legal_on = len(decorators_for_kind("service"))
kind = { let d = decl_info(Service); d.kind }
doc = doc_comment(Service)
source = ast_string(Limits)
map_sig = { let s = fn_signature("map"); s.signature }
builtins = len(builtin_names())
doubled = eval("2 * 21")
service web { port = 8080u32 }
Two shapes recur. A builtin returning [&T] answers a list of further references, so the usual move is to map over it and reflect each one — that is what children and declarations do above. And a builtin returning a record must be bound before a field is read off it: decl_info(Service).kind fails with expected a reference, got call, while { let d = decl_info(Service); d.kind } answers "type". Note that only wcl get and a build catch it — the field is lazy, so wcl check alone reports OK.
§ 12.1ast_string
fn(target: &T) -> utf8
Pretty-print the canonical source behind a reference (type/interface/union/symbol_set/block/field) or a function value.
| Parameter | Type | Meaning |
|---|---|---|
| target | &T | A dataref to a declaration, or a function value. |
| Returns | utf8 | The canonical (pretty-printed) source text. |
source = ast_string(Limits)
$ wcl get schema.wcl source
"@block(\"limits\")\ntype Limits {\n cpu: f64\n}"
§ 12.2builtin_names
fn() -> [utf8]
The names of every registered built-in function, sorted. Pair with fn_signature to introspect each one.
| Parameter | Type | Meaning |
|---|---|---|
| Returns | [utf8] | Every built-in's name, sorted alphabetically. |
builtins = len(builtin_names())
$ wcl get schema.wcl builtins
103
§ 12.3child_types
fn(target: &T) -> [&T]
Reflect a type into references to the element types of its @child / @children block slots (own slots first, then inherited via extends). Pair with type_table / type_fields to auto-document the blocks a @document declares.
| Parameter | Type | Meaning |
|---|---|---|
| target | &T | A reference to a type or interface declaration. |
| Returns | [&T] | One type reference per block slot. Slots that accept a union or interface resolve to that type's name; scalar (non-block) fields are skipped. |
children = map(child_types(Inventory), fn(t: &Service) -> utf8 { { let d = decl_info(t); d.name } })
$ wcl get schema.wcl children
["Service"]
§ 12.4decl_info
fn(target: &T) -> record
Describe a top-level declaration: its name, kind, doc comment, and schema classification (block / table / decorator / document).
| Parameter | Type | Meaning |
|---|---|---|
| target | &T | A reference to a type, interface, union, or symbol_set declaration. |
| Returns | record | { name, full_name, kind, doc, is_imported, is_document, block_kind, table_kind, decorator_name, extends }. The classification fields are none when the decorator is absent. |
kind = { let d = decl_info(Service); d.kind }
$ wcl get schema.wcl kind
"type"
The kind of a @document type is still "type"; the is_document flag is what tells you it is the document schema.
§ 12.5decorator_arg
fn(target: &T, decorator: utf8, slot: utf8) -> any
Read one named argument of a decorator on a referenced declaration (none if absent).
| Parameter | Type | Meaning |
|---|---|---|
| target | &T | A reference to a type, field, block, or variant. |
| decorator | utf8 | The decorator name, e.g. "doc". |
| slot | utf8 | The argument (slot) name to read. |
| Returns | any | The argument's value, or none if absent. |
block_kind = decorator_arg(Service, "block", "name")
$ wcl get schema.wcl block_kind
"service"
§ 12.6decorator_names
fn(target: &T) -> [utf8]
List the names of the decorators attached to a referenced declaration.
| Parameter | Type | Meaning |
|---|---|---|
| target | &T | A reference to a type, field, block, or variant. |
| Returns | [utf8] | The decorator names, in source order. |
decorators = decorator_names(Service)
$ wcl get schema.wcl decorators
["block"]
§ 12.7decorators_for_kind
fn(kind: utf8) -> [&T]
List references to decorator schemas applicable to a block kind. Pair with decl_info, doc_comment, and type_fields to render them.
| Parameter | Type | Meaning |
|---|---|---|
| kind | utf8 | The block kind as a string; instance-derived kinds are supported. |
| Returns | [&T] | One reference per applicable decorator-schema declaration. |
legal_on = len(decorators_for_kind("service"))
$ wcl get schema.wcl legal_on
22
Answers the decorator schemas that are legal on the kind, which includes every built-in one — 22 of them for an ordinary block, before the document declares any of its own.
§ 12.8doc_comment
fn(target: &T) -> utf8
The doc comment — the contiguous run of # / // lines immediately above a declaration — attached to a reference, or "" when there is none. Complements decorator_arg(x, "doc", …) for @doc("…") metadata.
| Parameter | Type | Meaning |
|---|---|---|
| target | &T | A reference to a type, interface, union, variant, symbol_set, or field. |
| Returns | utf8 | The joined comment text, or "" when absent. |
doc = doc_comment(Service)
$ wcl get schema.wcl doc
"The edge service: terminates TLS and forwards to the app tier.\nOwned by the platform team."
The run must be unbroken: a blank line ends it, and anything above the blank line belongs to nothing. See Documents, fields and blocks.
§ 12.9eval
fn(src: utf8) -> any
Parse a string as a WCL expression and evaluate it in the current scope.
| Parameter | Type | Meaning |
|---|---|---|
| src | utf8 | WCL expression source to parse and evaluate. |
| Returns | any | The value the expression evaluates to. |
doubled = eval("2 * 21")
$ wcl get schema.wcl doubled
42
The expression is parsed and evaluated in the calling scope, so it can see self, sibling fields and let bindings: a field written doubled = eval("self.port * 2u32") inside a service whose port is 80u32 answers 160u32.
§ 12.10fn_signature
fn(f: any) -> record
Describe a function's parameters and return type. Pass a function value, or a built-in's name as a string.
| Parameter | Type | Meaning |
|---|---|---|
| f | any | A function value, or the name of a built-in as a utf8 string. |
| Returns | record | A record { doc, params: [{name, type, doc}], return_type, return_doc, signature, is_builtin }. |
map_sig = { let s = fn_signature("map"); s.signature }
$ wcl get schema.wcl map_sig
"fn(xs: [T], f: fn (T) -> U) -> [U]"
Takes a function value, or a builtin's name as a string. The record it returns has to be bound before you can read a field off it — fn_signature("map").signature fails with expected a reference, got call, while { let s = fn_signature("map"); s.signature } does not.
§ 12.11namespace_decls
fn(ns: utf8) -> [&T]
List references to every top-level declaration (type / interface / union / symbol_set) in a namespace, for schema-documentation generators. Pair with decl_info, doc_comment, type_fields, and ast_string to render each. Imported (library) declarations are included — filter on decl_info(d).is_imported to drop them.
| Parameter | Type | Meaning |
|---|---|---|
| ns | utf8 | The namespace, dotted (e.g. "wdoc"); "" for the root namespace. |
| Returns | [&T] | One reference per declaration: types first, then interfaces, unions, symbol sets, in source order. |
declarations = map(namespace_decls("app"), fn(t: &Service) -> utf8 { { let d = decl_info(t); d.name } })
$ wcl get schema.wcl declarations
["Service", "Limits", "Inventory"]
Imported declarations are included. Filter on decl_info(d).is_imported to keep only the ones the document declares itself.
§ 12.12type_fields
fn(target: &T) -> [record]
Reflect a type or interface into a list of field-description records (own fields first, then inherited via extends).
| Parameter | Type | Meaning |
|---|---|---|
| target | &T | A reference to a type or interface declaration. |
| Returns | [record] | One record per field: { name, type, is_function, optional, has_default, default, is_block, repeated, accepts, decorators }. |
fields = map(type_fields(Service), fn(f: record) -> utf8 { f.name })
$ wcl get schema.wcl fields
["name", "port", "limits"]
§ 13Paths and globs
Three pure string functions. Nothing here touches the disk: a path is a /-separated sequence of segments, empty and . segments are dropped, and a pattern with a trailing / owns its whole subtree. src, src/ and ./src are the same path to all three.
Glob syntax is shared by glob_match and glob_overlaps: * matches within one segment, ** spans whole segments, ? matches one character, and [abc] / [a-z] / [!x] are character classes.
§ 13.1glob_match
fn(pattern: utf8, path: utf8) -> bool
Match one concrete path against a glob. * stays within a segment, ** spans segments, ? matches one character, [a-z] / [!x] are character classes. A trailing / on the pattern matches the whole subtree.
| Parameter | Type | Meaning |
|---|---|---|
| pattern | utf8 | The glob pattern. |
| path | utf8 | The concrete path to test. |
| Returns | bool | true if the path matches the pattern. |
glob_match("src/*.rs", "src/main.rs") # → true
§ 13.2glob_overlaps
fn(a: utf8, b: utf8) -> bool
Whether two glob patterns can match a common path. Concrete paths are patterns too, so this subsumes glob_match for overlap gates. Trailing / means the whole subtree. Conservative: exotic negated-class pairings may report true when no shared path exists, never false when one does.
| Parameter | Type | Meaning |
|---|---|---|
| a | utf8 | The first glob pattern (or concrete path). |
| b | utf8 | The second glob pattern (or concrete path). |
| Returns | bool | true if some path is matched by both patterns. |
glob_overlaps("src/", "src/*.rs") # → true
Conservative in one direction only: an exotic pairing of negated classes may answer true when no shared path exists, but never false when one does.
§ 13.3path_contains
fn(parent: utf8, child: utf8) -> bool
Segment-aware path prefix test: whether child is parent itself or lives under it. Splits on /, so src/ does not contain src2/x. A path contains itself.
| Parameter | Type | Meaning |
|---|---|---|
| parent | utf8 | The containing path (trailing slash optional). |
| child | utf8 | The path to test. |
| Returns | bool | true if child equals parent or is nested beneath it. |
path_contains("src/", "src/core/mod.rs") # → true
Segment-aware, so src does not contain src2/x. A path contains itself.
§ 14Units
Two builtins, both going the same direction: from a stored base-unit number back to a labelled string. Writing 512MiB in a document is the other direction, and the evaluator does that one — see Values and primitives.
§ 14.1format_unit
fn(value: i64, type: utf8, unit: utf8) -> utf8
Render a base-unit value in a chosen unit, looking the factor up from a unit type by name: format_unit(size, "std.ByteSize", "MiB") → "5 MiB". The inverse of literal-unit resolution, so it stays correct if the type's @unit factor changes.
| Parameter | Type | Meaning |
|---|---|---|
| value | i64 | The stored value, in the type's base unit. |
| type | utf8 | The unit type's dotted name, e.g. "std.ByteSize". |
| unit | utf8 | The unit to render in, e.g. "MiB". |
| Returns | utf8 | The value divided by the unit's factor, suffixed with the unit (e.g. "5 MiB"). |
format_unit(5242880, "std.ByteSize", "MiB") # → "5 MiB"
Reads the factor off the named type's @unit decorator rather than hard-coding one, so it stays correct if the factor changes. The exact inverse of writing 5MiB — see Values and primitives.
§ 14.2format_unit_value
fn(value: i64, factor: i64, unit: utf8) -> utf8
Render a number in a unit given its factor explicitly: format_unit_value(5242880, 1048576, "MiB") → "5 MiB". The primitive behind format_unit for callers that already hold the factor.
| Parameter | Type | Meaning |
|---|---|---|
| value | i64 | The stored value, in the type's base unit. |
| factor | i64 | The unit's multiplier (base units per one unit). |
| unit | utf8 | The unit label to append. |
| Returns | utf8 | value / factor followed by the unit label. |
format_unit_value(5242880, 1048576, "MiB") # → "5 MiB"
The primitive behind format_unit, for a caller that already holds the factor. Nothing checks that the factor matches any declared unit.
§ 15Builtins the host adds
The two the wcl CLI registers on wdoc's behalf. They are not part of the language, they are not in wcl_lang, and a host embedding WCL without wdoc will not have them — but builtin_names() reports them under the CLI, so they are catalogued here rather than left unexplained.
Nothing stops you adding your own. A host registers a Rust closure against a name and it becomes callable from every document that host opens. See the wcl_lang crate docs for Environment::add_builtin and from_fn.
§ 15.1page_metadata
fn(ctx: TemplateCtx) -> PageMetadata
Return memoised reading order, neighbours and active TOC path plus authored heading metadata for the current template page. Site metadata is indexed once and no other page body is evaluated.
| Parameter | Type | Meaning |
|---|---|---|
| ctx | TemplateCtx | The current template context. |
| Returns | PageMetadata | Metadata for the current page. |
{ let m = page_metadata(c); m.next } # → the next TOC entry, or none
wdoc's, not the language's. It reads the current template context and answers the page's reading order, neighbours, active TOC path and heading outline. The site index is memoised and no page body is lowered. See Templates and layouts.
§ 15.2__wdoc_slot
(no signature registered)
Internal. No documentation is registered for it.
| Parameter | Type | Meaning |
|---|---|---|
| Returns | — | Nothing documented. |
__wdoc_slot(c.slots, name, :blocks) # → the blocks filling the named slot
wdoc's, and internal. It backs the template slot machinery and is the one builtin the CLI registers with no documented signature, which is why fn_signature("__wdoc_slot") answers a record whose every field is empty. Treat it as private to the standard library.
§ 16Where to go next
- Lists, tensors and records — the three collection values every builtin in the first four sections operates on.
- Functions — the fn(…) -> T { … } literal you hand to map, filter, fold and the _by family.
- Control flow — try/catch, which recovers the failure a builtin raises.
- Expressions and operators — call syntax, precedence, and the operators that do what a builtin would otherwise have to.
- Decorators — what the reflection builtins are reading when they answer a decorator name or argument.
- The CLI — wcl repl, which is the fastest way to check what a builtin does.