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

Variable-Length Arrays

A fixed array carries its bounds in its type. A variable-length array (VLA) parameter uses * instead: ARRAY[*] OF DINT accepts different one-dimensional DINT arrays. ARRAY[*, *] OF INT accepts two-dimensional INT arrays. The caller passes a small struct containing the array address and bounds. The callee uses these bounds to compute offsets at run time.

The example passes two arrays with different bounds to sum and a two-dimensional array to fill:

FUNCTION sum: DINT
    VAR_IN_OUT
        values: ARRAY[*] OF DINT;
    END_VAR
    VAR
        i: DINT;
    END_VAR

    FOR i := LOWER_BOUND(values, 1) TO UPPER_BOUND(values, 1) DO
        sum := sum + values[i];
    END_FOR
END_FUNCTION

FUNCTION fill: DINT
    VAR_INPUT {ref}
        grid: ARRAY[*, *] OF INT;
    END_VAR

    grid[0, 1] := 7;
END_FUNCTION

PROGRAM main
    VAR
        small: ARRAY[0..2] OF DINT := [1, 2, 3];
        large: ARRAY[10..19] OF DINT;
        table: ARRAY[0..1, 0..2] OF INT;
        total: DINT;
    END_VAR

    total := sum(small);
    total := total + sum(large);
    fill(table);
END_PROGRAM

Declaration

The parser produces an ordinary array type node with a flag that marks it as variable-length. The bounds are one placeholder node per *, so ARRAY[*] has one and ARRAY[*, *] has a list of two; the element type is a reference like in any array.

Pre-processing names these inline types __sum_values and __fill_grid, as it does for fixed arrays. The {ref} modifier marks an input as passed by reference. The Validation section explains allowed parameter forms and the behavior without this modifier.

Index

A VLA does not become an array in the index. The indexer registers it as a struct with two members and a source marker that says what the struct stands for:

Struct {
    /// The pre-processed type name, __sum_values
    name: TypeId,

    /// Two members: a pointer to the array and the bounds array
    members: Vec<VariableIndexEntry>,

    /// For a VLA: Internal(VariableLengthArray { inner_type_name, ndims })
    source: StructSource,
}

For values: ARRAY[*] OF DINT, the indexer creates four types. The main struct, __sum_values, has the nature __VLA, a marker used by resolution and validation.

The first member, struct_vla_dint_1, points to an array type with undetermined bounds. That type lets the resolver attach an array hint; codegen does not lay it out. The second member, dimensions, is a fixed DINT array with a lower and upper bound for each dimension:

__sum_values                       Struct, nature __VLA, source VariableLengthArray { DINT, 1 dim }
  .struct_vla_dint_1               __ptr_to___sum_values_vla_1_dint
  .dimensions                      __bounds___sum_values_vla_1_dint       ARRAY[0..1] OF DINT
__sum_values_vla_1_dint            Array of DINT, bounds Undetermined
__fill_grid                        Struct, nature __VLA, source VariableLengthArray { INT, 2 dims }
  .struct_vla_int_2                __ptr_to___fill_grid_vla_2_int
  .dimensions                      __bounds___fill_grid_vla_2_int         ARRAY[0..1, 0..1] OF DINT

The parameter entries do not point at the struct directly. Like every by-reference parameter, sum.values and fill.grid store an auto-dereferencing pointer type, __auto_pointer_to___sum_values, so that a body can write values[i] without a ^ (see Index, Indexing a unit). The names of the helper types are built from the struct name, the dimension count, and the element type, so two VLA parameters never share them, even when their shapes agree.

Annotations

Inside the callee, the VLA reference has a variable annotation for the struct type and a hint for the array behind its pointer. The hint permits indexing. The complete element access has the array’s element type:

    sum := sum + values[i];
                 ^^^^^^^^^        { kind: Value,                                        resulting_type: "DINT" }
                 ^^^^^^           { kind: Variable, qualified_name: "sum.values",       resulting_type: "__sum_values",  hint: Variable "__sum_values_vla_1_dint" }
                        ^         { kind: Variable, qualified_name: "sum.i",            resulting_type: "DINT",          hint: None }

    grid[0, 1] := 7;
    ^^^^^^^^^^                    { kind: Value,                                        resulting_type: "INT" }
    ^^^^                          { kind: Variable, qualified_name: "fill.grid",        resulting_type: "__fill_grid",   hint: Variable "__fill_grid_vla_2_int" }
                  ^               { kind: Value,                                        resulting_type: "DINT",          hint: "INT" }

At the call site the argument is an ordinary fixed array, and it receives the argument hint of the parameter, the auto-dereferencing pointer to the VLA struct. The mismatch between the annotation (a fixed array) and the hint (a pointer to a VLA struct) is the signal codegen acts on:

    total := sum(small);
                 ^^^^^            { kind: Variable, qualified_name: "main.small",       resulting_type: "__main_small",  hint: Argument { resulting_type: "__auto_pointer_to___sum_values", position: 0 } }

LOWER_BOUND and UPPER_BOUND are built-in generic functions, declared as FUNCTION LOWER_BOUND<U: __ANY_VLA, T: ANY_INT>: DINT. Their annotation hints the first argument with its own VLA type when it is one, and with the reserved placeholder type __VLA when it is not, so that the argument fails the type check with a readable name. The second argument keeps its integer type, and the call is a DINT value.

Lowering

No participant rewrites VLAs. The loop desugarer turns the FOR of the example into a WHILE TRUE loop, which is why the IR below reads the lower bound once in front of the loop and the upper bound again on every iteration.

Codegen

Layout

The struct is laid out as declared: a pointer and an array of i32 with two entries per dimension. The callee receives a pointer to it:

%__sum_values = type { ptr, [2 x i32] }
%__fill_grid = type { ptr, [4 x i32] }

define i32 @sum(ptr %0)
define i32 @fill(ptr %0)

Passing

The caller allocates the VLA struct on its stack when a fixed-array argument has a VLA parameter hint. It stores the first element’s address and the array’s known bounds. Bounds follow declaration order, lower then upper for each dimension. For sum(large) and fill(table):

  %outer_arr_gep2 = getelementptr inbounds [10 x i32], ptr %large, i32 0, i32 0
  %vla_struct3 = alloca %__sum_values, align 8
  %vla_array_gep4 = getelementptr inbounds nuw %__sum_values, ptr %vla_struct3, i32 0, i32 0
  %vla_dimensions_gep5 = getelementptr inbounds nuw %__sum_values, ptr %vla_struct3, i32 0, i32 1
  store [2 x i32] [i32 10, i32 19], ptr %vla_dimensions_gep5, align 4
  store ptr %outer_arr_gep2, ptr %vla_array_gep4, align 8
  ...
  %call7 = call i32 @sum(ptr %vla_struct_ptr6)

  store [4 x i32] [i32 0, i32 1, i32 0, i32 2], ptr %vla_dimensions_gep12, align 4
  ...
  %call14 = call i32 @fill(ptr %vla_struct_ptr13)

The same call with small stores [i32 0, i32 2]. The callee only ever sees the struct, so sum compiles once and runs on both arrays.

Note

Developer note. Every call allocates a fresh struct, and the value is copied once more into a second stack slot before the call (%vla_struct then %vla_struct_ptr). A loop that calls sum a thousand times performs a thousand allocations; the optimizer removes most of them. The wrap is done by the argument generator for functions and methods.

Element access

An access values[i] cannot use a constant offset, because the lower bound is a run-time value. Codegen loads the data pointer and the bounds from the struct, subtracts the lower bound from the index, and indexes the data pointer with the result:

  %vla_arr_gep = getelementptr inbounds nuw %__sum_values, ptr %deref17, i32 0, i32 0
  %vla_arr_ptr = load ptr, ptr %vla_arr_gep, align 8
  %dim_arr = getelementptr inbounds nuw %__sum_values, ptr %deref17, i32 0, i32 1
  %start_idx_ptr0 = getelementptr inbounds [2 x i32], ptr %dim_arr, i32 0, i32 0
  %start_idx_value0 = load i32, ptr %start_idx_ptr0, align 4
  %tmpVar19 = sub i32 %load_i18, %start_idx_value0
  %arr_val = getelementptr inbounds i32, ptr %vla_arr_ptr, i32 %tmpVar19
  %load_tmpVar = load i32, ptr %arr_val, align 4

