Skip to main content

TransformScript Syntax

This page is the reference for what is and isn't valid TransformScript. It covers the building blocks you write in a script: comments, literals, operators, and the forms that combine them into expressions. Read it when you need to confirm the exact syntax for a value or operator, or when you want to know why a piece of input is rejected.

For a guided introduction to writing scripts, see Writing a TransformScript.

Comments and Whitespace

Whitespace is insignificant. A ; is skipped and acts as a harmless separator. Comments come in three forms:

// line comment
# line comment
/* block comment (no nesting) */

A block comment must be closed. An unterminated block comment is invalid.

Literals

Strings

Strings use any of three interchangeable quote characters — "…", '…', or `…` — with identical meaning. Supported escapes are \n \t \r, \uXXXX (exactly four hex digits), and \$ \" \' \ \`. Any other escaped character passes through literally.

A string can interpolate an expression with $( … ):

"Hello, $(payload.name)!"
"Total: $(qty * price)"

The interpolated expression is parsed as ordinary TransformScript. An empty $() is invalid. A $ that is not followed by ( is a literal dollar sign.

Numbers

A number is digits or digits.digits. A value with a . is a decimal; otherwise it's an integer. TransformScript does not support radix (0xFF), exponent (1e3), or digit-grouping (1_000) forms. Negative numbers are not literals: -5 is the unary minus operator applied to 5.

Booleans and Null

true, false, null.

Temporal Literals

Dates, times, and durations are written between pipe characters, | … |, and classified by shape:

ExampleType
|2024-01-02|Date
|13:30:00|, |13:30:00.5|Time / LocalTime
|2024-01-02T13:30:00|LocalDateTime (no zone)
|2024-01-02T13:30:00Z|, |2024-01-02T13:30:00.5+05:00|DateTime (zoned)
|P1Y2M3DT4H|, |PT30M|, |P1W|Period (ISO-8601 duration)

Text between pipes that doesn't match one of these shapes is invalid.

Because | always opens a temporal literal, there is no binary | operator in the language. To thread a value through a series of steps, use the then function — see Functions and Lambdas.

Regular Expressions

Regular expressions are written /pattern/flags, for example /ab+/i. A / is read as a regular expression only where a value is expected: at the start of an expression, after an operator, an opening bracket, a comma, matches, and similar positions. Elsewhere, / is division.

Identifiers and Placeholders

Identifiers are [A-Za-z][A-Za-z0-9_]*. The placeholder tokens $, $$, and $$$ (runs of $) are the implicit lambda parameters (see Functions and Lambdas). A single _ is a standalone placeholder. @ is its own token, used for XML attribute access and as the current-element reference inside filter selectors.

Some identifiers are reserved and cannot be used as names: async, case, default, do, enum, for, import, input, ns, output, private, throw, type, unless, yield.

Keywords

true false null and or not default if else match case do var fun is to using as. The words mod, matches, at, and update are soft keywords that are meaningful only in position.

Operators and Precedence

The table lists TransformScript operators from lowest precedence (binds loosest) to highest (binds tightest). Binary operators are left-associative unless noted.

Precedence (low → high)Operators / forms
using(…) bindingsusing(a = …) body
conditionalif cond a else b
defaulta default b (use b when a is null)
ternarycond ? a : b
type castexpr as Type { options }
matchsubject match { … }
updatesubject update { … }
logical oror
logical andand
logical not (prefix)not x, !x
comparison== != < <= > >= ~=, and x is Type
additive / shift / concat+ - << >> ++
multiplicative* / % mod
unary minus (prefix)-x
postfixselector chains (.field, […]), function calls
primaryliterals, ( ) grouping, identifiers, objects, arrays, lambdas, do

Operator notes:

  • ++ is strict concatenation: string with string, array with array, or object with object (merge). + also concatenates when either operand is a string (1 + "a" produces "1a"), concatenates or pushes for arrays, and adds for numbers.
  • << and >> append and prepend (and merge objects). They are not bit shifts.
  • ~= is coercive equality. It coerces the operands to a common shape before comparing. == and != are strict.
  • default substitutes a fallback for null: payload.nickname default "n/a".
  • The lambda arrow -> is the two tokens - then >. ~ alone appears only in destructuring patterns.

See Operators and Precedence for the full operator reference, and Types and Coercion for how operators coerce their operands and what is, as, and ~= mean.

Expression Forms

Objects

{
  id: payload.id,
  "display name": payload.name,
  (dynamicKeyExpr): value,
  total: qty * price,
  note: payload.note if payload.note,     // entry included only if condition is truthy
  element @(lang: "en"): "Hi"             // XML attributes via @( … )
}

Keys may be bare identifiers, plain strings (no interpolation in a plain key), or dynamic (expr). Any entry may carry a trailing if cond to include it conditionally. A single key: value in value position is also a valid (braceless) one-entry object.

Arrays

[ 1, 2, 3, extra if includeExtra ]

Array elements may also carry a trailing if cond.

Ranges

Ranges exist only inside selector brackets and use the to keyword: payload.items[1 to 3]. There is no a..b or [a:b] range syntax.

Conditionals

There are three forms: the ternary cond ? a : b; if cond a else b, where the else is mandatory (there is no bare if and no then keyword); and match expressions (see Pattern Matching and Updates). Parentheses around an if predicate are optional.

Calls and Chaining

Functions are called in two equivalent ways: fn(receiver, args…) and the postfix (UFCS) form receiver fn(args…), which means the same call:

upper(payload.name)
payload.name upper()
payload.items map (row) -> row.id

There is no pipe operator and no .method() chaining. See Functions and Lambdas.

Blocks and Bindings

using(x = …, y = …) bodyExpr introduces local bindings for one expression. do { var x = …, fun f(a) = … --- body } is a block with its own bindings.

What Isn't Valid

The forms below are rejected. Each row shows the bad input and a plain description of why.

InputWhy it's rejected
0xFF, 1e3, 1_000Numbers can't use hex, exponent, or digit-grouping forms
"abcUnterminated string
a | bThere is no | operator; | always opens a temporal literal, so this reads as an unterminated temporal literal
input (as a name)input is a reserved word and can't be used as a name
|2024-13-99|Invalid temporal literal (no such month or day)
(1 + 2Missing closing parenthesis
1 +Expression ends with nothing after the +
a[1:3][a:b] range syntax isn't supported; use [1 to 3]
x match {}A match needs at least one case or an else