Expressions

Syntax and semantics of AOP expressions

This document specifies the syntax and semantics of the AOP Expression Language.

Expressions are built on top of the AOP Data Model.

They are available in various contexts such as filter conditions in data APIs, or in conditions (condition, available) in device adapter XMLs.

Grammar

The syntax of AOP expressions can be defined by the following grammar in ABNF form (RFC 5234). As usual, all string sequences in the grammar are case-insensitive.

; character classes
WSP       = " " / %x09 / %x0D / %x0A   ; space, \t, \r, \n
DIGIT     =  %x30-39                   ; 0-9
HEXDIGIT  = DIGIT / %x41-46 / %x61-66  ; 0-9, A-F, a-f
ALPHA     = %x41-5A / %x61-7A          ; A-Z, a-z

; number literals
decimal-int    = [ "-" ] 1*DIGIT
hex-int        = [ "-" ] "0x" 1*HEXDIGIT
fraction       = "." 1*DIGIT
exponent       = "e" [ "+" / "-" ] 1*DIGIT
float          = [ "-" ] 1*DIGIT ( fraction [ exponent ] / exponent )
number-literal = float / decimal-int / hex-int

; strings
sqs-char      = %x20-26 / %x28-7E / %x80-10FFFF  ; Unicode code points except ASCII control chars and ' (0x27)
dqs-char      = %x20-21 / %x23-7E / %x80-10FFFF  ; Unicode code points except ASCII control chars and " (0x22)
escape-sequence = "\" ( "\" / "'" / %x22 / %x09 / %x0D / %x0A ) ; \' \" \\ \t \r \n
                / "\" %x75 "{" 1*6HEXDIGIT "}" ; \u{X...X} for Unicode code points
single-quoted-string = "'" *( sqs-char / escape-sequence ) "'"
double-quoted-string = %x22 *( dqs-char / escape-sequence ) %x22
string-literal  = single-quoted-string / double-quoted-string

; variable
symbol        = [ "@" ] ALPHA *( ALPHA / DIGIT / "_" )
identifier    = symbol *( "." symbol )

; guid
guid-literal  = "{" 8HEXDIGIT "-" 4HEXDIGIT "-" 4HEXDIGIT "-" 4HEXDIGIT "-" 12HEXDIGIT  "}"

; null, bool, special float
keyword-literal = "null" / "true" / "false" / "NaN" / "Infinity"

; expressions
expression    = *WSP primary *WSP
primary       = "(" expression ")"
              / literal                                          ; -> ConstExpression
              / identifier                                       ; -> VariableExpression
              / identifier 1*WSP "exists"                        ; -> VariableExistsExpression
              / expression *WSP binary-operator *WSP expression  ; -> BinaryExpression
              / unary-operator *WSP expression                   ; -> UnaryExpression
              / expression postfix-operator                      ; -> UnaryExpression
              / identifier *WSP "(" [ argument-list ] ")"        ; -> FunctionCallExpression
              / "[" [ argument-list ] "]"                        ; -> ArrayExpression

argument-list = expression *( "," expression )

binary-operator = symbolic-binary-operator | (1*WSP textual-binary-operator 1*WSP)
symbolic-binary-operator = "+" / "-" / "*" / "/" / "%"
                         / "&&" / "||"
                         / "&" / "|" / "^"
                         / "=" / "==" / "<" / "<=" / ">" / ">="
textual-binary-operator  = "and" / "or" / "in" / "not" 1*WSP "in"
                         / "lt" / "lte" / "gt" / "gte"
                         / "bitand" / "bitor" / "bitxor"

unary-operator = symbolic-unary-operator | (textual-unary-operator 1*WSP)
symbolic-unary-operator = "-" / "!" / "~"
textual-unary-operator = "not"

postfix-operator = 1*WSP ( "is" 1*WSP "empty" / "is" 1*WSP "not" 1*WSP "empty" )
literal = keyword-literal / string-literal / number-literal / guid-literal

Keywords

The following is a list of reserved keywords: null, true, false, NaN, Infinity, not, if, match, let.

Keywords can not be used as identifiers. For example, if the parser encounters the token true in a context where an identifier is acceptable, it must be parsed as a keyword literal instead of an identifier. However, if the keyword is merely a prefix of an identifier (e.g. trueValue, null.var), it must be parsed as an identifier instead.

Keywords are case-insensitive, so TRUE, True, and true are all valid keyword literals that produce the boolean value true.

