Skip to content

Expressions

IS4 edited this page Aug 6, 2022 · 17 revisions

Expressions in PawnPlus enable runtime manipulation with dynamically typed objects. An expression tree can be constructed from individual operations and executed to obtain the value of the expression.

Introduction

An expression is a recursive tree-like structure consisting of various operations and operands. Similarly to iterators, it is an abstract data type and its behaviour depends on the manner it was created in. Expressions are immutable (with one exception), so they can't be modified after their creation. They are also linked by reference with each other, so constructing an expression will not duplicate its arguments.

There are many types of differently behaving expressions. Please see here for detailed listing.

An expression can be created from an initial value or another expression. There are nullary expressions (not depending on any operand), unary expressions (producing a value based on a single operand), binary expressions (two operands), ternary expressions (three operands), and variadic expressions. PawnPlus also comes with a powerful expression parser supporting all types of expressions and special syntax.

new Expression:arg1 = expr_const(10); // creates a new expression with a constant value of 10
new Expression:arg2 = expr_const(5); // creates a new expression with a constant value of 5
new Expression:add = expr_add(arg1, arg2); // creates a new expression that sums the two expressions
print_s(str_val(expr_get_var(add))); // computes the result of the expression and prints it

Since an expression tree cannot be modified once it is created, combining expressions will not duplicate their instances. Neither expr_delete nor garbage collection can delete an expression when it is used by other expressions.

new Expression:arg = expr_const(10);
new Expression:expr = expr_neg(arg); // negation expression
expr_delete(arg); // the expression cannot be truly deleted if it is used
assert !expr_valid(arg); // but the expression reference is not valid anymore
assert expr_get(expr) == -10;

Parameters

Expressions can be used to represent abstract or complex operations via a single object, in which case a parameter must be provided to an expression. For example, there can be a generic function that operates on list elements and the operation is provided externally via an expression:

stock DoSomething(List:l, Expression:e)
{
    for_list(it : l)
    {
        new Variant:v = iter_get_var(it);
        v = expr_get_var(expr_bind(e, expr_const_var(v)));
        iter_set_var(it, v);
    }
}

Calling DoSomething with expr_neg(expr_arg(0)) will negate all values in a list (no matter their types). expr_arg(0) represents an external argument given to the expression by code that executes it. Here, expr_bind is used to bind an expression to a constant value (obtained from the iterator). Without binding, the value of expr_arg(0) would be unresolved. The result of the expression is stored back in the list.

expr_bind only fills the initial sublist of arguments used by the expression with the given arguments. The rest is shifted so it can be filled by subsequent calls to expr_bind. For example, expr_bind(expr_add(expr_arg(0), expr_arg(1)), expr_const(10)) binds the value of expr_arg(0) to 10, but the second argument is left unbound. Next call to expr_bind starts at the second argument.

Execution

When an expression is executed, it performs its operation using the operands that were provided to the expression (if any). The operands receive a message corresponding to the type of the executed expression. For example, the call expression, when executed, sends the "evaluate" message to each of the arguments, and finally the "call" message to the function. The function expression then performs the actual call.

This behaviour indicates that expressions do not necessarily behave in a consistent way in all contexts. For example, the comma expression can return values from both operands at the same time, the last value, or no value at all, depending on the usage. When used as a function argument, the comma expression returns all values. When used as a single operand of an operator (+ etc.) only the last value is used. When used in expr_void, no values will be produced.

Parser

