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

Codegen

Codegen turns the validated and fully lowered tree into LLVM IR, the input of the LLVM back end that produces machine code. In

PROGRAM main
    VAR
        sintVar: SINT;
        dintVar: DINT;
    END_VAR

    sintVar := dintVar;
END_PROGRAM

the assignment is a node with two references, an annotation that says dintVar is a DINT, and a hint that says the value must become a SINT. A processor cannot run that. Codegen turns it into three instructions:

%load_dintVar = load i32, ptr %dintVar        ; read dintVar
%1 = trunc i32 %load_dintVar to i8            ; cut it to 8 bits, the hint says SINT
store i8 %1, ptr %sintVar                     ; write sintVar

Both names became addresses inside the program’s instance structure, and the hint became the truncation between the load and the store.

flowchart LR
    parse[Parse] --> index[Index] --> annotate[Annotate] --> validate[Validate] --> codegen[Codegen] --> link[Link]
    style codegen fill:#bfdbfe,stroke:#000,stroke-width:1px,stroke-dasharray:4 3,color:#0f172a

The stage receives the annotated project and produces one LLVM module per compilation unit, in parallel. Each module is built from the unit, the global index, the annotation map, and two things the resolver collected for this purpose: the set of names the unit depends on and the string literals it contains. The finished module is written as an object file, or as textual IR or bitcode with --ir or --bc, and handed to the Linker. With --single-module, and always with -c, all units are merged into one module first.

The following project connects the mechanisms in this chapter. Each section shows the relevant source operation and trimmed LLVM IR. plc --ir writes the complete module.

TYPE Speed: (Slow, Fast); END_TYPE

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

VAR_GLOBAL
    counter: DINT := 7;
END_VAR

FUNCTION scale: DINT
    VAR_INPUT
        value: DINT;
        factor: INT;
    END_VAR
    VAR_IN_OUT
        total: DINT;
    END_VAR
    VAR_OUTPUT
        overflow: BOOL;
    END_VAR

    scale := value * factor;
    total := total + scale;
    overflow := scale > 100;
END_FUNCTION

FUNCTION_BLOCK Buffer
    VAR_INPUT
        limit: INT;
    END_VAR
    VAR
        count: DINT;
        values: ARRAY[0..3] OF DINT;
    END_VAR

    IF count < limit THEN
        values[count] := count;
        count := count + 1;
    END_IF
END_FUNCTION_BLOCK

PROGRAM main
    VAR
        bufferInstance: Buffer;
        i: DINT := 1;
        sintVar: SINT;
        text: STRING := 'hello';
        p: Point;
        speed: Speed;
        ptr: REF_TO DINT;
        flag: BOOL;
    END_VAR

    bufferInstance(limit := 5);
    i := scale(i, factor := 3, total := counter, overflow => flag);
    sintVar := i;
    text := 'world';
    p.x := i;
    ptr := REF(i);
    ptr^ := 2;
    CASE speed OF
        Slow: i := 0;
        Fast: i := 1;
    END_CASE
END_PROGRAM

Building a module

Codegen builds a module in dependency order: types, globals, function declarations, initializer and string constants, then function bodies. A global needs its type; a function body needs declarations for the functions it calls.

The global index describes declarations by name. A second index connects those names to LLVM types, values, and addresses. Each generation step adds entries for later steps:

pub struct LlvmTypedIndex<'ink> {
    /// Lookups that fail here continue in the parent index
    parent_index: Option<&'ink LlvmTypedIndex<'ink>>,

    /// Type name to LLVM type, for data types and for POU instance structs
    type_associations: FxHashMap<String, AnyTypeEnum<'ink>>,
    pou_type_associations: FxHashMap<String, AnyTypeEnum<'ink>>,

    /// Variable name to global variable
    global_values: FxHashMap<String, GlobalValue<'ink>>,

    /// Type or variable name to its constant initial value
    initial_value_associations: FxHashMap<String, BasicValueEnum<'ink>>,

    /// Qualified variable name to the address it lives at inside the current function
    loaded_variable_associations: FxHashMap<String, PointerValue<'ink>>,

    /// POU name to LLVM function
    implementations: FxHashMap<String, FunctionValue<'ink>>,

    /// String literal to the global constant that holds it
    utf08_literals: FxHashMap<String, GlobalValue<'ink>>,
    utf16_literals: FxHashMap<String, GlobalValue<'ink>>,

    // ... other omitted fields
}

The module index holds types, globals, and functions. Each function body has a child index for its variable addresses. A lookup continues in the parent when the child has no entry. Keys are case-insensitive, as in the global index.