The keyword not is reserved and only used as a unary operator, since a function call to a function with that name would be ambiguous with the operator and a parenthesized operand (i.e., not (x)). For this reason, further textual prefix unary operators should not be added in the future.

The keywords if, match, and let are reserved for future use. Using them as identifiers will result in a parsing error.

Literals

A literal is a constant AOP value that is directly represented in the expression syntax.

Not all types have literal syntax; for example, there is no literal syntax for time or data values, but they can be constructed using built-in type constructor functions as described in the Type functions section.

The array syntax ([a, b, c]) is technically not a literal in this sense, since each element can contain arbitrary sub-expressions (including variable references or function calls) and its value is not fixed until evaluated.

AOP expressions support the following literal types:

Null literal

null produces the null value.

Bool literals

true and false produce the corresponding boolean values.

Integer literals

Integer literals are either decimal (e.g., 42, -100) or hexadecimal (e.g., 0xFF, -0x1A). The leading - is considered part of the literal if it is immediately followed by a digit; otherwise, it is parsed as a unary negation operator.

Values larger than 263-1 or smaller than −263 must result in a parsing error.

Hexadecimal literals are parsed as a magnitude with optional leading -, not as a raw bit pattern, so -1 is expressed as -0x1, not 0xFFFFFFFFFFFFFFFF. A negative literal therefore always has a leading -, and there is no exception for hex literals. The interpretation of hex literals also does not depend on the integer storage size.

Float literals

Decimal numbers with fractional part and/or exponent (e.g., 3.14, -1e-5). If a float literal has neither a fractional part nor an exponent, it is parsed as an integer literal instead (e.g., 1 is an integer, not a float). The leading - is considered part of the literal if it is immediately followed by a digit; otherwise, it is parsed as a unary negation operator.

Values exceeding the range of representable finite 64-bit float values (approximately ±1.7976931348623157e+308) must result in a parsing error. Literals with a magnitude too small to represent may round to zero and are accepted.

Keyword literals NaN and Infinity represent the special float values NaN and positive infinity, respectively. Negative infinity can be represented as -Infinity (which can be parsed as unary - operator plus Infinity constant expression, or as an combined -Infinity constant expression, both of which are equivalent).

String literals

String literals are text enclosed in single or double quotes, with support for escape sequences. Control characters (U+0000 to U+001F, U+007F) are not allowed unescaped in string literals, other Unicode characters are allowed verbatim.

The following escape sequences are supported:

  • \' → '
  • \" → "
  • \\ → \
  • \t → tab (U+0009)
  • \r → carriage return (U+000D)
  • \n → line feed (U+000A)
  • \u{X...X} → Unicode code point with 1 to 6 hex digits (e.g., \u{1F600} for 😀)

An invalid escape sequence results in a parsing error. Unicode code points above U+10FFFF are not allowed and result in a parsing error.

Guid literals

Guid literals use the standard format enclosed in curly braces (e.g., {550e8400-e29b-41d4-a716-446655440000}).

Operators

The following operators are supported in AOP expressions. Operators are listed from highest to lowest binding strength. The parser must respect the precedence rules when parsing expressions without parentheses; for example, 1 + 2 * 3 must be parsed as 1 + (2 * 3), not (1 + 2) * 3. Operators with the same precedence are parsed according to their associativity rules as described below; for example, 1 - 2 - 3 must be parsed as (1 - 2) - 3, not 1 - (2 - 3). As usual, parentheses can be used to override precedence and associativity rules when needed.

Level Operator Alias Category Description
1 - - Prefix unary Unary negation
~ - Prefix unary Bitwise NOT
! not Prefix unary Logical NOT
2 is empty - Postfix unary Checks if empty
is not empty - Postfix unary Checks if not empty
3 * - Binary Multiplication
/ - Binary Division
% - Binary Modulus
4 + - Binary Addition
- - Binary Subtraction
5 & bitand Binary Bitwise AND
6 ^ bitxor Binary Bitwise XOR
7 | bitor Binary Bitwise OR
8 in - Binary Membership test
not in - Binary Negated membership test
9 < lt Binary Less than
<= lte Binary Less than or equal
> gt Binary Greater than
>= gte Binary Greater than or equal
10 == = Binary Equality
!= - Binary Inequality
11 && and Binary Logical AND
12 || or Binary Logical OR

All binary and unary postfix operators are left-associative. All unary prefix operators are right-associative.

