Integer & hexadecimal literals

Definition

Integer literals denote a single integer value. Four bases are defined by the standard:

BasePrefixExampleDecimal value
Decimal(none)4242
Binary2#2#101010
Octal8#8#5242
Hexadecimal16#16#2A42

Underscores between digits are allowed for readability and have no semantic effect: 1_000_000, 2#1010_1100. A leading + or - sign turns the literal into a signed value. Hexadecimal digits are case-insensitive (16#FF = 16#ff).

Typed literals

A type prefix forces the literal’s type, separated from the value by #:

FormExampleType
BYTE#<int>BYTE#1BYTE (8-bit unsigned)
WORD#<int>WORD#16#FFFFWORD (16-bit unsigned)
DWORD#<int>DWORD#16#DEAD_BEEFDWORD (32-bit unsigned)
LWORD#<int>LWORD#0LWORD (64-bit unsigned)
SINT#<int>SINT#-1SINT (8-bit signed)
INT#<int>INT#-3INT (16-bit signed)
DINT#<int>DINT#100000DINT (32-bit signed)
LINT#<int>LINT#0LINT (64-bit signed)
UINT#<int>UINT#42UINT (16-bit unsigned)
UDINT#<int>UDINT#0UDINT (32-bit unsigned)
ULINT#<int>ULINT#0ULINT (64-bit unsigned)

A typed literal can combine with any base: BYTE#16#FF, INT#2#1010, DWORD#8#777.

Syntax

integer_literal :=
      decimal_literal
    | "2#" binary_digits
    | "8#" octal_digits
    | "16#" hex_digits
    | type_name "#" integer_literal

decimal_literal := ['+' | '-'] decimal_digit { ['_'] decimal_digit }

Examples

iCount   : INT  := 42;            (* untyped, widens to INT *)
bMask    : BYTE := 16#FF;         (* hex literal → BYTE 255 *)
iSigned  : INT  := INT#-3;        (* typed literal *)
dwFlags  : DWORD := DWORD#16#DEAD_BEEF;
bPattern : BYTE := 2#1010_1100;   (* binary, underscore for readability *)

Semantics

An untyped integer literal has the type of the smallest standard integer type that can hold it, with promotion to match its surrounding expression. A typed literal always has exactly the type of its prefix; passing a typed literal to a parameter of a different type triggers an explicit-conversion error unless an implicit widening rule applies.

IEC reference

IEC 61131-3 third edition (2013), clause 6.3.1 — “Numeric literals”. Underscore-as-digit-separator is in 6.3.1 paragraph 3.

matiec conformance

matiec accepts all four bases and all typed-literal prefixes listed above. Two emit-bugs are documented in the llm_signals block above and are worth repeating here:

  1. Hex literal in CASE label (16#01: etc.) → lexer tokenises 16# as a type prefix and chokes on the bare 01. Workaround: decimal labels.
  2. Typed literal as function argument (SHL(BYTE#1, x)) → matiec emits data__->BYTE instead of an integer constant, failing g++ on the PLC. Workaround: pre-compute the value, or use an explicit conversion call (INT_TO_BYTE(1)).

Both quirks were observed and worked around in the Ackersteuerung-2026-05-14 cleanup; they are reproducible with the matiec version shipped in ForgeIEC at the time of writing. A future matiec++/rustly backend (see Memory: project_codegen_cxx_migration / project_rusty_evaluation) is expected to make these quirks go away.

ForgeIEC notes

The ForgeIEC editor’s tree-sitter highlighter recognises all prefixes in this table and colours them as constants. The auto-completion suggests INT_TO_BYTE and BYTE_TO_INT when it detects an obvious type-mismatch in a literal context.

When defining initial values via project.write.add_variable, prefer untyped literals (1, 2, 42) for primitive types — the editor’s pool computes the IEC type from the variable’s declared type, and an untyped literal will match without a prefix.