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

POUs

A program organization unit (POU) contains code and declarations. Programs, function blocks, and classes keep state in an instance. Functions keep their local data on the stack. Methods and actions use their owner’s instance. These storage rules determine the generated layouts and call signatures.

The example uses a function, a function block, a class, a method, and an action. Follow how their variables become instance fields or stack slots:

FUNCTION scale: DINT
    VAR_INPUT
        value: DINT;
        factor: INT := 2;
    END_VAR
    VAR_IN_OUT
        count: DINT;
    END_VAR
    VAR_OUTPUT
        overflow: BOOL;
    END_VAR
    VAR
        tmp: DINT;
    END_VAR

    count := count + 1;
    tmp := value * factor;
    overflow := tmp < value;
    scale := tmp;
END_FUNCTION

FUNCTION_BLOCK Counter
    VAR_INPUT
        step: DINT := 1;
    END_VAR
    VAR_OUTPUT
        total: DINT;
    END_VAR
    VAR
        calls: DINT;
    END_VAR
    VAR_TEMP
        scratch: DINT;
    END_VAR

    METHOD reset
        total := 0;
        calls := 0;
    END_METHOD

    scratch := step;
    total := total + scratch;
    calls := calls + 1;
END_FUNCTION_BLOCK

ACTIONS Counter
    ACTION double
        step := step * 2;
    END_ACTION
END_ACTIONS

CLASS Limits
    VAR
        max: DINT := 100;
    END_VAR

    METHOD exceeded: BOOL
        VAR_INPUT
            candidate: DINT;
        END_VAR

        exceeded := candidate > max;
    END_METHOD
END_CLASS

PROGRAM main
    VAR
        counter: Counter;
        limits: Limits;
        hits: DINT;
        flag: BOOL;
    END_VAR

    counter(step := 5, total => hits);
    counter.double();
    hits := scale(counter.total, count := hits, overflow => flag);
    IF limits.exceeded(hits) THEN
        counter.reset();
    END_IF
END_PROGRAM

Declaration

The parser splits every POU into two nodes: a declaration with the name, the kind, the return type, and the variable blocks, and an implementation with the statements (see Lexer and Parser).

A method has its own POU declaration, such as Counter.reset, with a reference to its parent. The parser stores parent and method side by side. An action has only an implementation, such as Counter.double, which uses Counter as its type. A class also has an implementation; its body is empty in a valid program.

Index

The index records POU declarations, implementations, and variable layouts. Actions reuse the parent’s layout. The POU entry identifies the kind:

pub enum PouIndexEntry {
    /// One static instance, held in instance_variable, of the struct instance_struct_name
    Program { name, instance_struct_name, instance_variable, .. },

    /// Many instances; the struct is instance_struct_name; super_class and interfaces for polymorphism
    FunctionBlock { name, instance_struct_name, super_class, interfaces, .. },

    /// Like a function block, but it declares no parameters and has no body
    Class { name, instance_struct_name, super_class, interfaces, .. },

    /// No instance; the return type and whether the parameter list is variadic or generic
    Function { name, return_type, generics, is_variadic, .. },

    /// Belongs to parent_name; its own parameters live in the struct instance_struct_name
    Method { name, parent_name, return_type, instance_struct_name, .. },

    /// Belongs to parent_name and shares its instance struct
    Action { name, parent_name, instance_struct_name, .. },
}

The implementation entry connects a callable name with the body and its kind:

pub struct ImplementationIndexEntry {
    /// The name a call uses, "Counter.reset" for a method
    call_name: String,

    /// The struct type the body runs on; the parent for an action, the method's own struct for a method
    type_name: String,

    /// The class or function block a method belongs to
    associated_class: Option<String>,

    /// Program, Function, FunctionBlock, Class, Method, Action, or a generated constructor
    implementation_type: ImplementationType,

    // ... other omitted fields
}

The type is the instance struct, registered under the name of the POU in the type index. It has one member per declared variable, in declaration order, whatever block the variable is in, and each member records its block as an argument type.