Note that exists is not an operator but rather a reserved keyword that can only appear immediately after an identifier to form a VariableExistsExpression. It binds tighter than any operator, so !x exists is parsed as !(x exists), not (!x) exists.

Lexical categories

Operators are divided into two lexical categories:

  • Symbolic operators (e.g., +, &&, !=)
    • Symbolic operators are self-delimiting and do not require surrounding whitespace.
  • Textual operators (e.g., and, or, is not empty)
    • Textual operators must be surrounded by at least one whitespace character (on both sides for binary operators, on the operand side for unary operators).
    • Textual operators are case-insensitive, so AND, And, and and are all valid and equivalent.
    • Textual operators that consist of multiple words (e.g., is not empty) must have at least one whitespace character between each word.
Operator aliases

Some operators have multiple equivalent forms. All forms are semantically equivalent and have the same precedence and associativity. They are provided as aliases for convenience and readability; for example, in XML contexts where & and < characters would require escaping, and can be used instead of &&, and lt can be used instead of <. The alias for = for == is provided to prevent errors, but the use of == is generally recommended.

Abstract syntax

The abstract syntax defines the logical structure of AOP expressions by decomposing them into distinct expression types. This representation is independent of concrete syntax and defines the semantic categories that the parser produces. Each expression type corresponds to a specific grammatical construct and has well-defined evaluation semantics as described in the Semantics section. The abstract syntax of AOP expressions is defined as follows:

AopExpression = 
| ConstExpression(AopValue Value)
| VariableExpression(SymbolPath Name)
| VariableExistsExpression(SymbolPath Name)
| BinaryExpression(AopExpression Left, BinaryOperation Operation, AopExpression Right)
| UnaryExpression(UnaryOperation Operation, AopExpression Operand)
| FunctionCallExpression(SymbolPath FunctionName, AopExpression[] Operands)
| ArrayExpression(AopExpression[] Elements)

An implementation must disallow creating AST nodes which do not round-trip. In other words, it must be possible to unambiguously convert between concrete syntax and the above abstract syntax. In particular:

  • Creating VariableExpression, VariableExistsExpression, or FunctionCallExpression with an empty name or with a name that is a reserved keyword is not allowed.

Semantics

The following section defines the semantics of AOP expressions, i.e., how expressions are evaluated to produce values.

ConstExpression

A ConstExpression evaluates to its declared AOP value.

VariableExpression

A VariableExpression evaluates to the value of the variable with the given name in the current environment.

Which variables are available in the environment depends on the context in which the expression is evaluated. For device adapter XMLs, refer to Device adapter XML contexts for details.

If a variable with the given name does not exist in the provided environment, the expression evaluates to null.

The same variable should always evaluate to the same value within a single evaluation of an expression. The evaluator may therefore cache variable values to avoid repeated lookups, but it must not cache values across multiple evaluations of the same expression.

VariableExistsExpression

A VariableExistsExpression evaluates to true if a variable with the given name exists in the current environment, and false otherwise.

BinaryExpression

The semantics of a BinaryExpression depend on the specific BinaryOperation. See the Operators section below for details.

With the exception of logical AND (&& / and) and OR (|| / or), all other binary operators evaluate both operands first, then apply the operator to the resulting values (eager evaluation).

Logical AND and OR use short-circuiting evaluation as described in the Operators section.

UnaryExpression

The semantics of a UnaryExpression depend on the specific UnaryOperation. See the Operators section below for details.

All unary operators evaluate their operand first, then apply the operator to the resulting value (eager evaluation).

FunctionCallExpression

All arguments are evaluated first, then the function is called (applicative order evaluation). The expression is evaluated to the return value of the function. The semantics of a FunctionCallExpression depend on the specific function being called.

Function names are case-insensitive, i.e., time("2024-01-01T00:00:00Z") and Time("2024-01-01T00:00:00Z") are equivalent.

See the Built-in functions section below for details on built-in functions; custom functions may have their own semantics as defined by their implementation.

ArrayExpression

An ArrayExpression is evaluated by evaluating each element expression in order, and returning an array value containing the resulting values.

Operator semantics

Binary operators receive exactly two operands and return a value. Unary operators receive exactly one operand and return a value.

Comparison operators

All comparison operators return bool. All comparison operators are defined for all operand types; there is no combination of operand types that does not return a boolean.