For easier debugging and representing more complex expressions in a more readable syntax, expr_parse can be used to convert a string to an expression tree it represents. The syntax is similar to Pawn, but there are some differences:

  • There are no statements; only expressions are supported.
  • Newline is not a special character.
  • Comments are not allowed.
  • , and ; are semantically equivalent, but ; doesn't have to be followed by an expression.
  • rankof, addressof, and nameof are introduced to assist with manipulating dynamic values and passing them to functions.
  • tagof, sizeof, and rankof may evaluate their argument if the result cannot be determined otherwise.
  • Strings and characters are tagged with char:.
  • Expressions may return multiple values in some contexts. Empty expression (()) is syntactically valid and results in expr_empty.
  • Function calls, array construction, and indexing accept a multi-valued expression. Commas or parentheses do not affect the number of values.
  • [a,b] is the same as [a][b].
  • $argn can be used to represent expr_arg(n).
  • $argsa_b can be used to represent expr_arg_pack(a, b).
  • $env represents expr_env().
  • try[main]catch[fallback] corresponds to expr_try(main, fallback).
  • (list)select[func] corresponds to expr_select(list, func).
  • (list)where[func] corresponds to expr_where(list, func).
  • void(expr) corresponds to expr_void(expr).
  • [expr](args) corresponds to expr_bind(expr, args).
  • <expr> corresponds to expr_quote(expr), unary ^ results in expr_dequote.
  • Operations don't follow standard Pawn rules; instead, variant operations semantics are in effect.
  • Single cells can be used for reference arguments, but they must be wrapped in addressof.
  • Unary operators *, &, @ corresponding to expr_extract, expr_variant, and expr_string.
  • Unary operator + corresponds to expr_nested.
  • Binary operator .. corresponds to expr_range.
  • There is no >>> operator, and the bit operators only work on objects with a weak tag.
  • All combinations of syntax allowed by the grammar are syntactically valid; only at execution it is decided whether they are allowed.
  • Operations may be performed on expressions instead of on values when executed in some cases, i.e. calling a conditional (?:) expression calls the expression that is chosen when the condition is evaluated.
  • Calling a single Expression: value calls the expression with the provided arguments.
  • Indexing a single Expression: value binds it to the arguments.

If a symbol (according to the Pawn rules) is encountered, it is first matched against the known keywords if it represents a special operation. If the symbol is not a keyword (or doesn't produce valid syntax that way), the internal table of literal values and intrinsic functions is searched for the symbol.

Name Type Meaning Example
null value The null variant.
true value true
false value false
cellmin value -2147483648
cellmax value 2147483647
cellbits value 32
concat function Produces a string by concatenating all arguments. concat("a", "b", "c")
eval function Parses the expression specified by the first argument and executes it on the remaining arguments. eval("$arg0 + $arg1", 5, 6)
unpack function Returns all elements of an array sequentially. unpack({1, 2, 3})
cast function Changes tags of all arguments to the tag specified by the first argument. cast(tagof(Float:), 1, 2, 3)

When an unknown symbol is encountered, the current scope where the call to expr_parse happens is searched first, similarly to debug_symbol. If no symbol is found, the public function and public variable tables are searched for the name, and lastly the registered native functions in the AMX instance. If the symbol is still not found, the expression resolves to expr_global indexing the expression environment.

The parser can be configured to disallow specific pieces of syntax. These are the corresponding options:

Option Meaning Example
parser_bare No other pieces of syntax are allowed.
parser_allow_unknown_symbols Accessing unresolved (i.e. global) symbols.
parser_allow_debug_symbols Accessing debug symbols.
parser_allow_natives Accessing native functions.
parser_allow_publics Accessing public functions.
parser_allow_pubvars Accessing public variables.
parser_allow_constructions Creating new instances. &1, @"str", <1>
parser_allow_ranges Creating ranges. 1..1000
parser_allow_arrays Creating arrays. {1, 2, 3}
parser_allow_queries Performing queries. (1, 2, 3) select [$arg0, $arg0]
parser_allow_arguments Accessing external arguments. $arg0
parser_allow_environment Accessing the global environment. $env["index"]
parser_allow_casts Performing tag casts. Float:0
parser_allow_literals Accessing literal expressions. null
parser_allow_intrinsics Accessing intrinsic functions. concat("a", "b")
parser_allow_strings Producing strings or characters. "str"
parser_all The whole syntax is allowed.

The parser flags are also passed to invoked intrinsic functions, i.e. calling eval on a string will parse it with the same set of flags as specified for the parser of the original string expression.

Lifetime and garbage collection

Expressions are garbage-collected like strings, variants, and iterators, since most of the references are used as temporary arguments to other expression constructors. In case an expression should remain existing for a longer time, expr_acquire and expr_release should be used. See this for more information about garbage collection.

Expression objects that are kept alive by other expressions will be removed from the pool of active expressions when they are "deleted", but the actual objects will survive until they are no longer used by other expressions. An expression in this state is no longer "valid" and cannot be revived.

Clone this wiki locally