With bounds 10..19, values[10] has offset zero. For several dimensions, codegen uses the same formula as fixed arrays, but loads the bounds at run time. It subtracts each lower bound, multiplies by the lengths of later dimensions, and adds the results. For grid[0, 1]:

  %1 = sub i32 %end_idx_value0, %start_idx_value0
  %len_dim0 = add i32 1, %1
  %2 = sub i32 %end_idx_value1, %start_idx_value1
  %len_dim1 = add i32 1, %2
  %adj_access0 = sub i32 0, %start_idx_value0
  %adj_access1 = sub i32 1, %start_idx_value1
  %multiply = mul i32 %adj_access0, %accessor_factor
  %multiply3 = mul i32 %adj_access1, 1
  %accessor = load i32, ptr %accum1, align 4
  %arr_val = getelementptr inbounds i16, ptr %vla_arr_ptr, i32 %accessor
  store i16 7, ptr %arr_val, align 2

Temporary stack slots named accum hold intermediate products and sums, which accounts for some of the extra IR.

Bounds

LOWER_BOUND(values, 1) reads one entry of the dimensions array. Dimension n occupies entries 2(n-1) and 2(n-1)+1, so the lower bound of dimension 1 is entry 0 and the upper bound entry 1; a literal dimension gives a constant entry, an expression gives the same arithmetic at run time:

  %dim = getelementptr inbounds nuw %__sum_values, ptr %deref, i32 0, i32 1
  %1 = getelementptr inbounds [2 x i32], ptr %dim, i32 0, i32 0
  %2 = load i32, ptr %1, align 4

No bounds check is generated for an element access; an index outside the passed array reads or writes past it, as it does for a fixed array.

Validation

The validator keeps VLAs to the places where a struct of caller-owned storage makes sense. A VLA is accepted as VAR_INPUT {ref}, VAR_OUTPUT, or VAR_IN_OUT of a function or method, and as VAR_IN_OUT of a function block. Every other block of a function or a method is rejected with E044, Variable Length Arrays are not allowed to be defined as Local variables inside a Function. A by-value VAR_INPUT without {ref} in a function is only the warning E047, Variable Length Arrays are always by-ref, even when declared in a by-value block; the parameter is still passed as a pointer to the struct.

Warning

Three more forms never reach validation. A global VLA, a VLA anywhere in a program, and a VLA in a function block outside VAR_IN_OUT abort the resolver with internal error: entered unreachable code, so their E044 is never printed. The accepted VAR_IN_OUT of a function block is unsafe today as well: the caller stores the bare array address into the parameter instead of the wrapped struct, and the body then reads that address as a struct.

At a call, the argument must match the parameter in element type and dimension count. sum(ints) with an INT array and sum(grid) with a two-dimensional array are invalid assignments (E037), reported with the array types in the message. Inside a body, a VLA cannot be assigned to another VLA (E037).

An access must use as many indices as the VLA has dimensions (E045), and a literal dimension passed to LOWER_BOUND or UPPER_BOUND must be between one and the dimension count (E046). A fixed array passed to a bound function fails the generic constraint and is reported against the placeholder type as cannot assign 'ARRAY[0..2] OF DINT' to 'VARIABLE LENGTH ARRAY' (E037).

At a glance

Structured TextIndexAnnotationLLVM
values: ARRAY[*] OF DINT (parameter)struct __<pou>_<var> with a pointer and a bounds member, nature __VLA; the parameter holds an auto-dereferencing pointer to itVariable of the struct, hinted with the placeholder array __<pou>_<var>_vla_1_dint{ ptr, [2 x i32] }, passed as ptr
ARRAY[*, *] OF INTsame, with two dimensions in the source markersame{ ptr, [4 x i32] }
f(small) with small: ARRAY[0..2] OF DINTargument hinted with the pointer to the VLA structalloca of the struct, store data pointer and [0, 2], pass its address
values[i]Value of the element typeload pointer and lower bound, sub, getelementptr
grid[a, b]Value of the element typerun-time lengths, normalized indices, multiply and accumulate
LOWER_BOUND(values, 1)built-in generic <U: __ANY_VLA, T: ANY_INT>Value DINT; the VLA argument hinted with its own typeload entry 2(n-1) of the bounds array; UPPER_BOUND entry 2(n-1)+1