Skip to content

ce-bu/stm

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

STM — Structured Text Machine

A compiler toolchain and simulator for IEC 61131-3 Structured Text, the standard programming language for industrial PLCs. Written in Rust with a stack-based C virtual machine.

  .st source
      │
      ▼
 ┌──────────┐    ┌────────┐    ┌──────────┐
 │Preprocess│───▶│  Lexer │───▶│  Parser  │
 └──────────┘    └────────┘    └──────────┘
  INCLUDE &         tokens         AST
  pragmas                          │
                          ┌────────┴────────┐
                          ▼                 ▼
                    ┌──────────┐      ┌──────────┐
                    │ Compiler │      │ CodegenC │
                    └──────────┘      └──────────┘
                     bytecode            C source
                          │                 │
                          ▼                 ▼
                     .stbc binary      .c file (stcc)
                          │
                          ▼
                     ┌─────────┐
                     │  C VM   │
                     └─────────┘
                          │
                 ┌────────┴────────┐
                 ▼                 ▼
            CLI Simulator    TUI Debugger

Features

Language

  • Programs & FunctionsPROGRAM … END_PROGRAM, FUNCTION … END_FUNCTION
  • Data typesBOOL, INT, DINT, STRING
  • VariablesVAR, VAR_INPUT, VAR_OUTPUT, VAR_IN_OUT, VAR CONSTANT
  • StructsTYPE … STRUCT … END_STRUCT END_TYPE with dot-member access
  • Control flowIF/ELSIF/ELSE, CASE, FOR, WHILE, REPEAT/UNTIL
  • Operators — arithmetic (+, -, *, /, MOD, **), comparison, logical (AND, OR, XOR, NOT)
  • I/OGPIO_READ(), GPIO_WRITE(), EXT_READ(), EXT_WRITE(), TIME_MS()
  • Cyclic execution — program body is the scan cycle; both backends automatically loop and yield
  • Include filesINCLUDE 'path.st'; with cycle detection
  • Pragma directives{#pragma KEY VALUE} to customise generated code (see Pragmas)
  • Multi-arg PRINTPRINT('label:', value1, value2);
  • Comments(* block *), /* block */, // line

Toolchain

Binary Description
stm Compiler — .st.stbc bytecode
stcc C transpiler — .st.c source (PLC-style init/cycle)
sim CLI simulator — run for N cycles, dump variables
tui Interactive TUI debugger with live variable/GPIO editing

TUI Debugger

Four-panel terminal UI built with tui-realm:

TUI Debugger Screenshot

Panel Content
Watch Configurable variable watch list with labels & colors
Variables All VM variables with live values
GPIO Pin state with toggle support
Output PRINT output log

Key bindings:

Key Action
F5 Run continuously
F2 Pause
F8 Step one scan cycle
R Reset VM
Tab Switch panel focus
Shift+Tab Switch panel focus backwards
↑/↓ Navigate items
Space Toggle GPIO pin
Enter Edit variable value
Esc Cancel edit
F1 / ? Help overlay
Q Quit

Quick Start

# Build everything
cargo build --release

# Compile a program
cargo run --bin stm -- examples/tls.st -o tls.stbc

# Transpile to C
cargo run --bin stcc -- examples/tls.st -o tls.c --stubs

# Run in CLI simulator (3 cycles)
cargo run --bin sim -- examples/tls.st -n 3

# Run in TUI debugger with config
cargo run --bin tui -- examples/tls.st -c examples/tls.cfg

Example

{#pragma init_name tls_init}
{#pragma cycle_name tls_cycle}
{#pragma state_type TlsState}
{#pragma state_var tls}

INCLUDE 'utils.st';

TYPE MssSwitch : STRUCT
    mss : INT := 0;
    cnt : INT := 0;
END_STRUCT;
END_TYPE;

PROGRAM Main
VAR CONSTANT
    PIN_MSS1           : INT := 0;
    DEBOUNCE_THRESHOLD : INT := 3;
END_VAR
VAR
    sw1  : MssSwitch;
    mode : INT := 0;
END_VAR
    Debounce(PIN_MSS1, sw1.mss, sw1.cnt, DEBOUNCE_THRESHOLD
             => sw1.mss, sw1.cnt);
    mode := sw1.mss;
    PRINT('mode:', mode);
END_PROGRAM

The program body is the scan cycle — it is executed repeatedly by both backends automatically.

Simulator Config (.cfg)

Config files give GPIO pins friendly names and define a watch variable list with optional colors for the TUI:

[gpio]
0 = MSS1
1 = MSS2
2 = MSS3

[watch]
mode    = Operating Mode | cyan
cycle   = Scan Cycle     | yellow
sw1.mss = Switch 1 Value | green
sw1.cnt = Debounce Cnt 1 | gray

Available colors: red, green, blue, yellow, cyan, magenta, white, gray, light_red, light_green, light_blue, light_yellow, light_cyan, light_magenta.

CLI Options

stm (Compiler)

stm <input.st> [output.stbc]

sim (Simulator)

stm-sim [OPTIONS] <source>

Options:
  -c, --config <PATH>       Simulator config file (.cfg)
  -n, --cycles <N>          Number of scan cycles [default: 1000]
  -p, --pin <PIN=VALUE>     Pre-set a GPIO pin
  -e, --ext <ADDR:SIZE=VAL> Pre-set an external variable
  -v, --verbose             Print GPIO activity

stcc (C Transpiler)

stcc [OPTIONS] <source>

Options:
  -o, --output <PATH>       Output .c file path
  -s, --stubs               Generate stub implementations for runtime callbacks

tui (TUI Debugger)

stm-tui [OPTIONS] <source>

Options:
  -c, --config <PATH>       Simulator config file (.cfg)
  -p, --pin <PIN=VALUE>     Pre-set a GPIO pin
  -e, --ext <ADDR:SIZE=VAL> Pre-set an external variable

Pragmas

Pragma directives customise the C code generator output. Place them at the top of your .st file using the {#pragma KEY VALUE} syntax:

{#pragma init_name tls_init}
{#pragma cycle_name tls_cycle}
{#pragma state_type TlsState}
{#pragma state_var tls}
Pragma Default Description
init_name plc_init Name of the generated initialisation function
cycle_name plc_cycle Name of the generated scan-cycle function
state_type <Program>State Name of the state struct typedef
state_type_comment Trailing /* … */ comment on the state struct typedef
state_var s Name of the global state instance variable
state_var_comment Trailing /* … */ comment on the state variable declaration
comment_style inline Comment style for function parameters: inline (trailing /**< comments) or doxygen (@param block)
c_include Adds a #include directive to the generated C (repeatable)
c_header Injects raw C code after the includes (repeatable, supports block form)
stm_trace Route PRINT output through a custom function (see Tracing)

Pragmas are stripped during preprocessing and have no effect on the bytecode compiler.

Comment Preservation

Comments written in ST source are carried through to the generated C output. Use leading comments — a comment on the line immediately before a declaration — to annotate struct fields, variables, and function parameters:

TYPE MssSwitch : STRUCT
    (* Debounced stable value *)
    mss : INT := 0;
    (* Disagreement counter *)
    cnt : INT := 0;
END_STRUCT;
END_TYPE;

FUNCTION Debounce : INT
VAR_INPUT
    (* GPIO pin number to read *)
    pin : INT;
    (* Previous debounced stable value *)
    prev_stable : INT;
END_VAR

In the generated C the comments appear as:

  • Struct fields — trailing /**< … */ on the member line
  • Function parameters — trailing /**< … */ on the definition only (inline style, default) or a @param Doxygen block (doxygen style); forward declarations are kept comment-free
  • Local variables — trailing /**< … */ on the declaration line
  • Constants — trailing /**< … */ on the static const line
  • Function / program headers — block comment before the definition

Select the parameter comment style with {#pragma comment_style inline} or {#pragma comment_style doxygen}.

Type Overrides

The C code generator uses abstract type aliases so that the concrete integer widths can be changed without modifying generated code:

Alias Default C type Used for
StmBool int8_t BOOL values
StmShort int16_t
StmUShort uint16_t
StmLong int32_t INT values
StmULong uint32_t
StmLLong int64_t DINT values
StmULLong uint64_t

When no customisation is applied the generated C contains default typedefs guarded by #ifndef STM_TYPES_DEFINED, so they can be overridden in three ways:

1. Provide a custom header via c_include:

{#pragma c_include "my_stm_types.h"}

When c_include is present the default typedefs are not emitted — your header must define all Stm* types.

2. Inline C via c_header (single-line or block):

(* Single-line form — repeatable *)
{#pragma c_header #define STM_TYPES_DEFINED}
{#pragma c_header typedef int32_t StmBool;}

(* Block form *)
{#pragma c_header
#define STM_TYPES_DEFINED
typedef int32_t  StmBool;
typedef int32_t  StmLong;
typedef int64_t  StmLLong;
#pragma end}

3. From the C side — define the types before including the generated file:

#define STM_TYPES_DEFINED
typedef int32_t StmBool;
typedef int32_t StmLong;
// …
#include "tls.c"

Tracing

PRINT statements in ST are compiled to a single STM_PRINTF(...) macro call per statement. By default tracing is disabledSTM_PRINTF expands to a no-op unless you compile with -DSTM_TRACE:

/* Default behaviour (generated in the prelude): */
#ifndef STM_PRINTF
  #ifdef STM_TRACE
    #define STM_PRINTF(...) printf(__VA_ARGS__)
  #else
    #define STM_PRINTF(...) ((void)0)
  #endif
#endif

Three ways to enable or customise tracing:

1. Compile with -DSTM_TRACE — uses printf:

gcc -DSTM_TRACE -o app tls.c main.c

2. Use the stm_trace pragma — route all output through your own function directly from the ST source:

{#pragma stm_trace my_log}

This emits #define STM_PRINTF(...) my_log(__VA_ARGS__) — no #ifdef guard, your function is always called.

3. Define STM_PRINTF from the C side before including the generated code:

#define STM_PRINTF(...) my_logger(__VA_ARGS__)
#include "tls.c"

The #ifndef STM_PRINTF guard ensures the default is skipped.

Cyclic Execution Model

The PROGRAM body represents a single scan cycle. Each backend handles repetition automatically:

  • Bytecode VM — the compiler emits a Yield + Jump loop around the body so the VM yields after each cycle and the host (sim/TUI) drives the next call to vm_step().
  • C code generator — produces an init() function (variable setup) and a cycle() function (one scan), leaving the caller responsible for the main loop.

Architecture

src/
├── lib.rs              Library crate root
├── token.rs            Token enum
├── lexer.rs            Tokenizer (case-insensitive keywords)
├── ast.rs              AST node types
├── parser.rs           Recursive-descent parser
├── preprocess.rs       INCLUDE directive expansion
├── compiler.rs         AST → bytecode compiler
├── bytecode.rs         Bytecode instruction set & serialization
├── codegen_c.rs        AST → C code generator
├── ffi.rs              Rust ↔ C VM FFI bridge
├── simcfg.rs           .cfg file parser
├── main.rs             `stm` compiler binary
├── bin/
│   ├── stcc.rs         `stcc` C transpiler binary
│   ├── sim.rs          `sim` CLI simulator binary
│   └── tui/            `tui` TUI debugger binary
│       ├── main.rs         Entry point & event loop
│       ├── model.rs        App state & update logic
│       ├── vm.rs           SimState & VM callbacks
│       └── components/     UI panels
│           ├── watch.rs        Watch panel
│           ├── variables.rs    Variables panel
│           ├── gpio.rs         GPIO panel
│           ├── output.rs       Output panel
│           ├── status.rs       Status & help bars
│           └── help.rs         Help overlay
vm/
├── vm.c                Stack-based C virtual machine
└── vm.h                VM public API header

The Rust compiler (lexer → parser → compiler) produces a compact bytecode format. The C VM (vm/vm.c) executes it, linked into the Rust binaries via FFI using the cc build crate. Callbacks for GPIO, external memory, timers, and print output are trampolined from C back into Rust.

Building

Requires a C compiler (for the VM) and Rust 2021 edition.

cargo build            # debug build
cargo build --release  # optimised build
cargo test             # run all tests

License

ISC

About

No description, website, or topics provided.

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages