Skip to content

Expressions (PEL) ​

PEL (Phony Expression Language) is the small language used wherever PGDL composes or transforms values:

Used inAs
composition body / variants[].patterna template string with {{ … }} placeholders
composition let.computeda bare expression over prior let steps
composition output leavesexpressions evaluated against the locals
field computeda bare expression over sibling fields
modifiers.transforman expression over the produced value

Template syntax ​

Anything outside {{ … }} is literal text; inside is an expression. Escape braces with a backslash.

text
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.

text
{{first}}            the value bound under "first" (a param or a `let` result)
{{country.code}}     a nested property of an object-valued binding

A 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.

text
{{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 ​

ClassOperators
arithmetic+ - * / %
comparison== != < > <= >=
logical&& || !
grouping( )
literalsnumbers, '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).

json
"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.

OperatorsRule
+ - * / %, 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:

text
'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 comparable

The 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 ​

FunctionResult
lowercase uppercase capitalize titlecasecase transforms
camelcase snakecase kebabcase pascalcaseidentifier 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 ​

FunctionResult
add subtract multiply divide moduloarithmetic (also + - * / %)
round(n[, places]) floor ceil absrounding
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 ​

FunctionResult
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 ​

FunctionResult
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 ​

FunctionResult
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 ​

CallOutput
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 ​

FunctionResult
now() today()the reference instant
age(date)whole years to the reference instant
addDays subtractDays addMonths addYearsshift a date
startOfMonth endOfMonth startOfYearsnap 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.

Phony Cloud — Documentation & Specification