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

Strings

STRING uses one-byte storage units; WSTRING uses two-byte units. Both have fixed capacity plus one slot for a zero terminator. STRING[5] has five data slots, while an unsized STRING has 80. Assignment copies data within the target capacity and truncates longer values without a diagnostic. Parameter passing has a separate copy path, described below.

Strings use the array storage described in Arrays, with special rules for length and termination. The example shows literals, a shorter target, wide strings, and a function return:

VAR_GLOBAL CONSTANT
    SIZE: DINT := 5;
END_VAR

FUNCTION greet: STRING
    VAR_INPUT
        who: STRING;
    END_VAR

    greet := who;
END_FUNCTION

PROGRAM main
    VAR
        text: STRING;
        short: STRING[SIZE] := 'hi';
        wide: WSTRING[10];
    END_VAR

    text := 'hello';
    short := text;
    wide := "world";
    text := greet('bob');
END_PROGRAM

Declaration

The parser produces a string type node with two facts and no name: whether the type is wide, and the length expression if one was written. text: STRING is not such a node; it is a plain reference to the built-in type STRING.

Only STRING[SIZE] and WSTRING[10] are inline type definitions. Pre-processing at the start of the index stage moves them out into named types scoped to main, __main_short and __main_wide, and replaces the declaration with a reference to that name (see Index, Pre-processing). A string literal is a literal node with the text and a wide flag: 'hello' is narrow, "world" is wide.

Index

The type index holds one record per string type. Trimmed to the string variant of the type information:

String {
    /// Capacity in characters plus one for the terminator, as a literal or a constant expression
    size: TypeSize,

    /// Utf8 for STRING, Utf16 for WSTRING
    encoding: StringEncoding,

    /// Whether the source wrote a length; false for plain STRING and WSTRING
    declared_with_length: bool,
}

The stored size includes the terminator. Built-in strings have size 81, and WSTRING[10] has size 11. For STRING[SIZE], the index stores the expression SIZE + 1; constant evaluation resolves it to 6. The example therefore has four string types:

STRING          { size: 81,                encoding: Utf8,  declared_with_length: false }   built-in
WSTRING         { size: 81,                encoding: Utf16, declared_with_length: false }   built-in
__main_short    { size: SIZE + 1 -> 6,     encoding: Utf8,  declared_with_length: true }    from STRING[SIZE]
__main_wide     { size: 11,                encoding: Utf16, declared_with_length: true }    from WSTRING[10]

Variable entries store the string type name and any initializer ID. Here, main.text, greet.who, and the return variable use STRING; main.short uses __main_short. The type index supplies capacity and encoding.

Annotations

Each string literal gets a type named for its encoding and length. For 'hello', the resolver creates __STRING_5 with size 6 and later imports it into the global index. The target type becomes the hint used for copying. For the body of main:

    text := 'hello';
    ^^^^                     { kind: Variable, qualified_name: "main.text",  resulting_type: "STRING",       hint: None }
            ^^^^^^^          { kind: Value,                                  resulting_type: "__STRING_5",   hint: "STRING" }

    short := text;
    ^^^^^                    { kind: Variable, qualified_name: "main.short", resulting_type: "__main_short", hint: None }
             ^^^^            { kind: Variable, qualified_name: "main.text",  resulting_type: "STRING",       hint: "__main_short" }

    wide := "world";
    ^^^^                     { kind: Variable, qualified_name: "main.wide",  resulting_type: "__main_wide",  hint: None }
            ^^^^^^^          { kind: Value,                                  resulting_type: "__WSTRING_5",  hint: "__main_wide" }

    text := greet('bob');
    ^^^^                     { kind: Variable, qualified_name: "main.text",  resulting_type: "STRING",       hint: None }
            ^^^^^^^^^^^^     { kind: Value,                                  resulting_type: "STRING",       hint: "STRING" }
            ^^^^^            { kind: Function, qualified_name: "greet",      return_type: "STRING",          hint: None }
                  ^^^^^      { kind: Value,                                  resulting_type: "__STRING_3",   hint: Argument { resulting_type: "STRING", position: 0 } }

The resolver collects distinct literals from bodies so that codegen can emit private constants. The initializer 'hi' enters this collection after the init participant moves it into a constructor body.

For a comparison such as text = 'hello', the resolver attaches a replacement expression: STRING_EQUAL(text, 'hello'), or WSTRING_EQUAL for wide strings. It combines _EQUAL, _LESS, and _GREATER calls with NOT and OR for the other comparisons. The standard library supplies these functions; a missing one produces E073. See ReplacementAst.

Lowering

No participant rewrites strings themselves, but two rewrite the places they appear in. The aggregate-return lowerer turns greet into a void function with a VAR_IN_OUT greet: STRING parameter and gives the call site a temporary, so by codegen the last statement of main reads greet(__greet0, 'bob'); text := __greet0;. The init participant moves the initializer 'hi' out of the declaration into the constructor of main, as self.short := 'hi'. Codegen sees only assignments, calls, and pointer parameters.

Codegen

Layout

A string type becomes an array of its size, i8 for STRING and i16 for WSTRING. The program instance of the example is:

