Version: 1.7.0 Date: May 2026 Status: Draft
Companion to the Okyline Core Language Quick Reference. Covers the $compute expression language for cross-field business rules and computed validations.
Okyline includes a pure, deterministic expression language for computed validations. It lets you express cross-field business rules - e.g. total == subtotal * (1 + taxRate) - that cannot be captured by per-field constraints alone. Expressions are declared in a $compute block and attached to fields with (%Name).
$compute BlockDeclared at root level, alongside $oky:
{
"$compute": {
"TaxAmount": "subtotal * taxRate",
"ValidTotal": "total == subtotal + %TaxAmount"
},
"$oky": {
"invoice": {
"subtotal": 100.0,
"taxRate": 0.2,
"total|(%ValidTotal)": 120.0
}
}
}↳ %TaxAmount references another expression; (%ValidTotal) validates the field. %Name.field.deep accesses members of the compute’s result.
Rules: - Expression names: start with letter, then letters/digits/underscores - Circular dependencies → parsing error - Expressions are evaluated lazily when referenced
Parameterized computes - suffix the name with a formal parameter list; call with matching arguments:
"$compute": {
"CategoryCheck(cat)": "category != cat || rate > 0",
"InRange(lo, hi)": "it >= lo && it <= hi"
},
"$oky": {
"age|(%InRange(18, 120))": 30,
"rateS|(%CategoryCheck('S'))": 20
}↳ Params shadow sibling fields inside the body; use this.field to unshadow. Args on a field constraint must be literals or dotted paths; full expressions are allowed in compute bodies. Arity mismatch = load error. Params must not collide with special variables (it, parent, index, …). A parameter bound to an object exposes its members via p.field; a list parameter composes with firstOf/at/findFirst/sum/…
(%Name)"fieldName|(%ExpressionName)": exampleValue
| Result | Validation |
|---|---|
true |
Pass |
false |
Fail - Compute error |
Non-boolean / null |
Fail - Compute error |
Constraint exclusivity: (%Name) cannot combine with other (...) constraints (range, comparison). Include range checks inside the expression instead.
Evaluation order: Type → Standard constraints (@, {...}, ~...~) → Computed expression
Container vs element compute:
"field|@ [size] (%ContainerCheck) -> (%ElementCheck)": [10, 20]
↳ Before ->: evaluated once on collection (it = collection). After ->: evaluated per element (it = element).
| Prefix | Resolves from |
|---|---|
| (none) | Current object |
this. |
Current object (explicit) |
parent. |
Parent of the current context |
root. |
Document root |
origin. |
Origin element (lambda only) |
prev. / next. |
Adjacent elements (lambda only) |
first. / last. |
Collection endpoints (lambda only) |
Field resolution: direct name access, dot notation for nested (address.city), missing fields → null, null-safe navigation.
The it variable: - In field constraint: value of the field being validated - In aggregation lambda on scalars: current element - In aggregation lambda on objects: current object (rarely needed - use property names) - In aggregation lambda on nested collections: the inner collection (composable: sum(map(matrix, sum(it)))) - Outside any context: null
| Variable | Description | Null when |
|---|---|---|
origin |
Element whose validation triggered the call | Never |
prev / next |
Adjacent elements | First / Last |
first / last |
Collection endpoints | Empty collection |
index |
0-based iteration index | Never |
size |
Collection size | Never |
| Predicate | True when |
|---|---|
isOrigin |
Current element is the origin |
isFirst |
Current element is first |
isLast |
Current element is last |
"$compute": {
"IsUnique": "countIf(parent.items, id == origin.id) == 1"
}↳ Inside the lambda: id = iteration element’s id, origin.id = validated element’s id
| Operator | Description | Null behavior |
|---|---|---|
?? |
Null coalescing (high precedence, short-circuit) | Returns right if left is null |
+ - * / |
Arithmetic | Null propagates. Exception: string + null → treats null as "" |
> < >= <= |
Comparison | Returns null |
== != |
Equality | null == null → true |
=== !== |
Strict equality (IEEE-754 bit-exact) | Strict |
&& \|\| ! |
Logical | Null → false; !null → true |
? : |
Ternary | Condition null → false branch |
List literals: [a, b, c] - inline list, usable anywhere a value is expected.
Precedence (high to low): ?? → * / → + - → comparisons → equality → && → || → ? :
| Function | Description | Example |
|---|---|---|
date(s, pattern?) |
Parse to LocalDate (default yyyy-MM-dd) |
date("2024-03-15") |
formatDate(d, pattern?) |
Format date to string | formatDate(d, "dd/MM/yy") → "15/03/24" |
today() |
Current system date | today() |
daysBetween(start, end) |
Days difference | daysBetween("2024-03-15","2024-03-18") → 3 |
plusDays(d, n) |
Add days | plusDays("2024-02-28", 1) → "2024-02-29" |
minusDays(d, n) |
Subtract days | |
plusMonths(d, n) |
Add months | plusMonths("2024-01-31", 1) → "2024-02-29" |
minusMonths(d, n) |
Subtract months | |
plusYears(d, n) |
Add years | |
minusYears(d, n) |
Subtract years | |
year(d) month(d) day(d) |
Extract component | month("2024-03-15") → 3 |
dayOfWeek(d) |
MON=1 … SUN=7 | dayOfWeek("2024-03-15") → 5 |
dayOfYear(d) |
1-366 | |
weekOfYear(d) |
ISO week 1-53 | |
quarter(d) |
1-4 | quarter("2024-09-15") → 3 |
semester(d) |
1-2 | |
isWeekend(d) |
True if Sat/Sun | |
isLeapYear(d) |
True if leap year | |
before(d1, d2) |
d1 < d2 | |
after(d1, d2) |
d1 > d2 | |
equals(d1, d2) |
d1 == d2 |
| Function | Description | Example |
|---|---|---|
length(s) |
String length (null → 0) | length("hey") → 3 |
isEmpty(s) |
Length == 0 | |
isNullOrEmpty(s) |
Null or empty | |
isNull(v) |
v is null | |
hasValue(v) |
v is not null (opposite of isNull) | hasValue(code) → presence check |
substring(s, start, len) |
Extract substring (len = length, not end index) | substring("Hello",1,3) → "ell" |
substringBefore(s, delim) |
Before first occurrence | substringBefore("a:b:c",":") → "a" |
substringAfter(s, delim) |
After first occurrence | substringAfter("a:b:c",":") → "b:c" |
substringBeforeLast(s, d) |
Before last occurrence | |
substringAfterLast(s, d) |
After last occurrence | substringAfterLast("a.b.txt",".") → "txt" |
replace(s, target, repl) |
Replace all | |
replaceFirst(s, t, r) |
Replace first | |
replaceLast(s, t, r) |
Replace last | |
trim(s) ltrim(s) rtrim(s) |
Trim whitespace | |
toUpperCase(s) toLowerCase(s) |
Case conversion | |
capitalize(s) decapitalize(s) |
First char case | capitalize("hello") → "Hello" |
startsWith(s, prefix) |
Check prefix | |
endsWith(s, suffix) |
Check suffix | |
contains(s, search) |
Substring found | |
removePrefix(s, p) |
Remove prefix if present | |
removeSuffix(s, sf) |
Remove suffix if present | |
removeRange(s, start, len) |
Remove len chars from start | removeRange("Hello",1,3) → "Ho" |
padStart(s, len, ch) |
Left pad | padStart("7",3,"0") → "007" |
padEnd(s, len, ch) |
Right pad | |
repeat(times, ch) |
Repeat char | repeat(5,"*") → "*****" |
indexOf(s, sub) |
First index (-1 if not found) | |
indexOfFirst(s, sub) |
Alias for indexOf | |
indexOfLast(s, sub) |
Last index |
String index handling: negative start → clamped to 0, negative length → 0, start > length → "", end > length → clamped. Never throws.
String ↔︎ List:
| Function | Description | Example |
|---|---|---|
chars(s) |
String → list of characters | chars("abc") → ["a","b","c"] |
split(s, sep) |
Split by literal separator | split("a,b,c", ",") → ["a","b","c"] |
join(coll, sep) |
Join list into string | join(["a","b"], "-") → "a-b" |
countIf(chars(name), in(it, ["a","e","i","o","u"])) // count vowels
join(filter(split(csv, ","), it != ""), ",") // clean CSV| Function | Description | Example |
|---|---|---|
abs(x) |
Absolute value | abs(-5) → 5 |
sqrt(x) |
Square root | sqrt(9) → 3.0 |
floor(x, scale?) |
Round down | floor(3.1415, 2) → 3.14 |
ceil(x, scale?) |
Round up | ceil(3.1415, 2) → 3.15 |
round(x, scale?, mode?) |
Round (default HALF_UP) | round(3.5, 0, "HALF_EVEN") → 4.0 |
mod(a, b) |
Remainder | mod(10, 3) → 1 |
pow(base, exp) |
Power | pow(2, 3) → 8.0 |
log(x) |
Natural log | |
log10(x) |
Base-10 log | log10(1000) → 3.0 |
toInt(v) |
Convert to integer | toInt(3.7) → 4 |
toNum(v) |
Convert to number | toNum("42") → 42.0 |
toStr(v) |
Convert to string | toStr(42) → "42" |
Rounding modes: HALF_UP (default), HALF_DOWN, HALF_EVEN, UP, DOWN, CEILING, FLOOR
Precision: Exact decimal arithmetic within 6 decimal places (configurable via $decimalScale, see Core spec §7.3). No IEEE-754 artifacts (0.1 + 0.2 = 0.3 exactly). Division by zero → null.
Type promotion: int + int → int, int + decimal → decimal, int / int → decimal (division always returns decimal).
| Function | Object collection | Scalar collection | Description |
|---|---|---|---|
sum |
sum(coll, expr) |
sum(coll) |
Sum |
average |
average(coll, expr) |
average(coll) |
Mean |
min |
min(coll, expr) |
min(coll) |
Minimum |
max |
max(coll, expr) |
max(coll) |
Maximum |
count |
count(coll) |
count(coll) |
Non-null elements |
countAll |
countAll(coll) |
countAll(coll) |
All elements (incl. null) |
countIf |
countIf(coll, pred) |
countIf(coll, pred) |
Count where true |
exists |
exists(coll, pred) |
exists(coll, pred) |
Any match |
notExists |
notExists(coll, pred) |
notExists(coll, pred) |
No match |
sumIf |
sumIf(coll, pred, expr) |
sumIf(coll, pred, val) |
Conditional sum |
map |
map(coll, expr) |
map(coll, expr) |
Transform each |
filter |
filter(coll, pred) |
filter(coll, pred) |
Keep where true |
sum(items, quantity * unitPrice) // object collection
sum([10, 20, 30]) // scalar collection
filter([1, 2, 3, 4], it > 2) // → [3, 4]
map(items, toUpperCase(code)) // transformMembership - in:
| Form | Example |
|---|---|
| List literal | in(status, ["DRAFT", "SENT"]) |
| Nomenclature | in(status, '$INVOICE_STATUS') |
| Array field | in(code, allowedCodes) |
↳ null value → false. List as first arg → containsAll semantics.
Lookup - lookup:
lookup(currency, rates) // get value by key from map
lookup(key, src) ?? defaultValue // with fallback
join(map(chars(code), lookup(it, charMap) ?? it), "") // char-by-char transformCompute reference - %Name:
"$compute": {
"LineTotal": "netAmount * (1 + vat)",
"InvoiceTotal": "sum(lines, %LineTotal)"
}↳ %LineTotal evaluates LineTotal for each element in the aggregation
List element access - retrieve a single element (null-safe):
| Function | Returns |
|---|---|
firstOf(coll) |
First element, null if empty |
lastOf(coll) |
Last element, null if empty |
findFirst(coll, pred) |
First matching element, null otherwise |
findLast(coll, pred) |
Last matching element (reverse scan), null otherwise |
at(coll, idx) |
Element at numeric idx (0-based, list/array, or positional access on map values), null if out of bounds / negative |
at(map, "key") |
Value for string key (map only - at(list, "0") returns null, no implicit String→Integer) |
Member access on any expression - chain .field after a function call or parenthesized expression:
firstOf(items).amount
findFirst(lines, cat == 'S').amount
%getParty('S').address.city
at(payments, 0).status↳ Dotted paths on bare identifiers (parent.x.y) are unchanged - the postfix .field applies after ) or ]. Resolution on null base → null.
| Context | Behavior |
|---|---|
Arithmetic (+ - * /) |
Null propagates |
String + |
Null treated as "" |
Comparisons (< > >= <=) |
Returns null |
Equality (==) |
null == null → true; null == x → false |
Logical (&& \|\|) |
Null → false |
!null |
→ true |
| Division by zero | → null |
| Missing field | → null |
obj.field on null obj |
→ null (null-safe) |
Okyline® is a registered trademark of Akwatype.