Understanding Expression Evaluation in TwinCAT

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 As­so­cia­ti­vi­ty Ope­ra­ti­on Sym­bol Ex­am­ples Notes
Strongest Bind­ing: 0 left to right AT-De­cla­ra­tion <identifier> AT <Address> foo AT %I* :REAL, bar AT %M0.5 :BOOL 1
1 left to right Grou­ping-Op­er­a­tor (<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 Mem­ber Ac­cess Dot . foo.bar,
THIS^.bar, baz[0].bar
4
2 left to right

Mem­ber Ac­cess Squared Braces

[] foo[0], bar[foo+1][baz, 7] 5
2 left to right Pointer De­ref­er­enc­ing ^ THIS^, foo^ 6
2 left to right

Bit Ac­cess

 . 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 Mi­nus

- -3, -foo, 1.5E-3 9
3 left to right

Unary Bit­wise Nega­tion

NOT NOT foo, NOT 16#AFFE 9
3 left to right

Unary Log­i­cal Nega­tion

NOT NOT foo, NOT TRUE 10
4 left to right

Mul­ti­pli­ca­­tions

* foo * bar, DWORD#16#AFFE * DWORD#3 11
4 left to right

Di­vi­sion

/ foo / bar, 3 / 3 11
4 left to right

Mo­du­lo

MOD foo MOD 2, 3 MOD 3 11
5 left to right

Ad­di­tion

+ foo + bar, 3 + 7 12
5 left to right

Sub­trac­tion

- 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

Bit­wise And

AND foo AND bar, 16#00FF AND 16#AFFE 13
8 left to right

Lo­gi­cal And fully eval­u­ated

AND foo AND bar, TRUE AND FALSE 13
8 left to right

Lo­gi­cal And min­i­mal eval­u­ated

AND_THEN foo AND_THEN bar 13
9 left to right

Bit­wise Ex­clu­sive Or

XOR foo XOR bar, 16#5001 XOR 16#FFFF 14
9 left to right

Lo­gi­cal Ex­clu­sive Or

XOR foo XOR bar, TRUE XOR TRUE 14
9 left to right

Bit­wise Or

OR foo OR bar, 16#A0F0 OR 16#0F0E 15
9 left to right

Lo­gi­cal Or fully eval­u­ated

OR foo OR bar, TRUE OR FALSE 15
9 left to right

Lo­gi­cal Or min­i­mal eval­u­ated

OR_ELSE foo OR_ELSE bar, TRUE OR_ELSE FALSE 15
10 right to left

As­sign­ment

:= foo := 42, foo := bar := 23, baz := foo 16
10 right to left

Con­di­tional Set As­sign­ment

S= foo S= bar, foo S= bar S= fooBar OR barFoo 17
10 right to left

Con­di­tional Un­set As­sign­ment

R= foo R= bar, foo R= bar R= baz 17
10 right to left

Ref­er­ence As­sign­ment

REF= foo REF= bar 18
10 left to right

Out­put As­sign­ment

=> 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 Wild­card

* 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-De­cla­ra­tion)

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 a REFERENCE TO. Note that S= and R= 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 and LTIME

 

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 be 0 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 the OR on the right side.
  • It cannot be evaluated yet.

2 Bitwise OR (2 OR bar)

  • The OR has the same binding strength as XOR.
  • It can be evaluated now:
    2 OR bar2 OR 1618

3 Bitwise XOR (18 XOR Baz(fooBaz => bar))

  • The XOR has a weaker binding than the function call Baz(fooBaz => bar).
  • It cannot be evaluated yet.

4 Function Call (Baz(fooBaz => bar))

  • The assignment (=>) can be evaluated when Baz is called.
  • There are no other expressions inside the parameter parentheses of Baz.
  • It can be evaluated now:
    Baz0
    fooBaz => bar3 => barbar := 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 the XOR 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 018

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

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.