Data types

Every type the unit depends on becomes an LLVM type:

Structured TextLLVM
BOOL, SINT, USINT, BYTEi8 (comparisons produce i1 and are widened)
INT, UINT, WORDi16
DINT, UDINT, DWORD, TIME, DATEi32
LINT, ULINT, LWORD, LTIME, LDATEi64
REAL, LREALfloat, double
STRING[n], WSTRING[n][n+1 x i8], [n+1 x i16], one slot for the terminator
ARRAY[a..b, c..d] OF T[len x T], all dimensions flattened into one length
STRUCT, FUNCTION_BLOCK, PROGRAM, CLASSa named struct with one field per member, in declaration order
POINTER TO, REF_TO, REFERENCE TOptr
enum, subrange, aliasthe LLVM type of the underlying type

For the example, Point, Buffer, and main become named structs; scale is a function and gets none, its variables live on the stack; Speed is an i32:

%Point  = type { i32, i32 }
%Buffer = type { ptr, i16, i32, [4 x i32] }
%main   = type { %Buffer, i32, i8, [81 x i8], %Point, i32, ptr, i8 }

The leading ptr of Buffer is the __vtable member that the polymorphism lowerer adds to every function block. main embeds its Buffer by value, and text is [81 x i8]: 80 characters plus the terminator.

Codegen first registers empty named structs, then fills their members. This permits pointers to the same struct or one declared later. Function return variables and temporary variables live on the stack, outside these layouts.

Codegen then computes a constant initial value for each type: a struct from the evaluated initializers of its members, an array or a string from its literal. One type’s initial value can depend on another’s, so the computation runs as a queue that stops when a full pass makes no progress.

Global variables

The dependency set decides which globals a module declares: every global variable and every program instance the unit refers to becomes an LLVM global. A variable declared in this unit gets its initial value, which is the evaluated initializer, otherwise the initial value of its type, otherwise zero:

@counter       = global i32 7
@main_instance = global %main { %Buffer zeroinitializer, i32 1, i8 0, [81 x i8] c"hello\00...", %Point zeroinitializer, i32 0, ptr null, i8 0 }
@Speed.Slow    = unnamed_addr constant i32 0
@Speed.Fast    = unnamed_addr constant i32 1

A program instance is named after the program with an _instance suffix and starts at the constant of its type; i := 1 and text := 'hello' are visible in it. An enum variant is a constant named by its qualified name. A variable declared in another file appears as @counter = external global i32, without a value, and the linker connects the two.

Functions

Structured Text separates stateless from stateful POUs, and codegen makes the split visible in every signature:

  • Stateless: a function. A call knows nothing of the call before it, so its variables live on the stack; the LLVM function takes the inputs as arguments and returns the result.
  • Stateful: a function block or program. Inputs, outputs, and locals are members of its instance struct, which survives between calls; the LLVM function takes one argument, a pointer to that instance, and returns nothing. Methods and actions are ordinary LLVM functions that take the instance pointer first; a method adds its own parameters after it.
define i32  @scale(i32 %0, i16 %1, ptr %2, ptr %3)   ; value, factor, total (in-out), overflow (output)
define void @Buffer(ptr %0)                           ; the instance
define void @main(ptr %0)                             ; the instance

In a function, VAR_IN_OUT and VAR_OUTPUT parameters are pointers, and an aggregate input (struct, array, string) is passed as a pointer and copied into a local inside the callee. In a stateful POU only VAR_IN_OUT is a pointer; an output and an aggregate input are ordinary members of the instance, which the caller fills before the call and reads after it.

Functions are created in two passes, like structs: first a declaration for every POU the unit depends on, including POUs from other units, then the bodies of the POUs declared in this unit. If scale were in a second file, the module of main would contain declare i32 @scale(i32, i16, ptr, ptr) with no body.

A body starts by making every variable addressable, so the statements can treat both kinds alike. A function copies each argument into a stack slot and starts its return variable at zero; a stateful POU computes one pointer per member into the instance:

define i32 @scale(i32 %0, i16 %1, ptr %2, ptr %3) {
entry:
  %scale = alloca i32                 ; stack slot for the return variable
  %value = alloca i32                 ; stack slot for value
  store i32 %0, ptr %value            ; copy the argument into it
  %factor = alloca i16
  store i16 %1, ptr %factor
  %total = alloca ptr                 ; in-out: the slot holds the caller's address
  store ptr %2, ptr %total
  %overflow = alloca ptr              ; output: same
  store ptr %3, ptr %overflow
  store i32 0, ptr %scale             ; the return variable starts at zero
  ...
  %scale_ret = load i32, ptr %scale   ; read the return variable
  ret i32 %scale_ret                  ; and return it
}