Two rules change the type a member stores. A VAR_IN_OUT, and a VAR_OUTPUT of a function or method, is passed by reference, so the member gets an auto-dereferencing pointer type, __auto_pointer_to_DINT, that the indexer registers on demand. And a function or method with a return type gets one extra member named like the POU, so scale.scale and Limits.exceeded.exceeded carry argument type Return. For the example:

scale            Function, return_type "DINT"
    scale.value       DINT                     ByVal(Input)     0
    scale.factor      INT                      ByVal(Input)     1   initial value: 2
    scale.count       __auto_pointer_to_DINT   ByRef(InOut)     2
    scale.overflow    __auto_pointer_to_BOOL   ByRef(Output)    3
    scale.tmp         DINT                     ByVal(Local)     4
    scale.scale       DINT                     ByVal(Return)    5

Counter          FunctionBlock, instance struct "Counter"
    Counter.step      DINT                     ByVal(Input)     0   initial value: 1
    Counter.total     DINT                     ByVal(Output)    1
    Counter.calls     DINT                     ByVal(Local)     2
    Counter.scratch   DINT                     ByVal(Temp)      3

Counter.reset    Method, parent "Counter", instance struct "Counter.reset" (no members)
Counter.double   Action, parent "Counter", instance struct "Counter"

Limits           Class, instance struct "Limits"
    Limits.max        DINT                     ByVal(Local)     0   initial value: 100

Limits.exceeded  Method, parent "Limits", return_type "BOOL"
    Limits.exceeded.candidate   DINT           ByVal(Input)     0
    Limits.exceeded.exceeded    BOOL           ByVal(Return)    1

main             Program, instance variable "main_instance" of type "main"
    main.counter      Counter                  ByVal(Local)     0
    main.limits       Limits                   ByVal(Local)     1
    main.hits         DINT                     ByVal(Local)     2
    main.flag         BOOL                     ByVal(Local)     3

The struct of a function exists only so that scale.tmp can be looked up like any other member; no instance is ever created. A VAR_OUTPUT of a function block is a by-value member, because the instance keeps it; in a function or method it is a by-reference parameter, because no instance keeps it.

A program also records its single instance as a global variable entry named main_instance. A function block or class registers a default-instance entry __Counter__init as well, which no later stage reads (see the Initializers chapter).

Annotations

With the index in place, the resolver gives every name in a body its meaning. Function and method names receive Function annotations with return types. Program and action names receive Program annotations. Function block and class instances are variables of the corresponding POU type. Member references use the declaring POU in their qualified name, including parameters and return variables:

    counter(step := 5, total => hits);
    ^^^^^^^                        { kind: Variable, qualified_name: "main.counter", resulting_type: "Counter" }
            ^^^^^^^^^              { kind: None,                                     hint: Argument { resulting_type: "DINT", position: 0, pou: "Counter" } }
            ^^^^                   { kind: Variable, qualified_name: "Counter.step",  resulting_type: "DINT" }
                       ^^^^^^^^^^^^^  { kind: None,                                  hint: Argument { resulting_type: "DINT", position: 1, pou: "Counter" } }

    counter.double();
            ^^^^^^                 { kind: Program,  qualified_name: "Counter.double" }

    hits := scale(counter.total, count := hits, overflow => flag);
            ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^  { kind: Value, resulting_type: "DINT", hint: "DINT" }
            ^^^^^                  { kind: Function, qualified_name: "scale", return_type: "DINT" }
                  ^^^^^^^^^^^^^    { kind: Variable, qualified_name: "Counter.total", resulting_type: "DINT", hint: Argument { resulting_type: "DINT", position: 0, pou: "scale" } }
                                 ^^^^^^^^^^^^^  { kind: None, hint: Argument { resulting_type: "__auto_pointer_to_DINT", position: 2, pou: "scale" } }
                                 ^^^^^          { kind: Variable, qualified_name: "scale.count", argument_type: ByRef(InOut), auto_deref: Default }

    IF limits.exceeded(hits) THEN
       ^^^^^^^^^^^^^^^^^^^^^       { kind: Value,    resulting_type: "BOOL" }
              ^^^^^^^^             { kind: Function, qualified_name: "Limits.exceeded", return_type: "BOOL" }

