Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Arrays, subranges and enumerations

An index outside its array faults

Every index that is not a constant is checked where it is used. One outside the array’s bounds is a fault INDEX: carrying on would read or write memory that is not the array’s. A constant index outside the bounds is refused when the program is built.

The C reads element 0 on its way out, so no memory outside the array is ever touched, even for the one statement that faults. Rockwell’s Logix also makes an out-of-range subscript a major fault; CODESYS and TwinCAT leave it unchecked unless the project adds CheckBounds, whose template clamps the index instead.

Bounds are constants

An array’s bounds, a STRING’s length and a subrange’s limits are fixed when the program is built (Ed.3 6.4.4.5.1): literals, + - * / MOD, and the names of constants (Ed.3 6.6.1.2.4), E0032 for anything else. A constant is a POU’s own VAR CONSTANT, or a VAR_GLOBAL CONSTANT through its VAR_EXTERNAL CONSTANT; a TYPE sees the global constants directly, as a CODESYS GVL’s VAR_GLOBAL CONSTANT are (CODESYS refuses a plain VAR_GLOBAL there, C0161):

TYPE ROW : ARRAY [0..W] OF SINT; END_TYPE     (* W a VAR_GLOBAL CONSTANT *)
PROGRAM main
    VAR CONSTANT N : INT := 5; END_VAR
    VAR
        a : ARRAY [1..N] OF INT;
        s : STRING[N];
        lim : INT (0..N) := N;                (* an initial value too *)
    END_VAR
END_PROGRAM

The bounds are within DINT, as CODESYS has them, since LOWER_BOUND and UPPER_BOUND give a DINT; the lower one is not above the upper one.

Initial values are constant expressions

An initial value is a literal or a constant expression (Ed.3 6.4.4.1, Figure 6; 6.6.1.2.4): literals, the names of constants, enumerated and named values, the operators, and the standard functions on them. The compiler works it out, so nothing of it is left to the run - no flag and no fault as the program starts:

VAR CONSTANT
    N  : INT := 3;
    N2 : INT := N + 1;                    (* 4: a constant of constants *)
    KR : REAL := 1.5;
    KT : TIME := T#2s;
    KM : MODE := run;
