Integer & hexadecimal literals
Definition
Integer literals denote a single integer value. Four bases are defined by the standard:
| Base | Prefix | Example | Decimal value |
|---|---|---|---|
| Decimal | (none) | 42 | 42 |
| Binary | 2# | 2#1010 | 10 |
| Octal | 8# | 8#52 | 42 |
| Hexadecimal | 16# | 16#2A | 42 |
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 #:
| Form | Example | Type |
|---|---|---|
BYTE#<int> | BYTE#1 | BYTE (8-bit unsigned) |
WORD#<int> | WORD#16#FFFF | WORD (16-bit unsigned) |
DWORD#<int> | DWORD#16#DEAD_BEEF | DWORD (32-bit unsigned) |
LWORD#<int> | LWORD#0 | LWORD (64-bit unsigned) |
SINT#<int> | SINT#-1 | SINT (8-bit signed) |
INT#<int> | INT#-3 | INT (16-bit signed) |
DINT#<int> | DINT#100000 | DINT (32-bit signed) |
LINT#<int> | LINT#0 | LINT (64-bit signed) |
UINT#<int> | UINT#42 | UINT (16-bit unsigned) |
UDINT#<int> | UDINT#0 | UDINT (32-bit unsigned) |
ULINT#<int> | ULINT#0 | ULINT (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:
- Hex literal in CASE label (
16#01:etc.) → lexer tokenises16#as a type prefix and chokes on the bare01. Workaround: decimal labels. - Typed literal as function argument (
SHL(BYTE#1, x)) → matiec emitsdata__->BYTEinstead 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.