Skip to content

PEL operators and coercion ​

The exact typed semantics of PEL operators, verified against crates/pgdl/src/pel/eval.rs (evaluator) and parse.rs (grammar/precedence). This is the portable contract: a re-implementation in another runtime must reproduce these rules exactly. The frozen behavioural vectors live in phony-core/conformance/vectors.json — treat that file as normative.

For the readable guide, see Expressions (PEL); for the callable functions, see PEL functions.

Numeric values ​

Several rules turn on whether a value is numeric. A value is numeric if it is:

  • a JSON number, or
  • a string that trims to a finite decimal — " 12 " is 12.

Everything else is non-numeric: "12abc", "", "nan", "inf" (parsing is all-or-nothing; non-finite is rejected), and every boolean, null, array, and object. This definition is shared by arithmetic, equality, and ordering.

Arithmetic — + - * / % and unary - ​

Both operands must be numeric, otherwise the expression is a BadParams error. There is no silent string-to-zero coercion (unlike the arithmetic functions, which coerce). A whole-number result is narrowed to an integer.

ExpressionResult
2 + 35
'2' + '3'5 (numeric strings)
'a' + 'b'error — use concat() to join strings
10 / 42.5
x / 0error — divide by zero
x % 0error — modulo by zero
-'5'-5
-'a'error — unary - needs a numeric operand

Equality — == / != ​

Typed: no cross-type stringly equality. != is the negation of ==.

left \ rightnullnumber / numeric-stringstringboolarray / object
nulltruefalsefalsefalsefalse
numberfalsenumeric ==numeric if numeric-string, else falsefalsefalse
stringfalsenumeric if numeric-string, else falsescalar ==falsefalse
boolfalsefalsefalsevalue ==false
array / objectfalsefalsefalsefalsestructural ==

Consequences:

ExpressionResult
null == nulltrue
null == ''false
1 == '1'true
'10' == '10.0'true (both numeric)
true == 1false
true == 'true'false
'a' == 'a'true
[1,2] == [1,2]true (structural)

Ordering — < <= > >= ​

Two numerics compare numerically; two strings compare lexicographically by Unicode scalar value. Any other mix is a BadParams error — there is no surprising cross-type ordering.

ExpressionResult
2 < 10true
'a' < 'b'true
'10' < '9'false (both numeric strings → numeric compare)
5 < 'a'error — cannot compare number and non-numeric string

Logical — && \|\| ! ​

Operate on truthiness and short-circuit. && and || return a boolean.

Falsy values: null, 0, "", [], {}, false. Everything else is truthy.

ValueTruthy?
nullno
false / trueas-is
0 / any non-zero numberno / yes
"" / non-empty stringno / yes
[] / non-empty arrayno / yes
{} / non-empty objectno / yes

Precedence ​

Lower rows bind tighter (verified against binding_power in parse.rs). All binary operators are left-associative; ! and unary - are prefix and bind tighter than every binary operator.

PrecedenceOperators
1 (loosest)||
2&&
3== !=
4< > <= >=
5+ -
6* / %
7 (tightest)unary ! unary -

So 1 + 2 * 3 parses as 1 + (2 * 3) and a || b && c as a || (b && c). Parenthesize to override.

Literals and references ​

  • Numbers: decimal digits with an optional . (e.g. 3.14).
  • Strings: single- or double-quoted.
  • Booleans / null: true, false, null.
  • Lists: [a, b, c].
  • Objects: { key: value, … } (keys are identifiers or strings).
  • References: a bare name, optionally dotted to index object properties (country.code). A name resolves against the generator's local bindings (its params and let results) or the bound value; indexing a non-object is an error. There is no table, row, or sibling-field scope.
  • Calls: name(arg, …).

String escapes ​

Inside a string literal the lexer recognizes:

EscapeResult
\nnewline
\ttab
\\backslash
\" \'the quote character
\x (any other)the character x literally (so \d is d)

= alone is a parse error (use ==); a lone & or \| is a parse error (use && / ||).

Templates vs. expressions ​

PEL has two surfaces:

  • A template is a string with … placeholder … segments. It is scanned into literal text, inline-generator nodes, and expression nodes. A placeholder whose trimmed body starts with an inline keyword (number, pattern, date, datetime, time, uuid, ulid, nanoid) is an inline generator; any other body is an expression. In template text, \{, \}, and \\ escape to literal braces / backslash; an unterminated placeholder is an error.
  • An expression is a bare PEL expression (no surrounding text), evaluated with its native type preserved.

Where a value is authored as a string (composition let params, output leaves), the rule is:

  • a string that is exactly one whole placeholder is evaluated as an expression and keeps its native type (number, boolean, array, …);
  • a string with embedded placeholders renders to text;
  • a string with no placeholders passes through unchanged.

The following fenced examples use the placeholder braces literally:

"{{ 1 + 2 }}"            → 3        (whole expression: native number)
"total: {{ 1 + 2 }}"     → "total: 3"   (embedded: rendered text)
"plain"                  → "plain"  (no placeholder: passthrough)

Rendering a value into text uses string coercion: integral numbers print without a decimal point, null prints as empty, arrays/objects print as JSON.

Phony Cloud — Documentation & Specification