END_VAR
VAR
    a   : INT := N * 2 + 1;               (* 7 *)
    r   : REAL := KR * 2.0;               (* 3.0 *)
    t   : TIME := KT + T#500ms;           (* T#2s500ms *)
    t2  : TIME := KT * 1.5;               (* T#3s *)
    b   : BOOL := NOT (N > 2);            (* FALSE *)
    w   : BYTE := 16#0F AND 16#3C;        (* 16#0C *)
    w2  : WORD := SHL(16#0001, N);        (* 16#0008 *)
    m   : MODE := KM;                     (* run *)
    x   : INT := MAX(N, ABS(-5));         (* 5 *)
    i   : INT := REAL_TO_INT(KR);         (* 2: to the even one *)
    q   : REAL := SQRT(2.0) * KR;         (* the target's SQRT *)
    arr : ARRAY [1..N] OF INT := [N, 2(N2)];
    p   : PT := (x := N2, y := KR);
END_VAR
  • The constants are those a bound names (above). A method also names its type’s constants, a derived type its bases’; a TYPE’s initial value and a VAR_GLOBAL’s name the VAR_GLOBAL CONSTANTs directly. A constant may be declared below where it is named.
  • A string, an array or a structure given a constant’s name starts as that constant’s own initial value.
  • A part of a constant array or structure is a constant: KA[2], KP.x, KPS[N].y, the index a constant expression itself. It is the part’s initial value: the one the constant’s lists, else its type’s. A whole part goes where its type does (p : PT := KPS[2]). An index outside the array is E0032, as it would fault when the program runs. CODESYS and TwinCAT take a constant’s element there too.
  • An integer expression is worked out exactly, not in its operands’ type, as a bound is: d : DINT := N2 * 10000 is 40000 though N2 is an INT. A value its variable’s type does not hold is E0020, and so is one a conversion’s type does not: INT_TO_SINT(300), REAL_TO_INT(1.0E10). REAL is worked out in single precision, LREAL in double, as the run would; an infinite result or one that is no number is E0020 too.
  • The functions worked out are those whose result is exact: the operators’ own (ADD, GT, …), ABS, MIN, MAX, LIMIT, SEL, MUX, MOVE, the shifts and rotations, the conversions between numbers and TRUNC, and a duration times or over a number.
  • The mathematics of Table 28 and ** - SQRT, LN, LOG, EXP, SIN, COS, TAN, ASIN, ACOS, ATAN, EXPT - are the target’s own, which may differ from the compiler’s in the last digit. The compiler works the expression out only to tell that the result is a number its type holds, and the target computes it as the variable is set up. A result that is infinite or no number (SQRT(-1.0), LN(0.0)) is E0020, and so is one within a factor of four of the type’s largest value, where the last digit could decide.
  • A division by zero, a shift by a negative count, a MUX selecting an input it has not, an index outside a constant array, and a constant whose initial value names itself, have no value: E0032. MOD by zero is 0, as when the program runs (Integers).
  • A variable, or the result of a FUNCTION of the program’s, is no constant: E0032. CODESYS and TwinCAT take any expression there and compute it as the application starts; Ed.3 has a constant expression, and so has iecf.
  • An input, a block’s or a FUNCTION’s, not given at a call takes its initial value the same way, and so does a FUNCTION’s own variable at each call.

Not compiled yet (E0009) in an initial value: a function of strings or of dates (LEN('abc'), CONCAT_DATE_TOD), a conversion to or from text, and what depends on the last digit of the target’s mathematics - an integer converted from it (REAL_TO_INT(SQRT(X))), a division by it. The harness cases constinit and constfns run the rest.

Arrays of any size: ARRAY[*]

A parameter declared ARRAY [*] OF INT, or ARRAY [*, *] OF INT, takes an array of any bounds with as many dimensions and the same element type (Ed.3 6.5.3); LOWER_BOUND(a, d) and UPPER_BOUND(a, d) give dimension d’s bounds, a DINT (Table 15). This is Ed.3’s own Example 2:

FUNCTION SUM : DINT
    VAR_IN_OUT A : ARRAY [*] OF INT; END_VAR
    VAR i, s : DINT; END_VAR
    FOR i := LOWER_BOUND(A, 1) TO UPPER_BOUND(A, 1) DO
        s := s + A[i];
    END_FOR;
    SUM := s;
END_FUNCTION

It is an in-out of a FUNCTION, a method or a FUNCTION_BLOCK, as in CODESYS and TwinCAT, and is passed on to another’s (the harness case flexarrays). It is also a FUNCTION’s or method’s input or output, as Ed.3 allows and Siemens has it for an FC (the harness case flexio):

FUNCTION TOTAL : DINT
    VAR_INPUT a : ARRAY [*] OF INT; END_VAR
    VAR i : DINT; END_VAR
    FOR i := LOWER_BOUND(a, 1) TO UPPER_BOUND(a, 1) DO
        TOTAL := TOTAL + a[i];
    END_FOR;
END_FUNCTION

FUNCTION CLEAR_TO
    VAR_INPUT v : INT; END_VAR
    VAR_OUTPUT a : ARRAY [*] OF INT; END_VAR
    VAR i : DINT; END_VAR
    FOR i := LOWER_BOUND(a, 1) TO UPPER_BOUND(a, 1) DO
        a[i] := v;
    END_FOR;
END_FUNCTION

called TOTAL(x), TOTAL(a := x), CLEAR_TO(v := 7, a => x) or CLEAR_TO(7, x). Such an input or output is the caller’s array itself, not a copy of it - the function cannot hold an array whose size it does not know - and what follows from that is:

  • the input is read, never written: an assignment to it or to an element, or giving it to an in-out or an output, is E0015. It is given on to another ARRAY[*] input;
  • the output’s elements are set to their type’s initial value as the call starts, as any output of a FUNCTION is; what the function then writes is in the caller’s array at once, so a call that faults has already changed it, where an output of fixed bounds is only stored when the call returns;
  • the call gives each one an array: there is no initial value to stand in for an input left out, and no place for an output not taken (E0016);
  • one array given as the input and as the output of the same call is set to its initial value before it is read.

A FUNCTION_BLOCK’s input or output is kept in the instance, so an ARRAY[*] is only its in-out. Anywhere else an ARRAY[*] is E0033.

A whole ARRAY[*] is assigned from another, or from an array of fixed bounds, of as many dimensions and the same element type: dst := src;. The two must have as many elements in each dimension, which only the run knows: when they differ it is a fault INDEX and nothing is copied. The bounds themselves need not match, 1..3 copies onto -1..1. An ARRAY[*] is not assigned to an array of fixed bounds, nor given to a parameter of fixed bounds (E0013), as Siemens has it (the harness case flexcopy).

An index into one is checked against the bounds it was given, a fault INDEX outside them, as for any array. So is LOWER_BOUND(a, d) with a d outside the array’s dimensions, which Ed.3 calls an error: a fault INDEX when d is computed, refused when it is a constant. LOWER_BOUND and UPPER_BOUND take an array of fixed bounds too, as Ed.3’s Example 1 does.

Ed.3 Example 2 also gives SUM(A2[2]), one row of a two-dimensional ARRAY [1..20, -2..2]. An array indexed in its first dimensions only is the array of the others; so is an element of an array of arrays, by its declaration:

VAR
    a2 : ARRAY [1..2, 0..2] OF INT := [1, 2, 3, 4, 5, 6];
    aa : ARRAY [1..2] OF ARRAY [0..2] OF INT := [[1, 2, 3], [4, 5, 6]];
    row : ARRAY [0..2] OF INT;
END_VAR
total := SUM(a2[2]);        (* 15: the row 4, 5, 6 *)
total := SUM(aa[2]);        (* 15 *)
total := SUM(a2[i]);        (* the row i; i checked, a fault INDEX outside 1..2 *)
row := a2[1];               (* a row copied: 1, 2, 3 *)
x := a2[2][0];              (* 4, as a2[2, 0] *)
x := aa[2][0];              (* 4 *)

A row is an array like any other: given to an ARRAY[*] or to a parameter of its bounds, or assigned. An ARRAY[*, *] indexed once is an ARRAY[*], to pass on. More indices than dimensions is E0014.

The harness case flexarrays runs these.

A value outside a subrange is clamped, and flagged

A value stored into a subrange variable becomes the bound it passed (INT (0..100) given 150 holds 100) and is flagged CONVERSION. It holds for every store: an assignment, an element or field of the type, a function block’s input, an output taken into one, and an in-out given one: the function or block works on a copy, which is stored back - clamped - when the call returns (the harness case inoutcopy). A FOR over a subrange starts at its start clamped and stops at the bound it would pass. A constant initial value outside the subrange is refused when the program is built.

A subrange variable starts at its lower limit (Ed.3 6.4.4.4.2): x : INT (2..5) starts as 2, an element of that type too, unless the type or the declaration gives an initial value.

A subrange is a FUNCTION’s or a method’s input, output, in-out and result, and a block’s in-out, as it is a block’s input and output:

  • an input given a value outside it is clamped and flagged as the function starts, where the input is declared (as a STRING input too long is cut there); not given, it is its initial value;
  • an output, and the result, are clamped as the body stores into them; a result the body does not set is the lower limit;
  • an in-out is the caller’s variable: a store through it is clamped to the in-out’s own bounds, whatever the caller’s variable is.

The harness case subparams runs each.

CODESYS and TwinCAT do the same when the project adds the CheckRangeSigned and CheckRangeUnsigned functions, and nothing without them; iecf always checks, and the platform sees it.

ExpressionTypeResultFlagNote
INT#50INT(0..100)50—
INT#150INT(0..100)100flag CONVERSIONthe bound it passed
INT#-5INT(0..100)0flag CONVERSION
INT#100INT(0..100)100—the bound itself is inside

Enumerations

An enumerated value is held as an integer, the values numbered from 0 in their order, in an INT (as CODESYS and TwinCAT hold one), so it has at most 32768 of them (E0020 at the next). Only the type’s own values can be stored in it (Ed.3 6.4.4.2); there is nothing to check at run time. A variable starts as the type’s initial value, the first of its values unless the declaration gives one. A value two enumerations have is written Type#value (E0010 otherwise).

TYPE
    mode : (idle, run, stop) := run;
END_TYPE
VAR
    m  : mode;                      (* run *)
    mi : (low, mid, top) := mid;    (* declared in place *)
END_VAR

An enumeration is also a FUNCTION’s or method’s input, output, in-out and result and a block’s input, output and in-out; an input not given and a result not set are the type’s initial value.

Types with named values

A declaration that gives its values numbers, or names a base type, is a type with named values (Ed.3 6.4.4.3). Its variables are used as the base type is: any value of it assigned, arithmetic, comparison with a number; the names are constants of that type, a CASE’s labels among them.

TYPE
    col : INT (red := 1, green := 5, blue) := green;   (* blue is 6 *)
    lv  : WORD (lo := 16#10, hi := 16#20);
END_TYPE
VAR c : col; x : INT; END_VAR                          (* c is green, 5 *)
c := 27;
c := red + 1;
x := c + col#blue;
CASE c OF
    red:              x := 1;
    green - 3, blue:  x := 2;
END_CASE;

The base type is an integer or a bit string type (but BOOL), INT when the declaration names none, as in CODESYS and TwinCAT: (slow := 2, fast := 4) is added to an INT as it is, and a value an INT cannot hold needs the type named, DINT (far := 100000). A value is a constant expression the base type holds (E0020 otherwise); one not given is one more than the value before it, from 0, as CODESYS and TwinCAT count. A variable of the base type is given to an in-out of the type. A variable’s own declaration may be one, x : INT (lo := 1, hi := 5) := hi;: its names are known in the POU, its numbers may name the POU’s constants, and where two such declarations share a name it is used without a type’s name as long as they give it the same number and base type (fbargs). The harness case enums runs all of this.

Arrays and structures given to a FUNCTION

An array or a structure is also a FUNCTION’s or method’s input, output, in-out or result: an input copied in as the function starts, so the function never writes the caller’s; a result returned whole, xs := RAMP(start := 1), or given straight to another’s input (the harness case fnparams). A part of what a FUNCTION gives is read where it is called, Shift(p).x, Ramp(3)[i], Make(2).n for an instance (the harness case callparts): the call is made once, into a buffer of the statement’s, and the part is read from it; an index is checked as any other. An in-out of one is given any variable of its type: one at an address (AT %MW0), a part of it, or one reached through a reference, is given as a copy, read before the call and stored back as it returns, since the image holds no C object to point at; so is an array there given to an ARRAY[*]. Two in-outs of one call given the same variable at an address are two copies: the one stored back last is what stays (the harness case inoutdata). A FUNCTION_BLOCK’s in-out of an array, a structure or a STRING is too, set by each call: the block works on the caller’s variable (flexarrays).