In Structured Text (ST) and Extended Structured Text (ExST), expressions are evaluated by applying operators according to specific binding rules. This blog post gives a brief overview of these rules in TwinCAT 3. We look at the basics of expression evaluation, exploring how operator precedence and associativity impact the order of operations. Whether you’re optimizing control logic or working on complex tasks, understanding TwinCAT’s approach to expression evaluation is crucial for error-free automation solutions.
Binding Strength and Associativity
TwinCAT 3 first processes the operator with the highest binding strength. When operators share the same binding strength, TwinCAT evaluates them from left to right, respecting their defined associativity.
Binding Strength | Associativity | Operation | Symbol | Examples | Notes |
Strongest Binding: 0 | left to right | AT-Declaration | <identifier> AT <Address> |
foo AT %I* :REAL , bar AT %M0.5 :BOOL |
1 |
1 | left to right | Grouping-Operator | (<Operand>) |
(foo OR bar) , (foo) |
2 |
2 | left to right | Call | <Expression>([<ParameterList>]) |
foo(foo) ,THIS^() ,bar[0](a := 1, foo, a => baz) |
3 |
2 | left to right | Member Access Dot | . |
foo.bar ,THIS^.bar , baz[0].bar |
4 |
2 | left to right |
Member Access Squared Braces |
[] |
foo[0] , bar[foo+1][baz, 7] |
5 |
2 | left to right | Pointer Dereferencing | ^ |
THIS^ , foo^ |
6 |
2 | left to right |
Bit Access |
. |
foo.3 , bar.BAZ , foo[0].15 |
7 |
3 | left to right | Unary Plus | + |
+WORD#16#AFFE , +foo , 1.5E+3 |
8 |
3 | left to right |
Unary Minus |
- |
-3 , -foo , 1.5E-3 |
9 |
3 | left to right |
Unary Bitwise Negation |
NOT |
NOT foo , NOT 16#AFFE |
9 |
3 | left to right |
Unary Logical Negation |
NOT |
NOT foo , NOT TRUE |
10 |
4 | left to right |
Multiplications |
* |
foo * bar , DWORD#16#AFFE * DWORD#3 |
11 |
4 | left to right |
Division |
/ |
foo / bar , 3 / 3 |
11 |
4 | left to right |
Modulo |
MOD |
foo MOD 2 , 3 MOD 3 |
11 |
5 | left to right |
Addition |
+ |
foo + bar , 3 + 7 |
12 |
5 | left to right |
Subtraction |
- |
foo - bar , 7 - 3 |
12 |
6 | left to right |
Less-Than |
< |
foo < bar , 1.5E-3 < 1.5 |
|
6 | left to right |
Greater-Than |
> |
foo > bar , 1.5E+3 > 1.5 |
|
6 | left to right |
Less-Than or Equal-To |
<= |
foo <= bar , 1.5E-3 <= 1.5 |
|
6 | left to right |
Greater-Than or Equal-To |
>= |
foo >= bar , 1.5E3 >= 1.5 |
|
7 | left to right |
Equal-To |
= |
foo = bar , 2 = 2 |
|
7 | left to right |
Not-Equal-To |
<> |
foo <> bar , 2 <> 3 |
|
8 | left to right |
Bitwise And |
AND |
foo AND bar , 16#00FF AND 16#AFFE |
13 |
8 | left to right |
Logical And fully evaluated |
AND |
foo AND bar , TRUE AND FALSE |
13 |
8 | left to right |
Logical And minimal evaluated |
AND_THEN |
foo AND_THEN bar |
13 |
9 | left to right |
Bitwise Exclusive Or |
XOR |
foo XOR bar , 16#5001 XOR 16#FFFF |
14 |
9 | left to right |
Logical Exclusive Or |
XOR |
foo XOR bar , TRUE XOR TRUE |
14 |
9 | left to right |
Bitwise Or |
OR |
foo OR bar , 16#A0F0 OR 16#0F0E |
15 |
9 | left to right |
Logical Or fully evaluated |
OR |
foo OR bar , TRUE OR FALSE |
15 |
9 | left to right |
Logical Or minimal evaluated |
OR_ELSE |
foo OR_ELSE bar , TRUE OR_ELSE FALSE |
15 |
10 | right to left |
Assignment |
:= |
foo := 42 , foo := bar := 23 , baz := foo |
16 |
10 | right to left |
Conditional Set Assignment |
S= |
foo S= bar , foo S= bar S= fooBar OR barFoo |
17 |
10 | right to left |
Conditional Unset Assignment |
R= |
foo R= bar , foo R= bar R= baz |
17 |
10 | right to left |
Reference Assignment |
REF= |
foo REF= bar |
18 |
10 | left to right |
Output Assignment |
=> |
foo(bar => baz) |
|
999 | left to right |
Range |
.. |
foo..bar: , :ARRAY[0..BAZ] OF |
|
999 | left to right |
Colon |
: |
foo :USINT , JMP bar: , 7: |
|
999 | left to right |
Range Wildcard |
* |
foo :ARRAY[*] OF ARRAY[*,*] OF bar |
|
Weakest Binding: 999 | left to right |
Comma |
, |
foo, bar, baz :BOOL; , foo(1,2,3) , 1,2,3,5,8: |
Notes: 1 (AT-Declaration)
An address is specified by indicating its memory position and size using special strings. An address starts with a percent sign (%), followed by the memory area prefix, an optional size prefix, and the memory position.
- Address Syntax:
%<Memory Area Prefix>[<Size Prefix>]<Memory Position>
- Memory Area Prefixes:
I
for Input,Q
for Output,M
for Flag Memory - Size Prefixes:
X
for Bit,B
for Byte (8 Bits),W
for Word (16 Bits),D
for DWORD (32 Bits),L
for QWORD (64 Bits) - Memory Position:
*
for a placeholder, or a byte number, optionally followed by a dot and the bit number - Address Pattern:
%[IQM]X[\\d]+(\\.[0-7])|%[IQM][BWDL][\\d]+|%[IQ][\\d]+(\\.[0-7])?|%[IQ]\\*
Notes: 2 (Grouping-Operator)
The operand can be any expression.
Notes: 3 (Call)
The expression must be an identifier of a Function, Function Block, Program, Action, or Method, or it must be an array access or a pointer dereference of a Function Block.
- The parameter list is optional.
- The parameter list is evaluated left to right, regardless of its declaration.
- Items in the parameter list are separated by commas.
For Input Parameters (VAR_INPUT
):
- The parameter identifier is optional. If not provided, the parameter must appear in the same position as in the declaration.
- Any expression with a stronger binding than
,
that matches the parameter type is allowed as an input parameter. - If the parameter identifier is used, the assignment operator
:=
is required, regardless of whether it’s aREFERENCE TO
. Note thatS=
andR=
won’t work for booleans. The identifier can be used in expressions, for example:THIS^.doFoo(foo := bar S= baz);
. - Input assignments are evaluated from right to left.
- Input parameters are always passed by value.
- Input parameters can be optionally declared.
For Output Values (VAR_OUTPUT
):
- The parameter identifier is required.
- The assignment operator
=>
is required. - Chaining this operator is not allowed, but multiple assignments are possible with repeated calls, e.g.,
THIS^.doFoo(foo => bar, foo => baz)
. - Output assignments are evaluated left to right.
- Declared output values are always optional in the call.
- Output value assignments occur after the call.
For Call-by-Reference Parameters (VAR_IN_OUT
):
- The parameter identifier is optional. If not provided, the parameter must appear in the same position as in the declaration.
- Only identifiers of variables with the same type and read/write access are allowed, but not
PROPERTY
. - If the parameter identifier is used, the assignment operator
:=
is required. - Call-by-reference parameters are not optional.
- The assignment is evaluated from right to left.
Notes: 4 (Member Access Dot)
The left-hand side must be an identifier, an array access, a pointer dereference, or a call.
- The right-hand side must be an identifier.
Notes: 5 (Member Access Squared Braces)
The left-hand side must be an identifier, an array access, a pointer dereference, or a call.
- The right-hand side can be any expression.
- Inside square brackets, any expression with a stronger binding than
,
that results in an integer value is allowed.
Notes: 6 (Pointer Dereferencing)
The left-hand side must be an identifier, an array access, a pointer dereference, or a call.
- The right-hand side can be any expression.
Notes: 7 (Bit Access)
The left-hand side must be an identifier, an array access, or a pointer dereference.
- The right-hand side must be an integer number between 0 and 63 or an identifier of a constant.
Notes: 8 (Unary Plus)
The right-hand side must be an identifier, a number literal.
- For float (
REAL
) and double (LREAL
) literals, it is possible to include an exponent in the literal, such as:1.23E+2
.
Notes: 9 (Unary Minus, Unary Bitwise Negation)
The right-hand side must be an identifier, a number literal.
- For float (
REAL
) and double (LREAL
) literals, it is possible to include an exponent in the literal, such as:1.23E-2
.
Notes: 10 (Unary Logical Negation)
It is not possible to combine logical and bitwise NOT
e.g. IF ((NOT BOOL#0) OR (NOT 1)) THEN
will not work.
Notes: 11 (Multiplications, Division, Modulo)
Works only with:
ANY_BIT
(BYTE
,WORD
,DWORD
,LWORD
,__XWORD
)ANY_NUM
(REAL
,LREAL
,SINT
,USINT
,INT
,UINT
,DINT
,UDINT
,LINT
,ULINT
,__XINT
,__UXINT
)
Notes: 12 (Addition, Subtraction)
Works only with:
ANY_BIT
(BYTE
,WORD
,DWORD
,LWORD
,__XWORD
)ANY_NUM
(REAL
,LREAL
,SINT
,USINT
,INT
,UINT
,DINT
,UDINT
,LINT
,ULINT
,__XINT
,__UXINT
)TIME
andLTIME
Notes: 13 (Bitwise And, Logical And fully evaluated, Logical And minimal evaluated)
It is not possible to combine logical and bitwise AND
e.g. IF (3 AND BOOL#1 AND_THEN TRUE) THEN
will not work.
AND_THEN
works only with boolean expressions.
Notes: 14 (Bitwise Exclusive Or, Logical Exclusive Or)
It is not possible to combine logical and bitwise AND
e.g. IF (2 XOR BOOL#0) THEN
will not work.
15. Operations: Bitwise Or / Logical Or fully evaluated / Logical Or minimal evaluated
It is not possible to combine logical and bitwise AND
e.g. IF (2 OR BOOL#1 OR_ELSE TRUE) THEN
will not work.
OR_ELSE
works only with boolean expressions.
16. Operation: Assignment
The left-hand side must be a valid target with write access for the result of the right-hand side expression.
- It returns the value of the right-hand side expression, so it is possible to do multi-assignments.
- To combine it with other expressions it must be grouped, e.g.
foo := bar + (bar := 3)
. Otherwise, it is no valid target because of the precedence.
17. Operations: Conditional Set Assignment / Conditional Unset Assignment
The left-hand side must be a valid boolean target with write access.
- The right-hand side must be a boolean expression.
- It returns the value of the right-hand side expression, so it is possible to do multi-assignments.
- To combine it with other expressions it must be grouped, e.g.
IF (foo XOR (bar S= baz) AND (baz R= foo)) THEN
. Otherwise, it is no valid target because of the precedence.
18. Operation: Reference Assignment
The left-hand side must be a valid REFERENCE TO
target with write access.
- The right-hand side must be a valid variable/object (same type and no empty
REFERENCE
) with write access, or it must be0
to clear it. - It returns the address of the right-hand side variable/object.
- Multi-assignments are not possible.
- To combine it with other expressions it must be grouped,
e.g. bar := baz + (foo REF= bar);
. Otherwise, it is no valid target because of the precedence.
Resolving an Example
First, we create a Function
just to get a bit of complexity in our expression.
FUNCTION Baz :BYTE
VAR_OUTPUT
fooBaz :BYTE;
END_VAR
Baz := 0;
fooBaz := 3;
Now we create an expression with Baz
.
PROGRAM Main
VAR
bar :BYTE;
foo :BYTE;
END_VAR
foo := 2 OR bar XOR Baz(fooBaz => bar);
Let’s analyze the expression step by step, following the operator precedence and the provided variable values.
Initial Values
bar := 16;
fooBaz := 3;
Baz := 0;
Expression
foo := 2 OR bar XOR Baz(fooBaz => bar);
Let’s break this down from left to right using the operator precedence.
1 The Assignment (:=
) foo := 2
- The assignment (
:=
) has a weaker binding than theOR
on the right side. - It cannot be evaluated yet.
2 Bitwise OR (2 OR bar
)
- The
OR
has the same binding strength asXOR
. - It can be evaluated now:
2 OR bar
→2 OR 16
→18
3 Bitwise XOR (18 XOR Baz(fooBaz => bar)
)
- The
XOR
has a weaker binding than the function callBaz(fooBaz => bar)
. - It cannot be evaluated yet.
4 Function Call (Baz(fooBaz => bar)
)
- The assignment (
=>
) can be evaluated whenBaz
is called. - There are no other expressions inside the parameter parentheses of
Baz
. - It can be evaluated now:
Baz
→0
fooBaz => bar
→3 => bar
→bar := 3
5 End of Expression (;
)
- The expression is now
foo := 18 XOR 0;
. - We need to start again from the left side.
6 Assignment (:=
) foo := 18
- The assignment (
:=
) has a weaker binding than theXOR
on the right side. - It cannot be evaluated yet.
7 Bitwise XOR (18 XOR 0
)
- The
XOR
has no further expressions to evaluate. - It can be evaluated now:
18 XOR 0
→18
8 Assignment (:=
) foo := 18
- The assignment has no further expressions to evaluate.
- It can be evaluated now.
Final Result
- The value of
foo
after the expression is evaluated is 18. - The value of
bar
after the expression is evaluated is 3.
Treeified Expression
AST of the Expression
Full Evaluation
In TwinCAT, all expressions will be evaluated, but it is often unnecessary to evaluate every part of an expression. For most boolean expressions, it makes no sense to waste processing time. For example, in foo := bar OR baz OR fooBar OR fooBaz;
, there is no reason to continue evaluating once bar
is TRUE
. Similarly, in foo := bar AND baz AND fooBar AND fooBaz;
, if bar
is FALSE
, further evaluation is pointless but TwinCAT does it anyway.
Because of this, TwinCAT provides the operators AND_THEN
and OR_ELSE
. These operators work like boolean operators but are designed for minimal evaluation. Not equal 0
being treated as TRUE
. This is important to know for casting with pointers because for the other boolean operator (AND
, OR
, XOR
) just the value of the least significant bit is relevant. AND
, OR
and XOR
are both boolean and bit operators, AND_THEN
and OR_ELSE
work strictly with BOOL
and BIT
. This is not primarily a topic of precedence but rather of evaluation. Below are some examples that show why minimal evaluation is helpful and how it works together with full evaluation.
Minimal Evaluation Examples
(*
This is a sequence where each step is only called if the previous step returns TRUE.
However, foo.reportProgress() will always be called.
*)
success := (
foo.connectToServer()
AND_THEN foo.downloadFile()
AND_THEN foo.parseNewFile()
AND_THEN foo.distributeData()
AND foo.reportProgress()
);
(* It calls the strategies until the first one returns TRUE, but bar.execute is always called. *)
dummy := (bar.doFooberry() OR_ELSE bar.doBarcolate() OR_ELSE bar.doDefault() OR bar.execute());
(* It executes the command only if the interface has a valid address. *)
result := (THIS^.isValidCmd(cmd) AND_THEN cmd.execute());
Keep in Mind
Evaluation is from left to right
Expressions are evaluated in the order they appear, starting from the leftmost operation. The only exceptions are assignments (:=
, S=
, R=
, REF=
), which are evaluated from right to left. However, TwinCAT behaves inconsistently. While foo S= bar S= baz;
or foo := bar := baz;
is evaluated from right to left, foo REF= bar REF= baz;
behaves differently. In this case, foo REF= bar
is evaluated first, and since bar
is still an invalid reference at that point, it results in an error. Therefore, it is necessary to write:
bar REF= baz;
foo REF= bar;
Alternatively:
bar REF= baz;
foo REF= baz;
Although REF=
follows a right-to-left evaluation for assignments, the individual parts of the expression are evaluated from left to right. You might assume that REF=
has no return value, but an expression like foo := foo + (bar REF= foo);
demonstrates that REF=
indeed returns a value: the address of the right or assigned variable (here the return value of REF=
is equal to ADR(foo)
).
Immediate Evaluation
If a part of the expression can be evaluated (based on operator precedence), it will be evaluated immediately, regardless of any operations or expressions that appear on the right-hand side.