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()