⚠️ Proof of Concept: This emulator is an experimental prototype and not production-ready. APIs, behaviors, and grammar support may change without notice. Use for evaluation and feedback only.
This package wraps the Siemens SCL ANTLR grammar and exposes a strict TypeScript helper for building an abstract syntax tree (AST) from SCL source text. Tooling relies entirely on Node.js (via antlr-ng and the antlr4ng runtime), so no local Java installation is required.
- SCL parsing:
parseSclproduces a richly typed AST with source ranges for downstream analysis. - PLC state modelling:
createPlcStatesimulates S7 memory areas, optimized data blocks, and type-safe read/write helpers. - Deterministic execution:
executeSclProgrambuilds an intermediate representation (IR) and evaluates it against the PLC state in a single scan. - Diagnostics and tracing: runtime errors carry precise source locations, while optional tracing surfaces each statement's side effects.
- Tooling pipeline: reproducible Nix shell, codegen scripts, lint/test workflows, and modular docs under
docs/andspecs/.
flowchart LR
Source[[SCL source]] --> Parser[parseScl]
Parser --> Ast[SCL AST]
Ast --> IrBuilder[IR builder]
IrBuilder --> Interpreter[Execution engine]
Interpreter --> PlcState[PLC state simulator]
PlcState --> Snapshots[Snapshots & observers]
The parser is intentionally decoupled from the execution engine so tooling can inspect or transform the AST prior to evaluation. The PLC state simulator underpins both automated tests and interactive tooling.
sequenceDiagram
participant Dev as Developer
participant Parser as parseScl
participant Emulator as executeSclProgram
participant PLC as PlcState
participant Trace as Trace stream
Dev->>Parser: Submit SCL source
Parser-->>Dev: Typed AST
Dev->>Emulator: AST + PlcState + options
Emulator->>PLC: Resolve symbols & types
Emulator->>PLC: Apply IR instructions
PLC-->>Emulator: Memory effects
Emulator-->>Trace: Optional statement events
Emulator-->>Dev: Result snapshot + trace
Key extension points include custom PLC initialisation, symbol binding, loop guards, and observers that respond to state mutations.
src/parser/— ANTLR-backed lexer/parser utilities plus AST typings and error helpers.src/emulator/— IR builder, interpreter, error taxonomy, and execution/tracing APIs.src/plc/— In-memory Siemens S7 state model, observers, snapshotting, and diff helpers.src/index.ts— Public barrel exporting parser, emulator, and PLC primitives.Siemens-SCL-Antlr-Grammar/— Upstream grammar tracked as a git submodule for regeneration.docs/&specs/— Architecture notes, emulator walk-throughs, and feature specifications.
- Parse SCL source into an AST with
parseScl. - Initialise a PLC state via
createPlcState, sizing whichever memory areas your program touches and describing any optimized data blocks. - Execute the program by passing the AST, PLC state, and symbol bindings into
executeSclProgram. Bindings map SCL variable names to PLC addresses (direct such asM0.0or data-block paths such asProgramState.count), and optional flags surface tracing or tighten safety guards.
import {
analyzeFbSchema,
createPlcState,
executeSclProgram,
parseScl,
} from "scl-emulator";
const source = `
FUNCTION_BLOCK Toggle
VAR
toggleFlag : BOOL;
END_VAR
BEGIN
toggleFlag := NOT toggleFlag;
M0.0 := toggleFlag;
END_FUNCTION_BLOCK
`;
const schemaAst = parseScl(`
FUNCTION_BLOCK ProgramState
VAR
toggleFlag : BOOL := FALSE;
END_VAR
END_FUNCTION_BLOCK
`);
const schema = analyzeFbSchema(schemaAst);
const ast = parseScl(source);
const state = createPlcState({
flags: { size: 1 },
optimizedDataBlocks: {
instances: [{ name: "ProgramState", type: "ProgramState" }],
schema,
},
});
const result = executeSclProgram(ast, state, {
trace: true,
maxLoopIterations: 100,
symbols: {
toggleFlag: "ProgramState.toggleFlag",
},
});
const flag = state.readBool("M0.0"); // => { ok: true, value: true }
const snapshot = result.snapshot.dbSymbols["ProgramState.toggleFlag"]; // => BOOL write traceVitest coverage in tests/emulator/executeSclProgram.spec.ts demonstrates additional patterns:
- iterative control flow (
WHILE,FOR,EXIT,CONTINUE) withmaxLoopIterationsguarding infinite loops - CASE selectors (single values and ranges) targeting outputs like
QB0 - automatic declaration initialisation and runtime snapshots via
result.tracewhentrace: true - explicit error surfaces:
SclEmulatorBuildErrorfor unsupported statements andSclEmulatorRuntimeErrorwhen bindings or control-flow usage are invalid
Borrow the fixture in tests/fixtures/dbDefinitions/emulator.ts as a reference shape for deriving optimized data blocks directly from SCL sources via analyzeFbSchema when modelling richer programs.
- Enter the reproducible environment (run
./shell.shdirectly or inside./scripts/run-nix-container.sh). The shell provides Node.js 22 LTS, pnpm, Vitest, and Playwright. - Install dependencies with
pnpm install. This pullsantlr-ngand exposes its CLI fromnode_modules/.binfor local use. - Regenerate the lexer/parser artifacts when the grammar changes by running
pnpm antlr:generate. You usually do not need to call this manually -pnpm build,pnpm test, andpnpm lintinvoke it via theirpre*hooks so fresh artifacts exist automatically.
Generated files land in src/generated/ and are intentionally ignored by git. Regenerate after updating Siemens-SCL-Antlr-Grammar/scl.g4 or when the grammar submodule is rebased.
pnpm build— regenerates the ANTLR output and compiles TypeScript todist/pnpm test— regenerates the parser and executes the Vitest suitepnpm lint— runs ESLint with TypeScript-aware rules
Refer to 01-SPEC-scl-parser for implementation details and acceptance criteria.