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

Initializers

A declaration can specify an initial value: i: DINT := 1, TYPE MyInt: INT := 7, p: Point := (y := 2), or ptr: POINTER TO DINT := ADR(i). The index first tries to evaluate that expression as a constant.

Constant values can become static data. Constructors initialize instances, and statements at the start of a body initialize stack variables. These paths can write the same value more than once. This chapter follows where each initializer goes and which values need runtime work.

The example combines defaults, aggregate literals, addresses, and a call inside an array initializer. It brings together the storage rules from the preceding construct chapters:

VAR_GLOBAL CONSTANT
    MAX: DINT := 3;
END_VAR

VAR_GLOBAL
    gCount: DINT := MAX + 1;
END_VAR

TYPE MyInt: INT := 7; END_TYPE

TYPE Point:
    STRUCT
        x: DINT := 1;
        y: DINT;
    END_STRUCT
END_TYPE

FUNCTION_BLOCK Counter
    VAR_INPUT
        step: DINT := 1;
    END_VAR
    VAR
        count: DINT;
    END_VAR
END_FUNCTION_BLOCK

FUNCTION pick: DINT
    VAR
        local: DINT := 5;
    END_VAR

    pick := local;
END_FUNCTION

PROGRAM main
    VAR
        i: DINT := MAX + 1;
        n: MyInt;
        p: Point := (y := 2);
        values: ARRAY[0..2] OF DINT := [1, 2, 3];
        counter: Counter := (step := 10);
        ptr: POINTER TO DINT := ADR(i);
        r: REFERENCE TO DINT REF= i;
        readings: ARRAY[0..1] OF DINT := [pick(), 2];
    END_VAR
    VAR_TEMP
        t: DINT := 4;
    END_VAR
END_PROGRAM

Declaration

The parser stores the initializer as an expression on a variable or type declaration. MAX + 1 is a binary expression; (y := 2) is a parenthesized assignment. [1, 2, 3] is an array literal, and ADR(i) is a call. REF= i stores the reference i and marks the declaration for reference initialization.

Pre-processing at the start of the index stage moves inline types such as ARRAY[0..2] OF DINT out into named types (__main_values), but the initializer stays on the variable, not on the new type.

Index

Initializers share the index’s constant store with array bounds and string sizes. A variable or type entry keeps the ID of its expression. Each store entry records whether evaluation succeeded and, if not, why:

pub enum ConstExpression {
    /// Not evaluated yet; scope is the POU the expression was written in, lhs the variable it initializes
    Unresolved { statement: AstNode, scope: Option<String>, lhs: Option<String> },

    /// Folded to a literal, or accepted as is for a struct or array literal
    Resolved(AstNode),

    /// Cannot become static data; the reason says why and what to do instead
    Unresolvable { statement: AstNode, reason: Box<UnresolvableKind> },
}

pub enum UnresolvableKind {
    /// Not a constant, and never will be; reported by validation
    Misc(String),

    /// A literal that does not fit the target type; reported as a warning
    Overflow(String, SourceLocation),

    /// An address, which exists only after codegen has laid out the memory
    Address(InitData),
}

Each entry also records the target type name, so that the evaluator knows in which type to fold and whether the result fits. For the example, the store holds eighteen entries: one per initializer and four for the array bounds 0, 2, 0, 1. After evaluation, the initializers read:

MAX                 DINT             Resolved(3)
gCount              DINT             Resolved(4)                        folded from MAX + 1
MyInt               MyInt            Resolved(7)                        the type's default
Point.x             DINT             Resolved(1)
Counter.step        DINT             Resolved(1)
pick.local          DINT             Resolved(5)
main.i              DINT             Resolved(4)                        folded from MAX + 1
main.p              Point            Resolved(y := 2)                   struct literal, kept as written
main.values         __main_values    Resolved([1, 2, 3])                array literal, kept as written
main.counter        Counter          Resolved(step := 10)
main.ptr            __main_ptr       Unresolvable(ADR(i), Address)      "Try to re-resolve during codegen"
main.r              __main_r         Unresolvable(i, Address)           "Try to re-resolve during codegen"
main.readings       __main_readings  Unresolvable([pick(), 2], Misc)    "Call-statement 'pick' in initializer is not constant."
main.t              DINT             Resolved(4)