When comparing two AOP values, the result is one of the following:

  • Equal: Both operands are considered equal.
  • Less: The left operand is considered less than the right operand.
  • Greater: The left operand is considered greater than the right operand.
  • Unordered: The operands are not equal but also cannot be meaningfully ordered.

Depending on the operator, the comparison result is then interpreted as follows to produce the final boolean result:

Operator Equal Less Greater Unordered
==, = true false false false
!= false true true true
<, lt false true false false
<=, lte true true false false
>, gt false false true false
>=, gte true false true false

Value promotion

When comparing values of different types, comparison operators use the rules defined in Data model: conversions and comparison of values to promote values to a common type first.

If the values cannot be converted to a common type, the result is Unordered. Comparing null with any other type always results in Unordered.

Legacy time formats

In addition to the standard type conversion rules, expressions also allow additional legacy formats when converting string to time values:

  • “yyyy-M-d H:m:s” (example: “2026-6-19 8:30:45” or “2026-06-19 08:30:45”)
  • “yyyy-M-d H:m” (example: “2026-6-19 8:30” or “2026-06-19 08:30”)
  • “yyyy-M-d” (example: “2026-6-19” or “2026-06-19”)

These formats do not specify a time zone; they must be interpreted as the respective local time of the system. Because this is indeterministic, it is recommended to use the standard ISO 8601 format when possible, which is unambiguous and universally supported.

Value comparison

Values of the same type are compared according to the following rules:

Type Ordering
(null) Equal only to null
bool Equal if both are true or both are false, otherwise Unordered
int Equal, Less or Greater based on integer values
float Equal, Less or Greater based on IEEE 754 rules; NaN is Unordered with everything including itself
string Equal, Less or Greater based on Unicode code-point ordinal; no locale, no normalization
guid Equal, Less or Greater based on guid comparison rules
time Equal, Less or Greater based on chronological order
data Equal, Less or Greater based on lexicographic byte order
object Equal iff same property set (case-insensitive) with pairwise-equal values; otherwise Unordered
map Equal iff same key set (case-sensitive) with pairwise-equal values; otherwise Unordered
array Equal, Less, Greater or Unordered; lexicographic by element; first non-equal element pair determines order, result is Unordered if that element pair is Unordered; if one array is a prefix of the other, the shorter array is Less
indexed array Equal iff same index set with pairwise-equal values; otherwise Unordered

Logical operators

Logical AND (&& / and) and OR (|| / or)

&& / and (short-circuit AND):

  1. Evaluate left operand.
    1. Convert the operand value to bool according to standard conversion rules, defaulting to false if conversion fails.
    2. If the result is false, return false without evaluating right.
  2. Evaluate right operand.
    1. Convert the operand value to bool according to standard conversion rules, defaulting to false if conversion fails.
    2. Return the result of the right operand evaluation.

|| / or (short-circuit OR):

  1. Evaluate left operand.
    1. Convert the operand value to bool according to standard conversion rules, defaulting to false if conversion fails.
    2. If the result is true, return true without evaluating right.
  2. Evaluate right operand.
    1. Convert the operand value to bool according to standard conversion rules, defaulting to false if conversion fails.
    2. Return the result of the right operand evaluation.

Logical NOT (! / not)

If the operand is null, the result is null. For all other values, applies the same truthiness rule as is empty.

Arithmetic operators

Arithmetic operators are defined for int and float as described below.

All arithmetic operators involving float follow IEEE 754 double-precision semantics, including overflow, underflow, and special values like NaN and Infinity. For example, 1.0 / 0.0 → Infinity, -1.0 / 0.0 → -Infinity, 0.0 / 0.0 → NaN, and any operation involving NaN results in NaN.

Addition (+)

Performs addition for numeric types and concatenation for strings.

A B Description
int int Integer addition, with overflow wrapping
float float IEEE 754 double precision addition
int float Convert int to float, then add as floats
float int Convert int to float, then add as floats
string string String concatenation

Subtraction (-), multiplication (*), division (/), modulus (%)

Performs the corresponding arithmetic operation for numeric types.

A B Description
int int Integer operation, with overflow wrapping
float float IEEE 754 double precision operation
int float Convert int to float, then operate as floats
float int Convert int to float, then operate as floats

Special cases:

  • Integer division by zero evaluates to null.
  • Integer modulus by zero evaluates to null.
  • -9223372036854775808 / −1 and -9223372036854775808 % −1 evaluates to null.
  • int / int: Truncates toward zero.
  • int % int: Sign follows the dividend (e.g., -1 % 2 → -1, 1 % -2 → 1).

