← Back to home

Standard library

Loom ships a small standard library in two layers. Layer 0 is the set of built-in intrinsics — irreducible operations on scalar and collection receivers that the compiler renders natively on every backend (and, where marked queryable, pushes down to SQL). Layer 1 is the ambient prelude — ordinary expression-form top-level functions written in Loom on top of Layer 0, callable with nothing imported and inlined at each call site.

This page is generated from the registries; see docs/language.md for the surrounding expression-language reference.

Layer 0 — scalar intrinsics

Built-in operations on a scalar receiver. A queryable op may appear in a find … where predicate (and criterion / capability filters); a non-queryable one in that position is rejected — loom.find-where-not-queryable on a repository find, loom.criterion-not-selectable on a criterion or capability filter — rather than silently degrading.

startsWith is the one BOOL-returning queryable op, so it stands alone as a whole predicate (filter this.dataKey.startsWith(p)) instead of feeding a comparison. It lowers to an ANCHORED position test on every backend (strpos(col, $p) = 1, JPQL locate(:p, col) = 1, EF Core's one-argument StartsWith) rather than col LIKE $p || '%': the argument is a value, so a % or _ inside it matches literally.

These are Loom's OWN spellings, translated per target — s.toUpper() is toUpperCase() in JavaScript, .ToUpper() in C#, String.upcase/1 in Elixir. Two carry semantics the host language spells the same but defines differently, so read the contract, not the name: replace replaces every occurrence (not just the first), and substring(start, len) takes a LENGTH (not an end index).

Available in aggregate/operation bodies on all five backends, and in page / component expressions on the four JS frontends (React / Vue / Svelte / Angular), which share the TypeScript backend's snippet table. Feliz and Flutter page bodies do not translate them yet.

string

op signature queryable
trim (): string yes
toUpper (): string yes
toLower (): string yes
substring (start: int, len?: int): string no
startsWith (s: string): bool yes
endsWith (s: string): bool no
contains (s: string): bool no
replace (find: string, repl: string): string no
split (sep: string): string[] no

int

op signature queryable
abs (): int yes
min (other: int): int yes
max (other: int): int yes
divTrunc (divisor: int): int no

long

op signature queryable
abs (): long yes
min (other: long): long yes
max (other: long): long yes
divTrunc (divisor: long): long no

decimal

op signature queryable
abs (): decimal yes
min (other: decimal): decimal yes
max (other: decimal): decimal yes
round (places?: int): decimal yes
floor (): decimal yes
ceil (): decimal yes

money

op signature queryable
abs (): money yes
min (other: money): money yes
max (other: money): money yes
round (places?: int): money yes
floor (): money yes
ceil (): money yes

date

op signature queryable
year (): int no
month (): int no
day (): int no
daysUntil (other: date): int no

datetime

op signature queryable
startOfDay (): datetime yes
toDate (): date no

Collection operations

Operations on a collection receiver T[]. These render in-memory on every backend that executes domain logic; they are non-queryable (a reference- collection contains is the one exception — it pushes down to an EXISTS subquery). λ is a lambda whose parameter is bound to the element type.

Nine of these render in a page body too. The ops that RESHAPE a collection — count, where, any, all, map, sortBy, take, skip, join — have a real renderer on every frontend (React / Vue / Svelte / Angular / Feliz / Flutter, and the Phoenix LiveView walker), so rows.where(o => o.open).count is an ordinary page expression.

The other eight are refused in a page / component / store expression (loom.frontend-collection-op-unsupported), because the frontends disagree on how the RESULT is represented: sum / min / max / avg fold arithmetic, and money is a Decimal object on the JS frontends and Phoenix but a native scalar on Feliz and Flutter; first / firstOrNull differ on emptiness and on the optional type; distinct / contains need value equality, which Flutter's generated models do not define. For those, compute the value in a repository find, an aggregate derived, or a projection read model and bind the result in the page. See page-metamodel.md §8.1.

op signature
count int
sum (λ?): U
all (λ): bool
any (λ): bool
where (λ): T[]
first T
firstOrNull T?
contains bool
map (λ): U[]
sortBy (λ, desc?: bool): T[]
distinct T[]
take (n: int): T[]
skip (n: int): T[]
join (sep: string): string
min (λ): T?
max (λ): T?
avg (λ): decimal?

Value-object equality

Every value object answers one member it does not declare:

op signature
equals (other: Self): bool

a.equals(b) is VALUE equality — field by field, the defining property of a value object — and means exactly what a == b means on two value objects. It lowers to that same comparison, so each backend renders its own value-equality idiom: python == (frozen-dataclass __eq__), Java Objects.equals (record equals), .NET record ==, Elixir Loom.ValueObject.equal?/2 (field-wise across the map, column and collection-element shapes a value object takes there), TypeScript the equals() method every value-object class emits. The argument must be the same value object type and the receiver must be non-null (the loom.intrinsic-* diagnostics); a value object that declares its own equals member keeps it. Domain logic only — in a page expression compare the fields you mean instead.

Layer 1 — the ambient prelude

Auto-injected top-level functions, callable in any .ddd with nothing imported. Each is expression-form, so a call inlines and an uncalled function emits nothing; a user-declared top-level function of the same name shadows the prelude.

strings

String predicates and shaping.

// Loom stdlib — strings.
function isBlank(s: string): bool = s.trim().length == 0
function isPresent(s: string): bool = s.trim().length > 0
function truncate(s: string, n: int): string = s.substring(0, n)

math

Numeric clamping, ratios, and rounding.

// Loom stdlib — math.
function clamp(n: int, lo: int, hi: int): int = n.max(lo).min(hi)
function percentOf(part: decimal, whole: decimal): decimal = part / whole * 100
function roundTo(n: decimal, places: int): decimal = n.round(places)

temporal

Datetime comparisons against now(), and today()'s UTC calendar date.

// Loom stdlib — temporal.
function isOverdue(due: datetime): bool = now() > due
function isFuture(t: datetime): bool = t > now()
function isPast(t: datetime): bool = t < now()
function today(): date = now().toDate()