The evaluator processes a queue. Literals resolve after a range check. Constant references use the referenced value when available; otherwise they return to the queue. This allows expressions to depend on constants declared later.

A reference to a variable that is not constant is unresolvable, “x is no const reference”, unless the target is a pointer type; then it is an address. ADR, REF, and a bare reference that initializes a REFERENCE TO are addresses too. A call to a user function is a plain unresolvable, because the compiler does not execute code at compile time.

Struct and array literals keep their shape: every element is folded on its own, but the literal as a whole stays a list instead of one value. An element that is not constant makes the whole literal unresolvable, as readings shows. Outside a CONSTANT block, a variable without an initializer has no entry at all, and its value comes from its type: the type’s own default (n: MyInt starts at 7), or zero.

Note

Developer note. The index has an unused map of default-instance entries such as __Point__init and __Counter__init. These entries do not produce LLVM globals. Codegen reads defaults from the type index, while constructors provide runtime initialization.

Annotations

The resolver visits an initializer like any other expression, with the declaring POU as context, and hints it with the declared type of the variable (see Resolver). A struct literal gets no annotation of its own, only the hint, and its assignments resolve the member name against the target type, not against the current POU:

    i: DINT := MAX + 1;
               ^^^^^^^        { kind: Value,                                   resulting_type: "DINT",  hint: "DINT" }
               ^^^            { kind: Variable, qualified_name: "MAX", constant: true, resulting_type: "DINT", hint: None }

    p: Point := (y := 2);
                ^^^^^^^^      { kind: None,                                                             hint: "Point" }
                 ^            { kind: Variable, qualified_name: "Point.y",      resulting_type: "DINT",  hint: None }
                      ^       { kind: Value,                                   resulting_type: "DINT",  hint: "DINT" }

    ptr: POINTER TO DINT := ADR(i);
                            ^^^^^^   { kind: Value,                                resulting_type: "LWORD", hint: "__main_ptr" }
                                ^    { kind: Variable, qualified_name: "main.i",   resulting_type: "DINT",  hint: "DINT" }

    r: REFERENCE TO DINT REF= i;
                              ^      { kind: Variable, qualified_name: "main.i",   resulting_type: "DINT",  hint: "__main_r" }

TYPE MyInt: INT := 7; END_TYPE
                   ^                 { kind: Value,                                resulting_type: "DINT",  hint: "INT" }

ADR(i) is a LWORD value hinted to the pointer type __main_ptr; REF= i is the variable itself hinted to the reference type. Both hints tell codegen to store an address rather than a value. The elements of [1, 2, 3] are each hinted DINT, the literal as a whole __main_values.

Lowering

The init participant moves instance and type initialization into constructors such as main__ctor and Point__ctor. Function locals and VAR_TEMP variables get statements at the start of the body. It also removes the non-constant array literal from readings, so the rebuilt index no longer holds that unresolved initializer.

The array lowerer then splits the assignment self.readings := [pick(), 2] in the constructor into one assignment per element. Everything else stays where it is: the resolved entries are still in the store, and codegen reads them for the static data.

Codegen

Static data

Every global and every program instance gets a compile-time initial value. For a variable, codegen takes the resolved expression from the store; when there is none, the default of its type; when the type has none, zero. Unresolvable entries count as none. The instance of main and the globals are therefore complete before any code runs, apart from the two addresses and the array with the call:

@MAX = unnamed_addr constant i32 3
@gCount = global i32 4
@main_instance = global %main { i32 4, i16 7, %Point { i32 1, i32 2 }, [3 x i32] [i32 1, i32 2, i32 3], %Counter { ptr null, i32 10, i32 0 }, ptr null, ptr null, [2 x i32] zeroinitializer }

i starts at the folded value 4, and n uses the MyInt default 7. p combines the type’s x := 1 with the variable’s y := 2. counter uses its variable initializer step := 10 instead of the type default. Codegen computes struct defaults once per type and reuses them.

For every local or temporary member of aggregate type with an initializer, codegen also emits a constant named after the member, __main.values__init, that holds the value. When such a variable lives on the stack, in a function or a VAR_TEMP block, its slot is initialized with a memcpy from that constant instead of element by element. For a member of a program or function block the constant is emitted but not used, because the value is already in the instance.