%main = type { [81 x i8], [6 x i8], [11 x i16] }

@main_instance = global %main { [81 x i8] zeroinitializer, [6 x i8] c"hi\00\00\00\00", [11 x i16] zeroinitializer }

The constant initializer 'hi' is written into the static data padded with zeros to the full size; the constructor copies it once more at start-up, like every constant initial value (see Init). Every literal collected by the resolver becomes a private constant sized to the literal, not to any target, with the terminator included:

@utf08_literal_0 = private unnamed_addr constant [4 x i8] c"bob\00"
@utf08_literal_1 = private unnamed_addr constant [6 x i8] c"hello\00"
@utf08_literal_2 = private unnamed_addr constant [3 x i8] c"hi\00"
@utf16_literal_0 = private unnamed_addr constant [6 x i16] [i16 119, i16 111, i16 114, i16 108, i16 100, i16 0]

Assignment

A string assignment uses memcpy. Its byte count is min(target size - 1, source size) multiplied by the storage-unit width. The copy leaves the target’s final terminator slot unchanged. The three assignments in main copy 6, 5, and 12 bytes:

  call void @llvm.memcpy.p0.p0.i32(ptr align 1 %text, ptr align 1 @utf08_literal_1, i32 6, i1 false)
  call void @llvm.memcpy.p0.p0.i32(ptr align 1 %short, ptr align 1 %text, i32 5, i1 false)
  call void @llvm.memcpy.p0.p0.i32(ptr align 2 %wide, ptr align 2 @utf16_literal_0, i32 12, i1 false)

short := text copies five bytes of a possibly 80-character value. If text holds 'hello', short receives hello and keeps its terminator in slot six; if it held a longer value, the value is cut without a diagnostic. A one-character literal assigned to a CHAR is the one exception to the copy rule: c := 'x' becomes store i8 120.

Passing and returning

A string argument is passed as a pointer. In the callee, a by-value VAR_INPUT gets a local array of the parameter’s size, which is zeroed and then filled from the pointer with a copy of the parameter’s size minus one character. The result travels the other way, through the in-out pointer the aggregate-return lowerer added: the caller provides a zeroed temporary of the return type and copies it into the target afterwards:

define void @greet(ptr %0, ptr %1) {
entry:
  %greet = alloca ptr, align 8
  store ptr %0, ptr %greet, align 8
  %who = alloca [81 x i8], align 4
  call void @llvm.memset.p0.i64(ptr align 1 %who, i8 0, i64 81, i1 false)
  call void @llvm.memcpy.p0.p0.i64(ptr align 1 %who, ptr align 1 %1, i64 80, i1 false)
  %deref = load ptr, ptr %greet, align 8
  call void @llvm.memcpy.p0.p0.i32(ptr align 1 %deref, ptr align 1 %who, i32 80, i1 false)
  ret void
}

define void @main(ptr %0) {
  ...
  %__greet0 = alloca [81 x i8], align 4
  call void @llvm.memset.p0.i64(ptr align 1 %__greet0, i8 0, i64 ptrtoint (ptr getelementptr ([81 x i8], ptr null, i32 1) to i64), i1 false)
  call void @greet(ptr %__greet0, ptr @utf08_literal_0)
  call void @llvm.memcpy.p0.p0.i32(ptr align 1 %text, ptr align 1 %__greet0, i32 80, i1 false)
  ...
}

This is also the C calling convention for the standard library: a C function that takes or returns a STRING takes char* parameters, with the result buffer first.

Comparison

The hidden call the resolver attached is what codegen emits. Both operands are pointers, the literal directly from its constant, and the result is the BOOL the library function returns:

  %call = call i8 @STRING_EQUAL(ptr %text, ptr @utf08_literal_1)
  store i8 %call, ptr %same, align 1

Everything else that works on strings, such as LEN, CONCAT, LEFT, or FIND, is a library function declared in Structured Text and reached through the generic lowerer; codegen has no string instructions beyond copy.

Validation

The validator rejects assignments between STRING and WSTRING (E037), and literals longer than one character assigned to CHAR (E065 and E037). Ordinary string assignments do not require equal lengths: short := 'far too long' stores far t without a diagnostic. Interface method signatures do require matching string lengths and encodings (E118).

At a glance

Structured TextIndexAnnotationLLVM
STRINGbuilt-in, size 81, Utf8STRING[81 x i8]
STRING[n]pre-processed type __<pou>_<var>, size n + 1, Utf8that type’s name[n+1 x i8]
WSTRING[n]pre-processed type, size n + 1, Utf16that type’s name[n+1 x i16]
'abc'none until the resolver runs__STRING_3, size 4, hinted to the targetprivate constant [4 x i8] c"abc\00"
"abc"none until the resolver runs__WSTRING_3, size 4, hinted to the targetprivate constant [4 x i16]
a := bb hinted to the type of amemcpy of min(size(a) - 1, size(b)) characters
f(s)f.s of type STRINGs hinted to STRINGptr, copied into a local of the parameter’s size
a = breplaced by a call to STRING_EQUALcall i8 @STRING_EQUAL(ptr, ptr)