An argument hint carries the position of the parameter in the declaration of the callee, the POU that declares it, and the number of EXTENDS steps between the two. Codegen uses the position to find the struct member or the argument slot, the POU to read the declaration, and the step count to reach the base part of the instance.

Inside the callee the return member is an ordinary variable: scale := tmp in scale annotates the left side as scale.scale with argument type Return, and exceeded := candidate > max as Limits.exceeded.exceeded. The VAR_IN_OUT member count carries the auto-dereference marker, which is what makes count := count + 1 in the body read and write through the pointer without a ^.

Lowering

The index and the annotations above describe the POUs as the parser read them. Several participants then change the declarations, and each one runs the affected stages again before the next one sees the tree. The polymorphism lowerer adds method tables and a __vtable member to root classes and function blocks. Derived types access that member through the base inserted by the inheritance lowerer. The init participant creates constructors. The aggregate-return lowerer adds result parameters for strings, arrays, and structs.

After lowering, Counter has four instance fields instead of three. scratch remains a stack variable. The unit also contains ten type and POU constructors of kind Init, plus one unit constructor.

Codegen

Codegen reads the lowered tree and the rebuilt index. The layout comes first, because every signature and every call refers to it.

Layout

Every stateful POU becomes a named struct with one field per member that is not a VAR_TEMP, in member order; a function has no struct. A program instance is a global initialized with the constant initial values of its members, nested instances included:

%main = type { %Counter, %Limits, i32, i8 }
%Counter = type { ptr, i32, i32, i32 }
%Limits = type { ptr, i32 }

@main_instance = global %main { %Counter { ptr null, i32 1, i32 0, i32 0 }, %Limits { ptr null, i32 100 }, i32 0, i8 0 }

The first ptr of Counter and Limits is the __vtable member; the constructors store the table addresses at start-up (see Init, and the Initialization section of Codegen).

Signatures

Every POU becomes exactly one LLVM function, named after its call name with . replaced by __. The kind decides the arguments:

POUSignatureArguments
Program, function block, actionvoid @Counter(ptr)the instance
Classvoid @Limits(ptr)the instance; the body is empty
Methodi8 @Limits__exceeded(ptr, i32)the instance, then the method’s own parameters like a function
Functioni32 @scale(i32, i16, ptr, ptr)one argument per VAR_INPUT by value, one pointer per VAR_IN_OUT and VAR_OUTPUT; the return type is the result

A method receives its own parameters like a function and accesses parent members through the first argument, the instance pointer.

Bodies

A body starts by making every member addressable, and the two kinds differ here. A stateful body computes one pointer per struct member into the instance and allocates a stack slot only for VAR_TEMP:

define void @Counter(ptr %0) {
entry:
  %this = alloca ptr, align 8
  store ptr %0, ptr %this, align 8
  %__vtable = getelementptr inbounds nuw %Counter, ptr %0, i32 0, i32 0
  %step = getelementptr inbounds nuw %Counter, ptr %0, i32 0, i32 1
  %total = getelementptr inbounds nuw %Counter, ptr %0, i32 0, i32 2
  %calls = getelementptr inbounds nuw %Counter, ptr %0, i32 0, i32 3
  %scratch = alloca i32, align 4
  store i32 0, ptr %scratch, align 4
  ...
  ret void
}

The %this slot provides the instance pointer for THIS^ in function blocks and their methods and actions. Programs and classes do not get this slot. A method or action of Counter computes the same member pointers, so reset writes directly to the instance. An action also gets a fresh stack slot for each VAR_TEMP of its owner, so a temp never carries a value from one call to the next. A method cannot reach a VAR_TEMP of its owner at all.

A function allocates stack slots for its parameters, locals, and return variable. It stores the incoming arguments in those slots, gives each local its initial value, and zeroes the return variable:

define i32 @scale(i32 %0, i16 %1, ptr %2, ptr %3) {
entry:
  %scale = alloca i32, align 4
  %value = alloca i32, align 4
  store i32 %0, ptr %value, align 4
  %factor = alloca i16, align 4
  store i16 %1, ptr %factor, align 2
  %count = alloca ptr, align 8
  store ptr %2, ptr %count, align 8
  %overflow = alloca ptr, align 8
  store ptr %3, ptr %overflow, align 8
  %tmp = alloca i32, align 4
  store i32 0, ptr %tmp, align 4
  store i32 0, ptr %scale, align 4
  ...
  %scale_ret = load i32, ptr %scale, align 4
  ret i32 %scale_ret
}