Constructor

The generated constructor writes the same values again at start-up, and this is the only place where the three unresolvable initializers get their value. The stores and calls of main__ctor, without the address arithmetic and the empty constructors of the inline types:

  store i32 4, ptr %i, align 4
  call void @MyInt__ctor(ptr %n)
  call void @Point__ctor(ptr %p)
  store i32 2, ptr %y, align 4
  call void @llvm.memcpy.p0.p0.i64(ptr align 1 %values7, ptr align 1 @.const_init, i64 ptrtoint (ptr getelementptr ([3 x i32], ptr null, i32 1) to i64), i1 false)
  call void @Counter__ctor(ptr %counter)
  store i32 10, ptr %step, align 4
  store ptr %i15, ptr %ptr13, align 8
  store ptr %i21, ptr %r19, align 8
  %call = call i32 @pick()
  store i32 %call, ptr %tmpVar, align 4
  store i32 2, ptr %tmpVar27, align 4

The type constructor runs before the member’s own literal, so Point__ctor sets x := 1 and the following store sets y := 2. ADR(i) and REF= i become stores of the address of i, and pick() is called once, at start-up, not on every cycle. The unit constructor stores gCount := 4 in the same way and then calls main__ctor on the instance (see Codegen, Initialization).

Stack

A function local, a return variable, and a VAR_TEMP have no instance. Codegen stores their initial value when it creates the stack slot at the start of the body, and the statement the init participant prepended stores it again:

define i32 @pick() {
entry:
  %pick = alloca i32, align 4
  %local = alloca i32, align 4
  store i32 5, ptr %local, align 4
  store i32 0, ptr %pick, align 4
  store i32 5, ptr %local, align 4
  ...

The example writes resolved initializers twice: in static data and a constructor, or twice at the start of a body. LLVM can remove redundant stores. Addresses and runtime calls still need executable initialization, and the @llvm.global_ctors entry of the unit constructor makes that work happen before the program starts, also for a C program that links the object.

Validation

The validator turns the states of the store into diagnostics. An Unresolvable entry with a Misc reason is an error at the initializer, “Unresolved constant chosen variable: Call-statement ‘pick’ in initializer is not constant.” (E033). The same code is reported for an entry that is still Unresolved after evaluation, which happens when constants reference each other in a cycle, and for a CONSTANT variable whose type has no default that can be resolved. A CONSTANT without an initializer is otherwise fine: the parser gives it a default-value node that folds to the default of the type.

An Overflow reason is a warning. An Address reason is not an error; the validator checks instead that the pointed-to type matches the declared pointer type. A separate rule rejects the address of a temporary kept in a member variable (E109). It fires when the initializer names the temporary directly, as an AT alias or a REF= binding, but not for ADR or REF: it reads the argument of the call directly, and by then lowering has put that argument in an expression list.

A scalar initializer containing a call is rejected. The same call inside an array literal can pass because init lowering removes that initializer before validation. Array lowering then turns it into element assignments.

At a glance

InitializerConstant storeStatic dataConstructor or body
i: DINT := 1Resolved(1)i32 1 in the instancestore i32 1
i: DINT := MAX + 1Resolved(4), foldedi32 4store i32 4
TYPE MyInt: INT := 7Resolved(7) on the typedefault for every MyInt without its own initializerMyInt__ctor stores 7
p: Point := (y := 2)Resolved, literal kept as writtenstruct constant, type default for the other membersPoint__ctor(p), then a store for y
values: ARRAY := [1, 2, 3]Resolved, literal keptarray constantmemcpy from a constant
counter: Counter := (step := 10)Resolved, literal keptstruct constant with the FB’s defaultsCounter__ctor then store
ptr := ADR(i)Unresolvable(Address)ptr nullstore of the address of i
r REF= iUnresolvable(Address)ptr nullstore of the address of i
readings := [pick(), 2]Unresolvable(Misc), removed by loweringzeroinitializerone store per element, the call runs once
local: DINT := 5 in a functionResolved(5)nonetwo store at the start of the body
t: DINT := 4 in VAR_TEMPResolved(4)nonetwo store at the start of the body
chosen: DINT := pick()Unresolvable(Misc)nonenone; E033 rejects the program