Structured Text and Instruction List Compiler.
stc is an executable IEC 61131-3 Structured Text compiler front-end written in
Python. The current milestone turns the original grammar prototype into a
usable command-line tool that can parse ST files and emit an AST, C, or Rust for
the supported subset.
- IEC 61131-3 POU parsing for functions, function blocks, and programs.
- Instruction List (IL) parsing and shared C/Rust lowering: supported by the Edition 2 profile, diagnosed as deprecated in Edition 3, and rejected by the Edition 4 profile.
- Textual Sequential Function Charts (SFC) for the Edition 2, Edition 3, and Edition 4 profiles, including parallel branches, prioritized transitions, actions, indicators, and standard action qualifiers in both backends.
VAR_INPUTand function-localVARdeclarations.- Elementary types including
BOOL, signed/unsigned integers,REAL,LREAL, and bit-string integer families. - Derived type and declaration parsing for common arrays, structures, and strings in AST output.
- Integer, real, boolean, typed numeric literals such as
INT#10, strings, and date/time literals. - Assignments,
IF/ELSIF/ELSE,CASE,FOR,WHILE, andREPEAT. - Standard function call parsing for common IEC functions in AST output.
- C and Rust code generation for functions, programs, function blocks, and
standalone actions in the supported subset. Function-block instances expose
a cycle entry point (
_stepin C,stepin Rust); native C blocks retain their existing_setup/_loopABI. VAR_OUTPUTandVAR_IN_OUTlowering for functions and function blocks, plus standardEXITin generated loop bodies.- Target declarations for aliases, arrays, structures, strings, subranges, and enumerations when represented by the parser AST.
- JSON AST output for downstream tooling and regression tests.
- Minimal semantic checks for undeclared variables before code generation.
- Structural AST output for every parser production currently present in the grammar.
python3 -m stc examples/inter.st -g ast
python3 -m stc examples/inter.st -g c
python3 -m stc examples/inter.st -g rust
python3 -m stc examples/legacy.il -g c --std ed2 --language il
python3 -m stc examples/inter.st -g c -o build/inter.c
python3 -m stc examples/inter.st -g c --no-semantic-checkstc.cli is the compiler CLI entry point: -g c selects
CCodeGenerator, while -g rust selects RustCodeGenerator through the
shared compile_source() pipeline. The generated files contain IEC functions
as reusable C/Rust functions; they intentionally do not synthesize an
application main(), because a PLC runtime must decide how and when to invoke
the program cycle.
Use - or omit the source path to read from stdin.
Generated Rust includes source provenance, section comments, and Rustdoc for
translated IEC declarations. When rustfmt is installed, STC automatically
uses it to emit canonical Rust formatting; otherwise it keeps the backend's
readable built-in layout.
--language auto (the default) detects IL from the first executable line of
each POU. Use --language il when an IL body begins with a derived function
name and is therefore intentionally ambiguous with ST. Generated C or Rust is
always emitted as one self-contained source file.
Library clients can use the same result-based pipeline as the CLI:
from stc import compile_source
result = compile_source(source, "c", source_name="example.st")
if result.success:
c_source = result.output
else:
for diagnostic in result.diagnostics:
print(diagnostic.code, diagnostic.message)CompilationResult preserves the parse tree, AST, semantic context, source
map, diagnostics, and generated output when they are available. Syntax and
semantic failures are returned as data; invalid API usage and unexpected
compiler faults still raise exceptions.
The parser output is converted through an injectable AstBuilder. The current
AST is still dictionary-based, but it is detached from parser-owned objects so
typed nodes can replace it incrementally without changing the compilation API.
Add library search directories with -L and import either every export or one
selected symbol:
python3 -m stc application.st -g c -L ./libraries --import math
python3 -m stc application.st -g c -L ./libraries --import math:NativeAddEach library is described by stc-library.json:
{
"schema": 1,
"name": "math",
"exports": {
"NativeAdd": {
"source": "NativeAdd.st"
}
}
}Target-native implementations use matiec-style pragmas inside the IEC source.
A function uses a body section:
{#native c body}
NativeAdd = lhs + rhs;
{#end_native}
A function block uses Arduino-style setup and loop sections:
{#native c setup}
self->output_value = false;
{#end_native}
{#native c loop}
if (self->set_value) self->output_value = true;
{#end_native}
The C backend emits Block_setup(Block *self) and Block_loop(Block *self).
Native function code can access parameters, locals, and the generated return
variable by their IEC names. Function-block code accesses state through
self->field_name. A valid ST body remains in the declaration as a portable
fallback for targets without a matching native pragma.
Primitive functions are target-specific operations that the code generator
embeds only when they are referenced by the input program. Prefer a portable
Structured Text implementation in src/stc/stdlib/standard-functions.st when
the operation can be expressed in ST; use a primitive for target intrinsics or
runtime support that ST cannot provide.
To add a primitive:
-
If the function name is not already recognized as an IEC standard function, add it to
standard_functionsinsrc/stc/frontend/lexer.py. -
Add an implementation to each supported target file,
src/stc/runtime/primitives.candsrc/stc/runtime/primitives.rs. Enclose each implementation in matching marker comments (primitive names are normalized to uppercase):// STC_PRIMITIVE_BEGIN CLAMP MIN MAX #define CLAMP(lo, value, hi) (MAX((lo), MIN((value), (hi)))) // STC_PRIMITIVE_END CLAMP
The names after
CLAMPon the begin marker are dependencies. The loader includes them recursively, while theCOREblock is always included. Keep the begin and end names identical and keep the marker name equal to the Structured Text function name. -
Add coverage to
tests/test_primitives.py. Verify that requesting the new primitive includes its implementation and dependencies, omits unrelated blocks, and that generated C or Rust containing a call compiles when a compiler is available. -
Run the primitive tests, followed by the complete suite:
python3 -m unittest tests.test_primitives python3 -m unittest discover -s tests
The C generator already emits the common standard headers. If a primitive needs additional target imports or a new shared helper, add them to the backend preamble or to a dependency block rather than duplicating them in every primitive.
Syntax errors include the unexpected token, line/column, source line, and a caret:
stc: syntax error: unexpected token at line 6, column 9 near IDENTIFIER('broken')
broken := value_in;
^
python3 -m unittest discover -s testsInteresting Structured Text examples live under tests/fixtures/:
valid_ast/: syntax that must parse to JSON AST.valid_codegen/: syntax that must also emit C and Rust.invalid_semantic/: syntax that parses but must fail semantic code generation.
For example:
python3 -m stc tests/fixtures/valid_ast/case_and_for.st -g ast
python3 -m stc tests/fixtures/valid_codegen/typed_literals.st -g c
python3 -m stc tests/fixtures/invalid_semantic/undeclared_variable.st -g cThe current AST is structurally complete for every production currently present
in IECParser: no parser production is left as a placeholder. It is still a
dictionary-based parse tree, not yet a typed compiler IR. The next maturity step
is typed nodes with source spans, semantic symbols, and deterministic
diagnostics.
Use the parser coverage audit to track that work:
python3 tools/ast_coverage.py
python3 tools/ast_coverage.py --listWhen an Annex B text extraction is available at tmp/pdfs/annex_b.txt, the
parser can also be checked against the IEC 61131-3:2003 production names:
python3 tools/parser_doc_audit.py- Replace dict-based AST nodes with typed nodes carrying source spans.
- Add deterministic diagnostics with line/column ranges and recovery tests.
- Split parsing, semantic analysis, and backend generation into separate compiler phases.
- Implement a symbol table and type checker for functions, function blocks, programs, arrays, structs, direct variables, and configurations.
- Continue expanding grammar coverage for located declarations, array repeat initializers, positional calls, and broader standard function signatures.
- Add a compatibility corpus with accepted and rejected IEC 61131-3 programs.
- Add generated-code compile tests for C and Rust on CI.
- Add a runtime/library layer for standard IEC functions and function blocks.
- Add C++17 generation once the typed IR is stable.
- Add ST-level tests and an interactive execution/debugging loop.
- IEC 61131-3:2013, Programmable controllers, Part 3.
- Autonomy-Logic/STruCpp
- beremiz/matiec
During normal operation, the compiler hides known SLY/PLY warnings about unused tokens and productions, unreachable symbols, and grammar conflicts. Actual grammar construction errors remain enabled.
To enable the complete grammar audit and generate parser.out:
STC_PARSER_DIAGNOSTICS=1 python3 -m stc input.st -g cSemantic and syntax diagnostics include the file, line, column, source code, and a caret range:
example.st:13:9: error: Cannot assign ['REAL'] to ['INT']. [incompatible-assignment]
13 | b_1 := 10.5;
| ^~~~~~~~~~~
stc: 1 error generated.
Color is enabled automatically on interactive terminals and can be controlled explicitly:
python3 -m stc input.st -g c --diagnostic-color=always
python3 -m stc input.st -g c --diagnostic-color=neverThe C generator receives the semantic context and emits FOR loops and #line
directives so C compiler diagnostics can refer back to the Structured Text
source.
Semantic checks are organized under src/stc/semantic/checks/. Each check is a
small registered class with explicit phase and dependencies. See
docs/ADDING_SEMANTIC_CHECKS.md for a complete
example.