Unary negation (-)

Unary negation is defined for int and float as the arithmetic negation of the value. −(-9223372036854775808) overflows back to -9223372036854775808.

Bitwise (&, |, ^, ~)

Both operands must be int. All operations are 64-bit.

Operator Type Description
&, bitand binary Bitwise AND: x & y has a 1 bit wherever both x and y have a 1 bit
|, bitor binary Bitwise OR: x | y has a 1 bit wherever either x or y has a 1 bit
^, bitxor binary Bitwise XOR: x ^ y has a 1 bit wherever x and y differ
~ unary Bitwise NOT: ~x has a 1 bit wherever x has a 0 bit

Membership (in / not in)

Returns bool. The semantics of in depend on the types of the left and right operands as described in the table below.

not in is the logical negation of in, i.e., x not in y is equivalent to !(x in y).

Left operand Right operand Semantics
not array array true iff the left operand equals at least one element of the array according to == semantics
array array true iff every element of the left array equals at least one element in the right array according to == semantics
string string true iff the left string is a contiguous substring of the right string

The following edge cases illustrate the semantics of in:

Not-array-in-array (x in arr):

  • Duplicate elements in the right operand do not affect the result; for example, x in [x, x] is true.
  • null in arr is true if any element of arr is null.
  • x in [] is always false.
  • To check if an array value x is in an array, use [x] in arr (i.e., wrap x in a single-element array).

Array-in-array (arr1 in arr2):

  • Duplicate left elements do not require duplicates on the right, e.g., [x, x] in [x] is true.
  • [] in arr2 is always true.

String-in-string (s in t, both string):

  • "" in t is always true.

is empty / is not empty

Returns bool. is not empty is the logical negation of is empty.

A value is empty according to the following per-type rules:

Type Empty when
null always
bool value is false
int value is 0
float value is 0.0 (note: NaN is not empty)
string value is ""
guid value is all-zeros
time value is the epoch / zero time
data zero bytes
object no properties or
object schema is known, object type is sparse and all properties are empty
map no entries
array zero elements
indexed array no entries

Since a missing variable evaluates to null, missing_var is empty is true.

Built-in functions

AOP expressions provide a set of built-in functions which are available in every implementation and in any context. Additional custom functions beyond these built-ins may be provided by specific implementations.

For each function, the syntax, semantics, and error behavior are defined in this section.

If a built-in function is called with an incorrect number of arguments, or with unsupported argument types, or if a evaluation error occurs, the result is null. Note that functions do not automatically convert argument types; for example, if a built-in function requires a string argument, passing an int will not be converted automatically unless a function explicitly documents and supports it.

Type functions

AOP expressions provide type functions for all AOP value types (except for null). These functions serve as both type constructors and type conversion functions, depending on the way they are called.

When used as a type constructor, they can create values of types for which there is no literal syntax in the AOP expression grammar. For example, to create a time value, you can use the Time function with an appropriate argument.

Type constructors can also be used to coerce values that have decayed to string during transit back to their original types (e.g., guid, time, data).

All type functions follow a common pattern for their behavior based on the number and types of arguments:

  • When called with no arguments, they return the empty/zero value of the corresponding type. For example, Guid() returns an empty GUID, Object() returns an empty object.
  • When called with one argument, they attempt to convert that argument to the corresponding type using the standard conversion rules defined in the Data Model conversion tables. If the conversion fails, the result is null.
  • Calling with more than one argument is only supported for Object, Map and IndexedArray, where it serves as a variadic constructor for creating values from key-value pairs.

For all type functions, unsupported arity, unsupported argument types, or violated constructor constraints result in null.

Bool([value]) → bool

  • Bool() returns false.
  • Bool(value) applies standard conversion to bool.

Int([value]) → int

  • Int() returns 0.
  • Int(value) applies standard conversion to int.

Float([value]) → float

  • Float() returns 0.0.
  • Float(value) applies standard conversion to float.

String([value]) → string

  • String() returns "".
  • String(value) applies standard conversion to string.

Guid([value]) → guid

  • Guid() returns the empty GUID.
  • Guid(value) applies standard conversion to guid.

Time([value]) → time

  • Time() returns the zero or empty time value.
  • Time(value) applies standard conversion to time. It also supports the legacy time formats described in the legacy time formats section for string inputs, in addition to the standard ISO 8601 UTC format.

