Expressions (PEL)
PEL (Phony Expression Language) is the small language used wherever PGDL composes or transforms values:
| Used in | As |
|---|---|
composition body / variants[].pattern | a template string with {{ … }} placeholders |
composition let.computed | a bare expression over prior let steps |
composition output leaves | expressions evaluated against the locals |
field computed | a bare expression over sibling fields |
modifiers.transform | an expression over the produced value |
Template syntax
Anything outside {{ … }} is literal text; inside is an expression. Escape braces with a backslash.
Hi {{first_name}}! Ref #{{number:1000-9999}}
\{\{ literal braces \}\}References
A reference resolves against the generator's own bindings — its params and any let results. There is no table, row, or sibling field.
{{first}} the value bound under "first" (a param or a `let` result)
{{country.code}} a nested property of an object-valued bindingA let binds a name to one value; referencing that name again yields the same value (so the same name twice always agrees — coherence is a property of the let, not of any outer scope).
Inline generators
Generate a value inline, without a separate generator definition. Each occurrence draws a fresh sub-seed.
{{number:1-100}} {{number:0.0-1.0:2}} {{number:1-100:step:5}}
{{pattern:###-!@}} # digit · % non-zero · ! upper · @ lower · ? letter · ^ HEX · _ hex · * alnum · \ escapes
{{date:2020-01-01..2024-12-31}} {{datetime:-1year..now}} {{time:09:00..18:00}}
{{uuid}} {{uuid:v7}} {{ulid}} {{nanoid}} {{nanoid:10}}Operators
| Class | Operators |
|---|---|
| arithmetic | + - * / % |
| comparison | == != < > <= >= |
| logical | && || ! |
| grouping | ( ) |
| literals | numbers, 'strings'/"strings", lists ["a","b"], objects { k: expr } |
String literals support the escapes \n (newline), \t (tab), \\ (backslash), and \" / \' (quotes); any other escaped character is kept literally (\d is d).
"grade": { "computed": "if(score >= 90, 'A', if(score >= 80, 'B', 'C'))" },
"total": { "computed": "round((subtotal + tax) * (1 - discount), 2)" }How operators coerce (comparison, equality, arithmetic)
The rules are strict and identical across every runtime — no stringly-typed equality, no silent string-to-0. A value is numeric when it is a JSON number or a string that trims to a finite decimal (" 12 " → 12); "12abc", "", "nan", "inf", booleans, null, arrays and objects are not numeric.
| Operators | Rule |
|---|---|
+ - * / %, unary - | both operands must be numeric, else an error. Numeric strings still compute ('2' + '3' → 5); 'a' + 'b' errors. + never concatenates — join strings with concat(a, b). |
== / != | null equals only null; two numerics compare numerically (1 == '1' → true, '2.0' == 2 → true); two strings by value; two bools by value; arrays/objects structurally. Every other cross-type pair is not equal (true == 1 → false, true == 'true' → false, null == '' → false). |
< <= > >= | two numerics compare numerically; two strings compare lexicographically by Unicode scalar ('a' < 'b' → true, '10' < '9' → false); any other mix is an error. |
&& || ! | operate on truthiness (null / 0 / "" / [] / {} / false are falsy). |
Because + is arithmetic-only, build strings with concat(): concat(first, ' ', last), never first + ' ' + last.
A few concrete cases worth internalising — the same in every runtime:
'2' + '3' → 5 numeric strings compute
'a' + 'b' → error non-numeric operands (use concat('a','b') → "ab")
1 == '1' → true two numerics, compared numerically
'2.0' == 2 → true " " "
true == 1 → false cross-type: bool never equals number
true == 'true' → false bool never equals string
null == '' → false null equals only null
'10' < '9' → false two strings: lexicographic, not numeric
10 < '9' → error number <> string is not comparableThe design goal is no surprises across ports: nothing silently coerces a string to 0, and null never sneaks into an equality except against another null.
Function reference
Every function is arity-checked: a wrong argument count, an unknown function, or a divide/modulo by zero is a clean BadParams error — generation fails with a message, never a panic.
Case & string
| Function | Result |
|---|---|
lowercase uppercase capitalize titlecase | case transforms |
camelcase snakecase kebabcase pascalcase | identifier casings |
trim reverse length repeat(s, n) | basic ops |
truncate(s, n[, suffix]) | cut to n chars |
substring(s, start[, len]) | slice |
pad(s, n[, ch]) padRight(s, n[, ch]) | pad to width |
replace(s, from, to) concat(…) slugify(s) | rewrite |
split(s, sep) join(arr, sep) | string ↔ array |
contains(s, x) starts_with(s, x) ends_with(s, x) index_of(s, x) | search (index_of → −1 if absent) |
numerify(s) letterify(s) hexify(s) alphanumerify(s) | fill pattern placeholders (# % ! @ ? ^ _ *, \ escapes) |
Numeric
| Function | Result |
|---|---|
add subtract multiply divide modulo | arithmetic (also + - * / %) |
round(n[, places]) floor ceil abs | rounding |
min(a, b) max(a, b) clamp(v, lo, hi) | bounds |
uniform(lo, hi) | random float in range |
pow(b, e) sqrt(n) log(n[, base]) sign(n) | powers, roots, logs, sign (−1/0/1) |
Conditional
| Function | Result |
|---|---|
if(cond, then, else) | branch |
switch(x, k: v, …, default: v) | multi-way; errors if no arm matches and no default is given |
coalesce(…) | first non-null |
default(v, fallback) | fallback if v is empty/null |
optional(v) | "" if null |
Lists
| Function | Result |
|---|---|
pick(list) | random element |
cycle(list) | element at row % len (sequential by row) |
sample(list, n|"a-b") | n unique elements (array) |
pick_weighted(list) | weighted pick from [{value, weight}] — weight 0 = never selected; all-zero weights are an error |
first(list) last(list) nth(list, i) | positional access |
sum(list) length(list) | aggregate / count |
Masking & crypto
| Function | Result |
|---|---|
mask(v, n[, "start"]) | show last (or first) n, mask the rest |
redact(v) | [REDACTED] |
hash(v[, "sha256"|"sha1"|"md5"]) | hex digest |
hmac(data, key) | HMAC-SHA256 hex |
base64(v) urlEncode(v) | encodings |
anonymize(v[, "domain"]) | consistent pseudonym (referential integrity) |
Format
| Call | Output |
|---|---|
format(x, "decimal:2") | 1234.56 |
format(x, "percent") | 12.34% |
format(x, "number") | 1,234 |
format(x, "currency[:CODE]") | $1,234.56 / ₺… |
format(x, "filesize") | 1.5 MB |
Date & time
| Function | Result |
|---|---|
now() today() | the reference instant |
age(date) | whole years to the reference instant |
addDays subtractDays addMonths addYears | shift a date |
startOfMonth endOfMonth startOfYear | snap a date |
dateDiff(a, b, "days"|"hours"|"months"|"years") | difference |
TIP
now/today/age and relative ranges never read the system clock — they resolve against an injected reference instant, so output stays reproducible.
For the complete language design (including parts still on the roadmap), see Spec → Expression Language.