define void @Buffer(ptr %0) {
entry:
  %this = alloca ptr                                                     ; the instance pointer, for THIS
  store ptr %0, ptr %this
  %__vtable = getelementptr inbounds nuw %Buffer, ptr %0, i32 0, i32 0   ; address of member 0 in the instance
  %limit    = getelementptr inbounds nuw %Buffer, ptr %0, i32 0, i32 1   ; member 1
  %count    = getelementptr inbounds nuw %Buffer, ptr %0, i32 0, i32 2   ; member 2
  %values   = getelementptr inbounds nuw %Buffer, ptr %0, i32 0, i32 3   ; member 3
  ...
  ret void                                                               ; nothing to return
}

Every address is registered in the function’s child index under its qualified name (Buffer.count). A function ends by loading the variable that carries its own name and returning it.

Expressions

Two rules drive almost everything the expression generator does.

A reference is an address until a value is needed

p.x is one getelementptr per member step, values[count] a pointer computation into the array. On the left of an assignment the address is the target of a store; as an operand, its value is loaded. For p.x := i:

%x       = getelementptr inbounds nuw %Point, ptr %p, i32 0, i32 0   ; address of p.x: member 0 of p
%load_i2 = load i32, ptr %i                                          ; read i
store i32 %load_i2, ptr %x                                           ; write it to p.x

An array index becomes an offset first: the index minus the lower bound, times the stride of the dimension. For values[count] := count, with lower bound 0 and one dimension, both corrections do nothing and are still emitted:

%tmpVar2 = mul i32 1, %load_count                                              ; count times the stride (1)
%tmpVar3 = add i32 %tmpVar2, 0                                                 ; minus the lower bound (0)
%tmpVar4 = getelementptr inbounds [4 x i32], ptr %values, i32 0, i32 %tmpVar3  ; address of values[offset]
store i32 %load_count5, ptr %tmpVar4                                           ; write count there

Pointer variables that the resolver marked as auto-dereferencing (VAR_IN_OUT, REFERENCE TO) get one extra load. Inside scale, reading total loads the address from the stack slot first, then the value behind it. An explicit pointer works the same way without the marker: ptr := REF(i) stores the address of i, and ptr^ := 2 loads that address and stores through it:

%deref1     = load ptr, ptr %total     ; the address the caller passed
%load_total = load i32, ptr %deref1    ; the value behind it

store ptr %i, ptr %ptr                 ; ptr := REF(i)
%deref = load ptr, ptr %ptr            ; ptr^ := 2
store i32 2, ptr %deref

The type hint decides the conversion

Whenever a value flows into a place of a different type, codegen compares the annotated type with the hint and emits the cast: extension and truncation between integers, conversion between integers and floats, widening or narrowing between floats. In scale := value * factor, factor is an INT hinted to DINT; sintVar := i is the introduction’s truncation:

%4      = sext i16 %load_factor to i32   ; widen factor from INT to DINT
%tmpVar = mul i32 %load_value, %4        ; value * factor

%2 = trunc i32 %load_i1 to i8            ; cut i from DINT to SINT
store i8 %2, ptr %sintVar                ; write sintVar

Literals are created directly in the hinted type, so factor := 3 for an INT parameter produces i16 3 and no cast.

Binary and unary expressions become the matching integer or float instruction. Comparisons produce an i1, which is widened to i8 because that is the width of BOOL; where a branch needs a condition, the i8 is compared against zero to get an i1 back. For overflow := scale > 100:

%tmpVar5 = icmp sgt i32 %load_scale4, 100   ; scale > 100, as i1
%5       = zext i1 %tmpVar5 to i8           ; widen to the width of BOOL
store i8 %5, ptr %deref3                    ; write overflow through the output pointer

Codegen expects earlier stages to have resolved names and checked types. An unsupported expression stops the run with an internal codegen error.

Statements

Assignments

A single value (integer, real, pointer) is a store. An aggregate (struct, array, string) is a memcpy of the target’s size; for a string the copy stops at the target’s length, so a longer value is cut and never overflows. A string literal is a private global constant, created once per module. For text := 'world':

@utf08_literal_1 = private unnamed_addr constant [6 x i8] c"world\00"   ; the literal, once per module

call void @llvm.memcpy.p0.p0.i32(ptr align 1 %text, ptr align 1 @utf08_literal_1, i32 6, i1 false)   ; copy 6 bytes into text