Data([value]) → data

  • Data() returns empty data.
  • Data(value) applies standard conversion to data.

Object(...) → object

  • Object() returns an empty object.
  • Object(value) applies standard conversion to object.
  • Object(prop1, value1, prop2, value2, ...) constructs from key-value pairs.
    • Pair constructor constraints:
      • Argument count must be even.
      • The first argument of each pair is a property name, which must be a string which complies to Symbol restrictions.
      • Duplicate property names are not allowed. Note that property names (symbols) are case-insensitive, so prop and PROP would be considered duplicates.
    • Example: Object("a", 1, "b", 2) produces an object with properties a = 1 and b = 2.

Map(...) → map

  • Map() returns an empty map.
  • Map(value) applies standard conversion to map.
  • Map(key1, value1, key2, value2, ...) constructs from key-value pairs.
    • Pair constructor constraints:
      • Argument count must be even.
      • The first argument of each pair is a key, which must be a string.
      • Duplicate keys are not allowed.
    • Example: Map("a", 1, "b", 2) produces a map with entries "a" → 1 and "b" → 2.

Array([value]) → array

  • Array() returns an empty array.
  • Array(value) applies standard conversion to array.
  • Array does not provide a variadic constructor. Use [ ... ] syntax instead to produce array values.

IndexedArray(...) → indexed array

  • IndexedArray() returns an empty indexed array.
  • IndexedArray(value) applies standard conversion to indexed array.
  • IndexedArray(index1, value1, index2, value2, ...) constructs from index-value pairs.
    • Pair constructor constraints:
      • Argument count must be even.
      • The first argument of each pair is an index, which must be a int.
      • Indices must be non-negative.
      • Duplicate indices are not allowed.
    • Example: IndexedArray(1, "a", 5, "b") produces an indexed array with entries 1 → "a" and 5 → "b".

String functions

Case-insensitive functions perform a 1:1 Unicode simple case-fold:

  • Correct behavior is mandatory for code points U+0000–U+00FE (Basic Latin through Latin-1 Supplement). Case-insensitivity for code points above U+00FE should be implemented correctly if possible, but minor implementation-specific differences are acceptable.
  • No multi-character expansions: e.g., ß (U+00DF) folds only to itself, not to SS.
  • No Unicode normalization: precomposed and decomposed forms are not equated.

EqualsNoCase(a: string, b: string) → bool

Returns true iff a and b are equal case-insensitively.

ContainsNoCase(haystack: string, needle: string) → bool

Returns true iff needle is a contiguous substring of haystack, comparing case-insensitively.

StartsWith(str: string, prefix: string) → bool

Returns true iff str starts with prefix (case-sensitive).

StartsWithNoCase(str: string, prefix: string) → bool

Returns true iff str starts with prefix case-insensitively.

EndsWith(str: string, suffix: string) → bool

Returns true iff str ends with suffix (case-sensitive).

EndsWithNoCase(str: string, suffix: string) → bool

Returns true iff str ends with suffix case-insensitively.

Trim(str: string) → string

Removes the following leading and trailing ASCII whitespace: U+0020 SPACE, U+0009 TAB, U+000A LF, U+000D CR, U+000B VT, U+000C FF. Non-ASCII whitespace (e.g., U+00A0 NO-BREAK SPACE) is not removed.

Substring(str: string, start: int [, maxLength: int]) → string

Returns a substring of str starting at index start with length at most maxLength.

  • Indices are zero-based.
  • Indices are counted in Unicode code points for code points in the Unicode BMP (U+0000 to U+FFFF). For code points outside the BMP (e.g., emoji), the behavior is implementation-defined: either count as 1 or 2 code points.
  • start must be ≥ 0; otherwise null.
  • maxLength must be ≥ 0 if provided; otherwise null.
  • If start is greater than or equal to the length of the string, returns an empty string.
  • If maxLength is omitted or exceeds the remaining length, returns the rest of the string.

Miscellaneous functions

Size(value) → int

Returns the size of an AOP value. The function is defined for specific types as described in the table below.

Type of value Returns Example
string Number of characters1 Size("hello") → 5
data Number of bytes Size(Data("3q2+7w==")) → 4
object Number of properties Size(Object("a", 1, "b", 2)) → 2
map Number of entries Size(Map("a", 1, "b", 2)) → 2
array Number of elements Size(Array(1, 2, 3)) → 3
indexed array Number of entries Size(IndexedArray(1, "a", 5, "b")) → 2

