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__initand__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
| Initializer | Constant store | Static data | Constructor or body |
|---|---|---|---|
i: DINT := 1 | Resolved(1) | i32 1 in the instance | store i32 1 |
i: DINT := MAX + 1 | Resolved(4), folded | i32 4 | store i32 4 |
TYPE MyInt: INT := 7 | Resolved(7) on the type | default for every MyInt without its own initializer | MyInt__ctor stores 7 |
p: Point := (y := 2) | Resolved, literal kept as written | struct constant, type default for the other members | Point__ctor(p), then a store for y |
values: ARRAY := [1, 2, 3] | Resolved, literal kept | array constant | memcpy from a constant |
counter: Counter := (step := 10) | Resolved, literal kept | struct constant with the FB’s defaults | Counter__ctor then store |
ptr := ADR(i) | Unresolvable(Address) | ptr null | store of the address of i |
r REF= i | Unresolvable(Address) | ptr null | store of the address of i |
readings := [pick(), 2] | Unresolvable(Misc), removed by lowering | zeroinitializer | one store per element, the call runs once |
local: DINT := 5 in a function | Resolved(5) | none | two store at the start of the body |
t: DINT := 4 in VAR_TEMP | Resolved(4) | none | two store at the start of the body |
chosen: DINT := pick() | Unresolvable(Misc) | none | none; E033 rejects the program |