Calls to functions

Codegen places named and positional arguments in parameter order and fills omitted parameters with defaults. It passes scalar inputs by value and VAR_IN_OUT and VAR_OUTPUT arguments by address. The callee writes through these addresses. For i := scale(i, factor := 3, total := counter, overflow => flag):

%call = call i32 @scale(i32 %load_i, i16 3, ptr @counter, ptr %flag)   ; value, factor, address of total, address of overflow
store i32 %call, ptr %i                                                ; write the result to i

Calls to function blocks and programs

Their inputs are members of the instance, so codegen stores every passed argument into the member, calls the POU with the instance pointer, and copies => outputs back out afterwards. For bufferInstance(limit := 5):

%1 = getelementptr inbounds %Buffer, ptr %bufferInstance, i32 0, i32 1   ; address of bufferInstance.limit
store i16 5, ptr %1                                                      ; limit := 5
call void @Buffer(ptr %bufferInstance)                                   ; run the body on the instance

Built-in functions such as ADR, REF, SIZEOF, or MUX have their own code generators; REF(i) above produced no call, only the address of i.

IF

IF becomes one block per branch and a continue block after them; the condition is narrowed to i1 and branched on. For the IF in Buffer:

  %3 = icmp ne i8 %2, 0                                 ; narrow the BOOL to i1
  br i1 %3, label %condition_body, label %continue      ; jump to the branch or past it

condition_body:
  ...
  br label %continue                                    ; join after the branch

continue:
  ret void

CASE

CASE becomes a switch for literal labels and a chain of comparisons for range labels. The enum variants are constants, so both labels end up in the switch:

  switch i32 %load_speed, label %else [   ; no label matched: ELSE (empty here)
    i32 0, label %case                    ; Slow
    i32 1, label %case3                   ; Fast
  ]

Loops

FOR, WHILE, and REPEAT all arrive as WHILE TRUE loops from the loop desugarer, with the exit conditions as ordinary IF ... EXIT statements in the body. Codegen emits only the skeleton, a while_body block that branches back to itself and a continue block after it; EXIT and CONTINUE become jumps to those two blocks.

Initialization

A constant initial value goes into the global instance directly, as shown under Global variables. Everything that cannot be a compile-time constant, such as a pointer to another variable or a nested function block, is the work of the constructor functions. The init participant created them before codegen as ordinary POUs with a __ctor suffix, one per type and one per unit, and codegen treats them like any other function. For the example (the real name of the unit constructor also carries a hash of the file path):

@llvm.global_ctors = appending global [1 x { i32, ptr, ptr }] [{ i32, ptr, ptr } { i32 65535, ptr @__unit_cg_st__ctor, ptr null }]   ; run before the program starts

define void @main__ctor(ptr %0) {
entry:
  ...
  call void @Buffer__ctor(ptr %bufferInstance)   ; construct the embedded function block
  store i32 1, ptr %i                            ; i := 1
  call void @llvm.memcpy.p0.p0.i32(ptr align 1 %text, ptr align 1 @utf08_literal_0, i32 6, i1 false)   ; text := 'hello'
  call void @Point__ctor(ptr %p)                 ; construct the struct member
  ...
  ret void
}

define void @__unit_cg_st__ctor() {
entry:
  store i32 7, ptr @counter                                          ; counter := 7
  call void @__vtable_Buffer__ctor(ptr @__vtable_Buffer_instance)   ; fill the method table
  call void @main__ctor(ptr @main_instance)                         ; construct the program instance
  ret void
}

main__ctor receives the instance, calls the constructor of the embedded Buffer, and stores the initial values of i and text. The unit constructor calls it for the global instance, and the llvm.global_ctors entry runs the unit constructor before the program starts.

Note

The unoptimized IR includes redundant work. It computes unused member pointers, repeats some initialization, and emits offset arithmetic such as multiplication by one. LLVM removes much of this at -O1 and above. Method table members remain part of function block layouts.

Output

For an object file, LLVM runs its optimization passes at the chosen -O level and emits machine code for the target triple; the output format and the --fpic and --fno-pic flags decide whether the code is position-independent. IR and bitcode are written as they are, without optimization. With -g, a debug builder runs next to the generators and attaches DWARF debug information: one entry per POU, one per member and local variable, and a source location per statement.

Where it lives

WhatWhere
Codegensrc/codegen.rs, src/codegen/

What’s next

The generated objects can refer to symbols defined in other files. The Linker resolves these references, adds libraries, and produces the requested artifact.