1 For string inputs, Size returns the number of Unicode code points for code points in the Unicode BMP (U+0000 to U+FFFF). For code points outside the BMP (e.g., emoji), the behavior is implementation-defined. They may either count as 1 or 2 code points.

Get(collection, key [, default]) → value

Looks up a single element from a collection. If the element does not exist, returns default if provided; otherwise null.

Type of collection Type of key Lookup
array int Key is zero-based index; negative index → null
object string Property name (case-insensitive); non-property-name → null
map string Exact key (case-sensitive)
indexed array int Key is index; negative index → null

Custom functions

Custom functions allow users to extend the evaluator with additional functionality beyond the built-in functions. Refer to the documentation of the specific API or implementation for details on which additional custom functions are available and how to use them.

Custom functions should generally follow these guidelines:

  • In case of an error during evaluation (e.g., invalid argument type, invalid argument value, or any other error), the function should return null.
  • If the function is called with an unsupported number of arguments (which includes too many arguments), the function should return null.

A custom function should have a unique name which must not conflict with any built-in function name. Shadowing a built-in function is not allowed; the evaluator will generally treat this as an error. It may be advisable to prefix custom function names with a unique namespace to avoid conflicts.

Legacy expressions

This document defines the syntax and semantics of the revised AOP Expression Language. The original implementation is currently still in use in WinGuard and Advancis.SST v1.x (for Config XML evaluation). Note that the existing implementations of the original expression language already had subtle differences (for example, + and in with string operands was not universally implemented), which were unified in the revised language. In practice, the differences between the original and revised expression languages are minor and only affect rare edge cases which most users are unlikely to encounter.

The following is a non-exhaustive list of differences between the original and revised expression languages:

  • Syntax: The alternative JSON syntax is no longer supported.
  • Whitespace: Degenerate forms such as 1in[1] or not-1 or []is empty are no longer accepted; textual binary operators require surrounding whitespace, textual unary prefix operators require trailing whitespace, and postfix operators require leading whitespace. No changes were made to symbolic operators, which can be used without whitespace (e.g., 1+1 is still valid).
  • String literals: Rarely used escape sequences \v, \b, \f, \XXX (octal), \xXX (2 hex digits), \uXXXX (4 hex digits) were removed.
  • Float literals: The float literal 1. (no decimal digits after the dot) is no longer accepted; a digit is always required after the dot (e.g., 1.0).
  • Array expression: The original language supported array literals with arbitrary sub-expression, but statically evaluated them with an empty environment, which meant that variables and custom functions were not accessible within array literals. To fix this inconsistency, a dedicated AST node for array expressions was introduced in the revised language, which is evaluated dynamically.
  • Operator aliases: The operator aliases lt, lte, gt, gte, bitand, bitor, bitxor were added in the revised language.
  • Operator precedence: The binding strength of bitwise operators was changed to be between additive and membership operators, instead of below all other operators. This allows expressions like a & b == 0 to be parsed intuitively without extra parentheses.
  • Comparison semantics: Some comparison semantics were changed to be more intuitive. In general, comparison semantics were aligned with standard conversion rules and comparison of values for consistency. This includes the following notable changes:
    • null vs not-null is always Unordered, instead of null < not-null.
    • bool vs bool is Unordered if the values differ, instead of false < true.
    • bool vs int/float/string: Values are now promoted to bool to do a bool comparison, instead of promoting the bool to a number to do a numeric comparison. This means expressions like true < 2, true < '2' and false < '0.5' are no longer true.
    • bool vs other type: These comparisons are now Unordered, rather than comparing against the empty-ness of the value. To test for empty-ness, use the is empty operator instead.
    • guid/time vs string: If the conversion of string to guid/time fails, the comparison is now always Unordered instead of comparing against an empty value.
    • int/float vs string: The allowed convertible formats for string to int/float conversion were aligned such that exactly those formats are allowed that are also legal number literals in the expression language.
    • array vs array: Arrays are now compared fully lexicographically, instead of comparing their sizes first.
  • Operators semantics:
    • + and in with string operands are now consistently defined.
    • ! now applies the same truthiness rule as is empty instead of only applying to bool/int/float values, with the exception of !null which is now null instead of true.
  • Built-in functions: The original language did not have a consistent set of built-in functions.
Last modified September 25, 2026