simaticml-decoder 0.1.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- simaticml_decoder/__init__.py +13 -0
- simaticml_decoder/cli.py +113 -0
- simaticml_decoder/emit.py +365 -0
- simaticml_decoder/fold.py +652 -0
- simaticml_decoder/instructions.py +105 -0
- simaticml_decoder/ir.py +182 -0
- simaticml_decoder/model.py +264 -0
- simaticml_decoder/operand.py +141 -0
- simaticml_decoder/parse.py +596 -0
- simaticml_decoder/scl_reconstruct.py +75 -0
- simaticml_decoder-0.1.0.dist-info/METADATA +118 -0
- simaticml_decoder-0.1.0.dist-info/RECORD +14 -0
- simaticml_decoder-0.1.0.dist-info/WHEEL +4 -0
- simaticml_decoder-0.1.0.dist-info/entry_points.txt +2 -0
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"""simaticml-decoder — SimaticML LAD/FBD -> readable SCL + JSON metadata sidecar.
|
|
2
|
+
|
|
3
|
+
Pipeline (three independently testable phases):
|
|
4
|
+
|
|
5
|
+
parse.py XML -> model.* (faithful, dumb mirror of the XML syntax)
|
|
6
|
+
fold.py model.* -> ir.* (semantics: boolean tree + assignments)
|
|
7
|
+
emit.py ir.* -> SCL text + JSON sidecar
|
|
8
|
+
|
|
9
|
+
Supporting modules: instructions.py (part catalog, data not logic),
|
|
10
|
+
operand.py (Access -> display string), scl_reconstruct.py (SCL networks).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
__version__ = "0.1.0"
|
simaticml_decoder/cli.py
ADDED
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
"""Command-line entry point: one exported SimaticML block in, SCL and/or JSON out.
|
|
2
|
+
|
|
3
|
+
v0 surface (one file at a time, by decision — batch is a deliberate later add):
|
|
4
|
+
|
|
5
|
+
simaticml-decode BLOCK.xml [-o OUTDIR] [--format {scl,json,both}] [-q]
|
|
6
|
+
|
|
7
|
+
The pipeline is the three phases in order:
|
|
8
|
+
|
|
9
|
+
parse.parse_file XML -> model.Document
|
|
10
|
+
fold.fold_block model.* -> ir.DecodedBlock
|
|
11
|
+
emit.emit_scl / emit.emit_sidecar -> .scl text + .json sidecar
|
|
12
|
+
|
|
13
|
+
Artifacts are written next to the input (or into ``--output``) as ``<stem>.scl``
|
|
14
|
+
and ``<stem>.json``. Warnings (deferred constructs, unknown instructions) are
|
|
15
|
+
reported on stderr but do not fail the run — the decoded output still surfaces
|
|
16
|
+
every unhandled part loudly, so a warning is informative, not fatal.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from __future__ import annotations
|
|
20
|
+
|
|
21
|
+
import argparse
|
|
22
|
+
import json
|
|
23
|
+
import sys
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
from xml.etree import ElementTree as ET
|
|
26
|
+
|
|
27
|
+
from . import __version__, emit, fold, parse
|
|
28
|
+
|
|
29
|
+
_EPILOG = """\
|
|
30
|
+
examples:
|
|
31
|
+
simaticml-decode Motor.xml # writes Motor.scl + Motor.json beside it
|
|
32
|
+
simaticml-decode Motor.xml -o out/ # writes into out/
|
|
33
|
+
simaticml-decode Motor.xml --format scl # SCL only
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
38
|
+
p = argparse.ArgumentParser(
|
|
39
|
+
prog="simaticml-decode",
|
|
40
|
+
description="Translate an exported SimaticML LAD/FBD block (TIA V21) into "
|
|
41
|
+
"readability-first SCL plus a JSON metadata sidecar.",
|
|
42
|
+
epilog=_EPILOG,
|
|
43
|
+
formatter_class=argparse.RawDescriptionHelpFormatter,
|
|
44
|
+
)
|
|
45
|
+
p.add_argument("input", metavar="BLOCK.xml",
|
|
46
|
+
help="path to a single exported SimaticML .xml block")
|
|
47
|
+
p.add_argument("-o", "--output", metavar="DIR",
|
|
48
|
+
help="output directory (default: alongside the input file)")
|
|
49
|
+
p.add_argument("--format", choices=("scl", "json", "both"), default="both",
|
|
50
|
+
help="which artifact(s) to write (default: both)")
|
|
51
|
+
p.add_argument("-q", "--quiet", action="store_true",
|
|
52
|
+
help="suppress the per-file progress/warning summary on stderr")
|
|
53
|
+
p.add_argument("--version", action="version", version=f"%(prog)s {__version__}")
|
|
54
|
+
return p
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
def main(argv: list[str] | None = None) -> int:
|
|
58
|
+
args = build_parser().parse_args(argv)
|
|
59
|
+
|
|
60
|
+
input_path = Path(args.input)
|
|
61
|
+
if not input_path.is_file():
|
|
62
|
+
print(f"error: input file not found: {input_path}", file=sys.stderr)
|
|
63
|
+
return 2
|
|
64
|
+
|
|
65
|
+
try:
|
|
66
|
+
doc = parse.parse_file(str(input_path))
|
|
67
|
+
except ET.ParseError as exc:
|
|
68
|
+
print(f"error: {input_path} is not well-formed XML: {exc}", file=sys.stderr)
|
|
69
|
+
return 1
|
|
70
|
+
except (OSError, ValueError) as exc:
|
|
71
|
+
print(f"error: failed to read {input_path}: {exc}", file=sys.stderr)
|
|
72
|
+
return 1
|
|
73
|
+
|
|
74
|
+
decoded = fold.fold_block(doc)
|
|
75
|
+
|
|
76
|
+
out_dir = Path(args.output) if args.output else input_path.parent
|
|
77
|
+
try:
|
|
78
|
+
out_dir.mkdir(parents=True, exist_ok=True)
|
|
79
|
+
except OSError as exc:
|
|
80
|
+
print(f"error: cannot create output directory {out_dir}: {exc}", file=sys.stderr)
|
|
81
|
+
return 1
|
|
82
|
+
|
|
83
|
+
stem = input_path.stem
|
|
84
|
+
written: list[Path] = []
|
|
85
|
+
if args.format in ("scl", "both"):
|
|
86
|
+
written.append(_write(out_dir / f"{stem}.scl", emit.emit_scl(decoded)))
|
|
87
|
+
if args.format in ("json", "both"):
|
|
88
|
+
sidecar = json.dumps(emit.emit_sidecar(decoded), indent=2, ensure_ascii=False)
|
|
89
|
+
written.append(_write(out_dir / f"{stem}.json", sidecar + "\n"))
|
|
90
|
+
|
|
91
|
+
if not args.quiet:
|
|
92
|
+
_report(decoded, written)
|
|
93
|
+
return 0
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def _write(path: Path, text: str) -> Path:
|
|
97
|
+
path.write_text(text, encoding="utf-8")
|
|
98
|
+
return path
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _report(decoded, written: list[Path]) -> None:
|
|
102
|
+
label = f"{decoded.name} ({decoded.kind})"
|
|
103
|
+
files = ", ".join(p.name for p in written)
|
|
104
|
+
print(f"decoded {label}: {len(decoded.networks)} network(s) -> {files}",
|
|
105
|
+
file=sys.stderr)
|
|
106
|
+
if decoded.warnings:
|
|
107
|
+
print(f" {len(decoded.warnings)} warning(s):", file=sys.stderr)
|
|
108
|
+
for warning in decoded.warnings:
|
|
109
|
+
print(f" - {warning}", file=sys.stderr)
|
|
110
|
+
|
|
111
|
+
|
|
112
|
+
if __name__ == "__main__":
|
|
113
|
+
sys.exit(main())
|
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
"""Phase 3: ir.* -> readable SCL text + JSON metadata sidecar.
|
|
2
|
+
|
|
3
|
+
Two artifacts (readability-first, NOT recompilable):
|
|
4
|
+
|
|
5
|
+
* SCL text — per network: a ``// Network N: <title>`` header, then the folded
|
|
6
|
+
statements rendered as SCL, with load-bearing constructs called out (latches,
|
|
7
|
+
edges, and any ir.Unhandled rendered as a visible ``// (!) UNHANDLED ...``
|
|
8
|
+
line). Edges render as ``R_TRIG(...)`` / ``F_TRIG(...)`` so the rising/falling
|
|
9
|
+
intent is explicit at the point of use.
|
|
10
|
+
* JSON sidecar — a single dict (schema in IMPLEMENTATION_PLAN.md §7):
|
|
11
|
+
{ "block": {name, kind},
|
|
12
|
+
"interface": [...sections/members with ground-truth types...],
|
|
13
|
+
"networks": [{index, title, language, warnings}],
|
|
14
|
+
"xref": { tag: [{network, role, uid}, ...] }, # write/read map
|
|
15
|
+
"instruction_inventory": { "Contact": 20, ... },
|
|
16
|
+
"warnings": [...],
|
|
17
|
+
"trace": { uid: "claim/location", ... } } # UId -> claim map
|
|
18
|
+
|
|
19
|
+
This module is a faithful *renderer* of the IR: fold owns the semantics and the
|
|
20
|
+
readability rewrites (factoring, latch detection); emit owns text formatting and
|
|
21
|
+
never alters logic. Operator knowledge (``+``, ``<``, ``:=`` ...) is reused from
|
|
22
|
+
the instruction catalog rather than duplicated here.
|
|
23
|
+
"""
|
|
24
|
+
|
|
25
|
+
from __future__ import annotations
|
|
26
|
+
|
|
27
|
+
from . import instructions, ir, model
|
|
28
|
+
|
|
29
|
+
_INDENT = " "
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
# --------------------------------------------------------------------------- #
|
|
33
|
+
# SCL text artifact #
|
|
34
|
+
# --------------------------------------------------------------------------- #
|
|
35
|
+
def emit_scl(decoded: ir.DecodedBlock) -> str:
|
|
36
|
+
"""Render the readable SCL text artifact for a decoded block."""
|
|
37
|
+
lines: list[str] = [f"// Block: {decoded.name} ({decoded.kind})"]
|
|
38
|
+
if decoded.warnings:
|
|
39
|
+
lines.append(f"// {len(decoded.warnings)} warning(s) — see JSON sidecar")
|
|
40
|
+
lines.append("")
|
|
41
|
+
|
|
42
|
+
for net in decoded.networks:
|
|
43
|
+
lines.extend(_render_network(net))
|
|
44
|
+
lines.append("")
|
|
45
|
+
|
|
46
|
+
return "\n".join(lines).rstrip() + "\n"
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
def _render_network(net: ir.NetworkLogic) -> list[str]:
|
|
50
|
+
title = f": {net.title}" if net.title else ""
|
|
51
|
+
out = [f"// Network {net.index}{title} [{net.language}]"]
|
|
52
|
+
if net.comment:
|
|
53
|
+
out.append(f"// {net.comment}")
|
|
54
|
+
for warning in net.warnings:
|
|
55
|
+
out.append(f"// (!) {warning}")
|
|
56
|
+
|
|
57
|
+
if net.scl_text is not None:
|
|
58
|
+
# Reconstructed SCL network — already textual, emit verbatim.
|
|
59
|
+
out.append(net.scl_text)
|
|
60
|
+
return out
|
|
61
|
+
|
|
62
|
+
if not net.statements:
|
|
63
|
+
out.append("// (empty network)")
|
|
64
|
+
return out
|
|
65
|
+
|
|
66
|
+
for stmt in net.statements:
|
|
67
|
+
out.extend(_render_statement(stmt))
|
|
68
|
+
return out
|
|
69
|
+
|
|
70
|
+
|
|
71
|
+
# --------------------------------------------------------------------------- #
|
|
72
|
+
# Statements #
|
|
73
|
+
# --------------------------------------------------------------------------- #
|
|
74
|
+
def _render_statement(stmt: ir.Statement) -> list[str]:
|
|
75
|
+
if isinstance(stmt, ir.Assign):
|
|
76
|
+
return _render_assign(stmt)
|
|
77
|
+
if isinstance(stmt, ir.FlipFlop):
|
|
78
|
+
return _render_flipflop(stmt)
|
|
79
|
+
if isinstance(stmt, ir.BoxCall):
|
|
80
|
+
return _render_box(stmt)
|
|
81
|
+
if isinstance(stmt, ir.UserCall):
|
|
82
|
+
return _render_user_call(stmt)
|
|
83
|
+
if isinstance(stmt, ir.Unhandled):
|
|
84
|
+
return [_unhandled_line(stmt)]
|
|
85
|
+
return [f"// (!) UNRENDERED statement {type(stmt).__name__}"]
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
def _render_assign(stmt: ir.Assign) -> list[str]:
|
|
89
|
+
target = stmt.target.name
|
|
90
|
+
value = _expr(stmt.value)
|
|
91
|
+
trailer = ""
|
|
92
|
+
if stmt.is_latch:
|
|
93
|
+
trailer = f" // seal-in latch: {stmt.note}" if stmt.note else " // seal-in latch"
|
|
94
|
+
elif stmt.note:
|
|
95
|
+
trailer = f" // {stmt.note}"
|
|
96
|
+
|
|
97
|
+
if stmt.kind is ir.AssignKind.NORMAL:
|
|
98
|
+
return [f"{target} := {value};{trailer}"]
|
|
99
|
+
if stmt.kind is ir.AssignKind.NEGATED:
|
|
100
|
+
neg = _expr(ir.Not(operand=stmt.value))
|
|
101
|
+
return [f"{target} := {neg};{trailer}"]
|
|
102
|
+
if stmt.kind is ir.AssignKind.SET:
|
|
103
|
+
return _guard_block(stmt.value, [f"{target} := TRUE;"], trailer, "set coil ( S )")
|
|
104
|
+
# RESET
|
|
105
|
+
return _guard_block(stmt.value, [f"{target} := FALSE;"], trailer, "reset coil ( R )")
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
def _render_flipflop(stmt: ir.FlipFlop) -> list[str]:
|
|
109
|
+
target = stmt.target.name
|
|
110
|
+
set_s = _expr(stmt.set_expr)
|
|
111
|
+
reset_s = _expr(stmt.reset_expr)
|
|
112
|
+
if stmt.reset_priority:
|
|
113
|
+
head = f"// RS flip-flop (reset priority): {target}"
|
|
114
|
+
first, first_val = reset_s, "FALSE"
|
|
115
|
+
second, second_val = set_s, "TRUE"
|
|
116
|
+
else:
|
|
117
|
+
head = f"// SR flip-flop (set priority): {target}"
|
|
118
|
+
first, first_val = set_s, "TRUE"
|
|
119
|
+
second, second_val = reset_s, "FALSE"
|
|
120
|
+
return [
|
|
121
|
+
head,
|
|
122
|
+
f"IF {first} THEN",
|
|
123
|
+
f"{_INDENT}{target} := {first_val};",
|
|
124
|
+
f"ELSIF {second} THEN",
|
|
125
|
+
f"{_INDENT}{target} := {second_val};",
|
|
126
|
+
"END_IF;",
|
|
127
|
+
]
|
|
128
|
+
|
|
129
|
+
|
|
130
|
+
def _render_box(stmt: ir.BoxCall) -> list[str]:
|
|
131
|
+
spec = instructions.lookup(stmt.instruction)
|
|
132
|
+
hint = spec.render if spec else None
|
|
133
|
+
body = _box_body(stmt, hint)
|
|
134
|
+
if not body:
|
|
135
|
+
# Nothing recognisable to render — surface loudly rather than drop it.
|
|
136
|
+
body = [f"// (!) UNHANDLED box {stmt.instruction} (UId {stmt.uid})"]
|
|
137
|
+
if stmt.enable is not None:
|
|
138
|
+
return _wrap_if(stmt.enable, body)
|
|
139
|
+
return body
|
|
140
|
+
|
|
141
|
+
|
|
142
|
+
def _box_body(stmt: ir.BoxCall, hint: str | None) -> list[str]:
|
|
143
|
+
if hint == ":=": # Move: dest := in;
|
|
144
|
+
src = _first(stmt.inputs, ("in", "in1"))
|
|
145
|
+
if src is not None and stmt.outputs:
|
|
146
|
+
return [f"{d.name} := {_expr(src)};" for d in stmt.outputs.values()]
|
|
147
|
+
elif hint in ("+", "-", "*", "/"): # Add/Sub/Mul/Div: dest := a <op> b;
|
|
148
|
+
operands = [stmt.inputs[k] for k in _sorted_pins(stmt.inputs)]
|
|
149
|
+
expr = f" {hint} ".join(_expr(o) for o in operands)
|
|
150
|
+
return [f"{d.name} := {expr};" for d in stmt.outputs.values()]
|
|
151
|
+
elif hint in ("+1", "-1"): # Inc/Dec: operand := operand +/- 1;
|
|
152
|
+
var = stmt.inputs.get("operand") or _first(stmt.inputs, ())
|
|
153
|
+
if var is not None:
|
|
154
|
+
name = _expr(var)
|
|
155
|
+
return [f"{name} := {name} {hint[0]} 1;"]
|
|
156
|
+
elif hint == "equation": # Calculate: dest := <equation>;
|
|
157
|
+
eq = stmt.inputs.get("__equation__")
|
|
158
|
+
if eq is not None and stmt.outputs:
|
|
159
|
+
return [f"{d.name} := {_expr(eq)};" for d in stmt.outputs.values()]
|
|
160
|
+
|
|
161
|
+
# Fallback / call-form boxes: timers (instance), system FCs, unknown boxes.
|
|
162
|
+
return _box_call_form(stmt)
|
|
163
|
+
|
|
164
|
+
|
|
165
|
+
def _box_call_form(stmt: ir.BoxCall) -> list[str]:
|
|
166
|
+
callee = stmt.instance or stmt.instruction
|
|
167
|
+
pairs = [f"{pin} := {_expr(e)}" for pin, e in stmt.inputs.items()
|
|
168
|
+
if pin != "__equation__"]
|
|
169
|
+
if stmt.instance is None:
|
|
170
|
+
# System FC: outputs are call parameters (OUT => dest).
|
|
171
|
+
pairs += [f"{pin} => {d.name}" for pin, d in stmt.outputs.items()]
|
|
172
|
+
lines = _format_call(callee, pairs)
|
|
173
|
+
if stmt.instance is not None:
|
|
174
|
+
# Instance box (timer): outputs read back off the instance member.
|
|
175
|
+
lines += [f"{d.name} := {stmt.instance}.{pin};"
|
|
176
|
+
for pin, d in stmt.outputs.items()]
|
|
177
|
+
return lines
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def _render_user_call(stmt: ir.UserCall) -> list[str]:
|
|
181
|
+
callee = stmt.instance or stmt.name
|
|
182
|
+
pairs = [f"{pin} := {_expr(val)}" for pin, val in stmt.params.items()]
|
|
183
|
+
lines = _format_call(callee, pairs)
|
|
184
|
+
if stmt.enable is not None:
|
|
185
|
+
return _wrap_if(stmt.enable, lines)
|
|
186
|
+
return lines
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
# --------------------------------------------------------------------------- #
|
|
190
|
+
# Expression rendering (SCL precedence: NOT > AND > OR; compares parenthesised #
|
|
191
|
+
# inside boolean context for readability — purely cosmetic, logic unchanged) #
|
|
192
|
+
# --------------------------------------------------------------------------- #
|
|
193
|
+
_WRAP = {
|
|
194
|
+
"top": frozenset(),
|
|
195
|
+
"not": frozenset({"and", "or", "cmp"}),
|
|
196
|
+
"and": frozenset({"or", "cmp"}),
|
|
197
|
+
"or": frozenset({"and", "cmp"}),
|
|
198
|
+
"cmp": frozenset({"and", "or", "not"}),
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
|
|
202
|
+
def _expr(expr: ir.Expr, ctx: str = "top") -> str:
|
|
203
|
+
text, kind = _expr_core(expr)
|
|
204
|
+
if kind in _WRAP[ctx]:
|
|
205
|
+
return f"({text})"
|
|
206
|
+
return text
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def _expr_core(expr: ir.Expr) -> tuple[str, str]:
|
|
210
|
+
if isinstance(expr, ir.VarRef):
|
|
211
|
+
return expr.name, "var"
|
|
212
|
+
if isinstance(expr, ir.Literal):
|
|
213
|
+
return expr.value, "lit"
|
|
214
|
+
if isinstance(expr, ir.Not):
|
|
215
|
+
return f"NOT {_expr(expr.operand, 'not')}", "not"
|
|
216
|
+
if isinstance(expr, ir.And):
|
|
217
|
+
return " AND ".join(_expr(o, "and") for o in expr.operands), "and"
|
|
218
|
+
if isinstance(expr, ir.Or):
|
|
219
|
+
return " OR ".join(_expr(o, "or") for o in expr.operands), "or"
|
|
220
|
+
if isinstance(expr, ir.Compare):
|
|
221
|
+
return f"{_expr(expr.left, 'cmp')} {expr.op} {_expr(expr.right, 'cmp')}", "cmp"
|
|
222
|
+
if isinstance(expr, ir.Edge):
|
|
223
|
+
fn = "R_TRIG" if expr.kind is ir.EdgeKind.RISING else "F_TRIG"
|
|
224
|
+
return f"{fn}({_expr(expr.signal)})", "edge"
|
|
225
|
+
if isinstance(expr, ir.RawExpr):
|
|
226
|
+
return expr.text, "raw"
|
|
227
|
+
if isinstance(expr, ir.Unhandled):
|
|
228
|
+
return f"(* (!) UNHANDLED {expr.part_name} (UId {expr.uid}) *)", "unhandled"
|
|
229
|
+
return repr(expr), "unhandled"
|
|
230
|
+
|
|
231
|
+
|
|
232
|
+
# --------------------------------------------------------------------------- #
|
|
233
|
+
# Small rendering helpers #
|
|
234
|
+
# --------------------------------------------------------------------------- #
|
|
235
|
+
def _wrap_if(cond: ir.Expr, body: list[str]) -> list[str]:
|
|
236
|
+
out = [f"IF {_expr(cond)} THEN"]
|
|
237
|
+
out += [f"{_INDENT}{line}" for line in body]
|
|
238
|
+
out.append("END_IF;")
|
|
239
|
+
return out
|
|
240
|
+
|
|
241
|
+
|
|
242
|
+
def _guard_block(cond: ir.Expr, body: list[str], trailer: str, label: str) -> list[str]:
|
|
243
|
+
lines = _wrap_if(cond, body)
|
|
244
|
+
lines[0] = f"{lines[0]}{trailer}" if trailer else f"{lines[0]} // {label}"
|
|
245
|
+
return lines
|
|
246
|
+
|
|
247
|
+
|
|
248
|
+
def _format_call(callee: str, pairs: list[str]) -> list[str]:
|
|
249
|
+
if not pairs:
|
|
250
|
+
return [f"{callee}();"]
|
|
251
|
+
if len(pairs) == 1:
|
|
252
|
+
return [f"{callee}({pairs[0]});"]
|
|
253
|
+
prefix = f"{callee}("
|
|
254
|
+
pad = " " * len(prefix)
|
|
255
|
+
lines = [f"{prefix}{pairs[0]},"]
|
|
256
|
+
lines += [f"{pad}{p}," for p in pairs[1:-1]]
|
|
257
|
+
lines.append(f"{pad}{pairs[-1]});")
|
|
258
|
+
return lines
|
|
259
|
+
|
|
260
|
+
|
|
261
|
+
def _first(inputs: dict[str, ir.Expr], preferred: tuple[str, ...]) -> ir.Expr | None:
|
|
262
|
+
for pin in preferred:
|
|
263
|
+
if pin in inputs:
|
|
264
|
+
return inputs[pin]
|
|
265
|
+
for pin, expr in inputs.items():
|
|
266
|
+
if pin != "__equation__":
|
|
267
|
+
return expr
|
|
268
|
+
return None
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
def _sorted_pins(inputs: dict[str, ir.Expr]) -> list[str]:
|
|
272
|
+
pins = [p for p in inputs if p != "__equation__"]
|
|
273
|
+
return sorted(pins, key=_pin_sort_key)
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def _pin_sort_key(pin: str) -> tuple[str, int]:
|
|
277
|
+
i = len(pin)
|
|
278
|
+
while i > 0 and pin[i - 1].isdigit():
|
|
279
|
+
i -= 1
|
|
280
|
+
return (pin[:i], int(pin[i:]) if pin[i:] else -1)
|
|
281
|
+
|
|
282
|
+
|
|
283
|
+
def _unhandled_line(stmt: ir.Unhandled) -> str:
|
|
284
|
+
note = f" - {stmt.note}" if stmt.note else ""
|
|
285
|
+
return f"// (!) UNHANDLED {stmt.part_name} (UId {stmt.uid}){note}"
|
|
286
|
+
|
|
287
|
+
|
|
288
|
+
# --------------------------------------------------------------------------- #
|
|
289
|
+
# JSON sidecar artifact #
|
|
290
|
+
# --------------------------------------------------------------------------- #
|
|
291
|
+
def emit_sidecar(decoded: ir.DecodedBlock) -> dict:
|
|
292
|
+
"""Build the JSON-serialisable sidecar dict (schema in plan §7)."""
|
|
293
|
+
return {
|
|
294
|
+
"block": {"name": decoded.name, "kind": decoded.kind},
|
|
295
|
+
"interface": _interface_json(decoded.interface),
|
|
296
|
+
"networks": [
|
|
297
|
+
{
|
|
298
|
+
"index": net.index,
|
|
299
|
+
"title": net.title,
|
|
300
|
+
"language": net.language,
|
|
301
|
+
"warnings": list(net.warnings),
|
|
302
|
+
}
|
|
303
|
+
for net in decoded.networks
|
|
304
|
+
],
|
|
305
|
+
"xref": {
|
|
306
|
+
tag: [
|
|
307
|
+
{"network": ref.network_index, "role": ref.role, "uid": ref.uid}
|
|
308
|
+
for ref in refs
|
|
309
|
+
]
|
|
310
|
+
for tag, refs in decoded.xref.items()
|
|
311
|
+
},
|
|
312
|
+
"instruction_inventory": dict(decoded.instruction_inventory),
|
|
313
|
+
"warnings": list(decoded.warnings),
|
|
314
|
+
"trace": _build_trace(decoded),
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
|
|
318
|
+
def _interface_json(interface: object) -> list[dict]:
|
|
319
|
+
if not isinstance(interface, model.Interface):
|
|
320
|
+
return []
|
|
321
|
+
return [
|
|
322
|
+
{"name": section.name, "members": [_member_json(m) for m in section.members]}
|
|
323
|
+
for section in interface.sections
|
|
324
|
+
]
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
def _member_json(member: model.Member) -> dict:
|
|
328
|
+
out: dict = {"name": member.name, "datatype": member.datatype}
|
|
329
|
+
if member.is_udt:
|
|
330
|
+
out["is_udt"] = True
|
|
331
|
+
for attr in ("version", "start_value", "remanence", "comment"):
|
|
332
|
+
value = getattr(member, attr)
|
|
333
|
+
if value is not None:
|
|
334
|
+
out[attr] = value
|
|
335
|
+
if member.children:
|
|
336
|
+
out["children"] = [_member_json(c) for c in member.children]
|
|
337
|
+
return out
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
def _build_trace(decoded: ir.DecodedBlock) -> dict[str, str]:
|
|
341
|
+
"""UId -> short claim, so any rendered statement is traceable to its net."""
|
|
342
|
+
trace: dict[str, str] = {}
|
|
343
|
+
for net in decoded.networks:
|
|
344
|
+
for stmt in net.statements:
|
|
345
|
+
uid = getattr(stmt, "uid", None)
|
|
346
|
+
if uid is None:
|
|
347
|
+
continue
|
|
348
|
+
trace[uid] = f"Network {net.index}: {_claim(stmt)}"
|
|
349
|
+
return trace
|
|
350
|
+
|
|
351
|
+
|
|
352
|
+
def _claim(stmt: ir.Statement) -> str:
|
|
353
|
+
if isinstance(stmt, ir.Assign):
|
|
354
|
+
return f"{stmt.kind.value} assign {stmt.target.name}"
|
|
355
|
+
if isinstance(stmt, ir.FlipFlop):
|
|
356
|
+
kind = "Rs" if stmt.reset_priority else "Sr"
|
|
357
|
+
return f"{kind} flip-flop {stmt.target.name}"
|
|
358
|
+
if isinstance(stmt, ir.BoxCall):
|
|
359
|
+
inst = f" {stmt.instance}" if stmt.instance else ""
|
|
360
|
+
return f"box {stmt.instruction}{inst}"
|
|
361
|
+
if isinstance(stmt, ir.UserCall):
|
|
362
|
+
return f"call {stmt.block_type} {stmt.name}"
|
|
363
|
+
if isinstance(stmt, ir.Unhandled):
|
|
364
|
+
return f"UNHANDLED {stmt.part_name}"
|
|
365
|
+
return type(stmt).__name__
|