An anonymous record is a product type identified by its fields rather than by a
declaration name. Write the type as { field: Type, ... } and the value as
{ field: value, ... }.
fn position(): { x: int, y: int } {
return { x: 10, y: 20 }
}
fn distance_from_origin(): int {
p := position()
return p.x + p.y
}No struct declaration is involved. The return type is the field list, so
the helper and its caller agree without a shared name.
Structural identity
Two record types with the same field names and field types are the same type, whatever order the fields appear in. Field order is a spelling choice, not part of the identity.
fn f(): int {
first: { x: int, y: int } = { x: 1, y: 2 }
second: { y: int, x: int } = first
return second.x
}Change a field’s type and you have a different record. The checker reports the
mismatch with the canonical $Rec<...> spelling:
fn f(): int {
r: { x: int } = { x: "hi" }
return 0
}Change the field set and you also have a different record. A literal with fewer fields does not satisfy an annotation with more — records do not have width subtyping.
fn f(): int {
r: { x: int, y: int } = { x: 1 }
return r.x
}A record is never interchangeable with a nominal struct that happens to have the same fields. Structs are nominal; records are structural, and the two families do not meet.
struct Point { x: int, y: int }
fn f(): int {
r: { x: int, y: int } = { x: 1, y: 2 }
p: Point = r
return p.x
}Literals
Each field is name: value, and fields must be separated by commas. A
trailing comma is fine. Newline-only separation does not parse, because a brace
block is also an expression and the commas are what disambiguate it:
fn f(): int {
r := {
status: 200
body: "ok"
}
return r.status
}Written correctly, the same literal can still span lines:
fn f(): int {
response := {
status: 200,
body: "ok",
cached: false,
}
if response.cached { return 0 }
return response.status
}Field shorthand picks up a visible binding of the same name:
fn f(): int {
status := 200
body := "ok"
response := { status, body }
return response.status + response.body.len()
}Records nest, both as values and in a struct field:
struct Report {
window: { start: int, end: int }
label: string
}
fn span(): int {
r := Report { window: { start: 1, end: 9 }, label: "day" }
return r.window.end - r.window.start
}Access and mutation
Fields use ordinary member syntax, and a mut binding makes them assignable.
fn tally(values: []int): int {
mut totals := { hits: 0, misses: 0 }
for v in values {
if v % 2 == 0 { totals.hits += 1 } else { totals.misses += 1 }
}
return totals.hits - totals.misses
}Records compare with == when their fields do:
fn f(): bool {
a := { x: 1, y: 2 }
b := { x: 1, y: 2 }
return a == b
}Patterns
Record patterns destructure by field name in match arms. Shorthand binds the
field to its own name; field: name renames it; .. accepts fields the pattern
does not mention.
fn f(): int {
response := { status: 200, body: "ok" }
return match response {
{ status, body } => status + body.len()
}
}fn f(): int {
response := { status: 200, body: "ok", cached: true }
return match response {
{ status: code, .. } => code
}
}Note the asymmetry with construction: a literal must supply every field, while
a pattern may ignore the rest with ...
Record patterns nest inside other patterns, which is the usual way to open a
record returned as an Option:
fn parse_pair(text: string): { key: string, value: string }? {
parts := text.split("=")
if parts.len() != 2 { return None }
return { key: parts.get(0) ?? "", value: parts.get(1) ?? "" }
}
fn render(text: string): string {
return match parse_pair(text) {
Some({ key, value }) => "${key}=${value}"
None => "(malformed)"
}
}Multi-value returns
Records are the readable alternative to a tuple when a helper produces two or three related results. The names travel with the value, so the call site does not have to remember positions.
fn bounds(values: []int): { minimum: int, maximum: int, count: int } {
mut lo := values.get(0) ?? 0
mut hi := lo
for v in values {
if v < lo { lo = v }
if v > hi { hi = v }
}
return { minimum: lo, maximum: hi, count: values.len() }
}
fn spread(values: []int): int {
b := bounds(values)
if b.count == 0 { return 0 }
return b.maximum - b.minimum
}The return type may be omitted entirely; the record shape is then inferred from the returned literal.
fn summarize(xs: []int) {
mut lo := 0
mut hi := 0
for v in xs {
if v < lo { lo = v }
if v > hi { hi = v }
}
return { count: xs.len(), low: lo, high: hi }
}
fn describe(xs: []int): string {
s := summarize(xs)
return "${s.count} values in [${s.low}, ${s.high}]"
}Prefer the explicit form on anything exported: an inferred record shape shows up
in diagnostics as $Rec<...>, which is harder to read than a written-out field
list.
Records in collections and closures
A record is an ordinary value, so it goes in lists, options, and closure results like any other.
fn doubled_table(xs: []int): int {
tagged := xs.map(v => { value: v, doubled: v * 2 })
return tagged.get(0)?.doubled ?? 0
}fn totals(rows: []{ id: int, amount: int }): int {
mut sum := 0
for row in rows {
sum += row.amount
}
return sum
}
fn f(): int {
return totals([{ id: 1, amount: 10 }, { id: 2, amount: 32 }])
}Query projections
A SELECT { ... } projection synthesizes a record type, which is why query
results can be consumed without declaring a row struct for every projection.
schema shop
model Booking {
id: int
passengers: int
price: float
}
fn revenue(): float ! SqlError {
rows := FROM Booking SELECT { id, total: passengers * price }?
mut sum := 0.0
for row in rows {
sum += row.total
}
return sum
}The element type here is { id: int, total: float } — the same structural type
you would get from writing that literal by hand. See
Query Projections.
Records versus structs
Reach for a record when the producer and the consumer are close together: a query projection used by one function, a helper returning two named results, a locally inferred intermediate shape.
Declare a struct when any of these apply:
- the shape crosses a module boundary or is part of a published API;
- it needs methods, trait implementations, or derives;
- its name carries domain meaning worth seeing in diagnostics;
- it must be recursive.
Records have no declaration site, so they cannot carry inherent methods or
implement traits. Promoting a record to a struct is a mechanical change — name
the type, keep the fields, and replace { ... } with Name { ... }.
A composed example
A small pipeline that never names a type: parse lines into records, filter them, and fold them into a summary record.
fn parse_line(line: string): { name: string, score: int }? {
parts := line.split(":")
if parts.len() != 2 { return None }
name := parts.get(0) ?? ""
score := (parts.get(1) ?? "0").to_int() ?? 0
return { name, score }
}
fn leaderboard(lines: []string) {
mut best := { name: "", score: 0 }
mut counted := 0
mut skipped := 0
for line in lines {
match parse_line(line) {
Some(entry) => {
counted += 1
if entry.score > best.score { best = entry }
}
None => skipped += 1
}
}
return { winner: best.name, top_score: best.score, counted, skipped }
}
fn report(lines: []string): string {
r := leaderboard(lines)
return "${r.winner} won with ${r.top_score} (${r.counted} ok, ${r.skipped} bad)"
}best = entry type-checks because parse_line returns exactly the record shape
best was initialized with — structural identity, no declaration required.