count and overflow hold pointers, and every access loads the pointer first and then the value behind it; that is the auto-dereference the resolver marked. Writing overflow writes the caller’s flag directly.

Calls

The kind of the callee decides how a call is built. A call to a stateful POU stores each passed argument into the member of the instance, calls the function with the instance pointer, and, after the call, copies every => output out of the instance into its target. Unpassed inputs keep their last value:

  %1 = getelementptr inbounds %Counter, ptr %counter, i32 0, i32 1
  store i32 5, ptr %1, align 4
  call void @Counter(ptr %counter)
  %2 = getelementptr inbounds %Counter, ptr %counter, i32 0, i32 2
  %3 = load i32, ptr %2, align 4
  store i32 %3, ptr %hits, align 4
  call void @Counter__double(ptr %counter)

A program call passes its global instance; an action call passes its owner’s instance. A function call places arguments in parameter order, uses defaults for omitted inputs, and passes addresses for by-reference parameters. The callee writes through those addresses:

  %load_total = load i32, ptr %total, align 4
  %call = call i32 @scale(i32 %load_total, i16 2, ptr %hits, ptr %flag)
  store i32 %call, ptr %hits, align 4

factor was not passed, so its default 2 appears as the literal i16 2. A method call is a function call with the instance in front: call i8 @Limits__exceeded(ptr %limits, i32 %load_hits). A call of an unqualified method or action inside a body takes the instance from the first argument of the running function.

Aggregate VAR_INPUT parameters (strings, arrays, structs) are passed as pointers even though they are by-value, and the callee copies the value into a local of its own; the Strings chapter shows the copy. VAR_INPUT {ref} skips the copy and makes the parameter a pointer that the body reads through.

Validation

Codegen only sees a POU that the validator accepted. The rules that concern the POU as a whole live in the POU validator. A program, function block, or class must not declare a return type (E026). A class must not declare VAR_INPUT, VAR_OUTPUT, or VAR_IN_OUT (E019) and must not have a body (E017). An ACTIONS block with no container name takes the POU above it; with no POU above it, the block is reported (E022). EXTENDS and IMPLEMENTS are allowed on classes and function blocks only (E110), the base and the interfaces must exist (E048), and an implementing method must match the declared signature (E112, E118).

Calls and bodies are checked in the statement validator. A call of a program, function block, or method must pass an argument for every VAR_IN_OUT (E030); a function call with too few arguments is reported by the argument count instead (E032). A reference to an action without the call parentheses is reported (E095), while a bare reference to a program is accepted. A method that names a VAR_TEMP of its owner is rejected (E137). Duplicate POU names are found by the global validation of the index (E004).

At a glance

Structured TextIndexAnnotation of a referenceLLVM
PROGRAM mainProgram entry, struct main, global main_instanceProgram%main, @main_instance, void @main(ptr)
FUNCTION_BLOCK CounterFunctionBlock entry, struct CounterVariable of type Counter%Counter, void @Counter(ptr)
CLASS LimitsClass entry, struct LimitsVariable of type Limits%Limits, empty void @Limits(ptr)
FUNCTION scale: DINTFunction entry, struct scale with return member scale.scaleFunction, return type DINTi32 @scale(...), no struct
METHOD resetMethod entry, struct Counter.reset for its parametersFunctionvoid @Counter__reset(ptr, ...)
ACTION doubleAction entry, parent’s structProgramvoid @Counter__double(ptr)
VAR_INPUT xByVal(Input) memberstruct field, or a by-value argument in a function
VAR_OUTPUT xByVal(Output) member, ByRef(Output) in a function or methodstruct field copied out after the call, or a pointer argument
VAR_IN_OUT xByRef(InOut) member of type __auto_pointer_to_Tauto_deref: Defaultptr field or argument, loaded before every access
VAR_TEMP xByVal(Temp) memberstack slot in the body and in each action, not a struct field