STAR
Cal STAR — firmware, DAQ server, and engine design
Loading...
Searching...
No Matches
StateScript.hpp
Go to the documentation of this file.
1#pragma once
2
3#include <cstdint>
4#include <map>
5#include <set>
6#include <string>
7#include <vector>
8
9/**
10 * The state-script language: a small Python-looking language an operator writes in the config UI,
11 * which the sequencer runs on entry to a dynamic state.
12 *
13 * ── What this is, and what it deliberately is not ─────────────────────────────────────────────
14 *
15 * A script never becomes a C++ function. It is parsed once, at sequencer startup, into the flat
16 * arena below, and an interpreter walks that arena. There is no code generation, no dlopen, no
17 * eval, and no compiler anywhere near the stand.
18 *
19 * That is not merely the easier option, it is the correct one. Because the program is data being
20 * walked, the runner can check a stop flag, a wall deadline and an iteration budget between any
21 * two statements — which is what lets an abort preempt a running script at a known boundary and
22 * what makes a runaway loop bounded. Compiled code offers no such seam.
23 *
24 * ── Names are slugs, resolved positionally ───────────────────────────────────────────────────
25 *
26 * `open_valve(GSE_HIGH_PRESS_CONTROL)`, not `open_valve("GSE High Press Control")`. The slug is
27 * the canonical config name uppercased with whitespace collapsed to underscores.
28 *
29 * The namespace comes from the ARGUMENT SLOT, never from the name itself: the argument of
30 * open_valve/close_valve is an actuator role, of pressure() a PT sensor role, of transition_to()
31 * a state. An identifier anywhere else is a variable.
32 *
33 * That is not a style choice. On the shipped `server` profile FUEL_PRESS and FUEL_VENT are each
34 * BOTH a valve and a state, and on `digital-twin` FUEL_UPSTREAM is both a valve and a PT sensor.
35 * A flat namespace would be ambiguous against config that exists today. Positional resolution
36 * also means collisions WITHIN a namespace are an error while collisions ACROSS namespaces are
37 * fine and expected — a blanket collision rule would refuse this rig's live config.
38 *
39 * ── Everything checkable at parse time is checked at parse time ──────────────────────────────
40 *
41 * A script that cannot be proven bounded, or that names something config does not define, is
42 * refused at load. The state then never becomes enterable, so the failure is an unavailable
43 * button rather than a valve opening on a script that was going to fail anyway.
44 */
45namespace fsw {
46namespace script {
47
48// ─────────────────────────────────────────────────────────────────────────────────────────────
49// Resource caps
50//
51// These bound the program, not the rig. The rig's bound is the per-state script_timeout_ms, which
52// is mandatory and enforced separately. These exist so that an unbounded script is refused before
53// it can run at all, rather than discovered mid-run with a valve open.
54// ─────────────────────────────────────────────────────────────────────────────────────────────
55inline constexpr size_t kMaxSourceBytes = 4096;
56inline constexpr uint32_t kMaxLines = 200;
57inline constexpr uint32_t kMaxStatements = 500;
58inline constexpr uint32_t kMaxNestingDepth = 8;
59inline constexpr uint32_t kMaxVariables = 32;
60inline constexpr uint32_t kMaxExprDepth = 32;
61/** Widest indent unit accepted. The first indented line sets the unit; every later indent must be
62 * an exact multiple of it, which kills "3 spaces here, 4 there" without a stack of columns. */
63inline constexpr uint32_t kMaxIndentUnit = 8;
64
65/** Which table a slug is resolved against. Determined by argument position, never by spelling. */
66enum class Ns : uint8_t {
67 Actuator, // open_valve / close_valve
68 PtSensor, // pressure
69 State, // transition_to
70};
71
72const char* ns_name(Ns ns);
73
74/** One slug reference, with its source span so a rename can rewrite exactly this token. */
75struct SlugRef {
76 std::string slug;
78 uint32_t line = 0; // 1-based
79 uint32_t col = 0; // 1-based
80 uint32_t len = 0;
81};
82
92
93enum class ExprKind : uint8_t {
98 Unary, // op: Neg, Not
99 Binary, // op: arithmetic, comparison, And, Or
100};
101
118
119const char* op_name(Op op);
120
121struct Stmt {
123 uint32_t line = 0;
124 uint32_t col = 0;
125 /** Assign: index into ScriptProgram::variables. */
126 int32_t var = -1;
127 /** Assign value, Delay seconds, If/While condition. Index into ScriptProgram::exprs. */
128 int32_t expr = -1;
129 /** OpenValve/CloseValve/TransitionTo target. Index into ScriptProgram::slugs. */
130 int32_t slug = -1;
131 /** If/While body, and If's else arm: [begin, end) into ScriptProgram::block_items. */
132 int32_t body_begin = -1;
133 int32_t body_end = -1;
134 int32_t else_begin = -1;
135 int32_t else_end = -1;
136};
137
138struct Expr {
140 uint32_t line = 0;
141 uint32_t col = 0;
142 double number = 0.0; // Number
143 int32_t var = -1; // Var -> variables index
144 int32_t slug = -1; // Pressure -> slugs index
145 Op op = Op::Add; // Unary / Binary
146 int32_t lhs = -1;
147 int32_t rhs = -1; // Binary only
148};
149
150/**
151 * A parsed script, as a flat arena.
152 *
153 * Children are int32_t indices, never pointers, and -1 means none. This is deliberate: the whole
154 * program copies by value into ScriptRunner::start() with no ownership question — matching
155 * HoldSpec's "a number that is passed in cannot go stale" discipline — and every size cap becomes
156 * a vector size check rather than a tree walk.
157 */
159 std::vector<Stmt> stmts;
160 std::vector<Expr> exprs;
161 /** Statement indices, grouped into contiguous [begin, end) block ranges. */
162 std::vector<int32_t> block_items;
163 int32_t top_begin = 0;
164 int32_t top_end = 0;
165 /** Every slug reference in SOURCE ORDER, with spans. The rename path rewrites these; the
166 * pressure subscriber derives its subscribe list from the Ns::PtSensor ones. */
167 std::vector<SlugRef> slugs;
168 std::vector<std::string> variables;
169 uint32_t max_depth = 0;
170
171 bool empty() const {
172 return top_begin == top_end;
173 }
174};
175
176/**
177 * Diagnostic codes.
178 *
179 * Tests and the shared corpus assert on the CODE and the LINE, never the message text, so wording
180 * can improve without churning them.
181 */
221
222const char* diag_name(Diag d);
223
226 uint32_t line = 0; // 1-based
227 uint32_t col = 0; // 1-based
228 /** Human wording, including the offending text. Never asserted on by tests. */
229 std::string message;
230};
231
234 std::vector<Diagnostic> diagnostics;
235
236 bool ok() const {
237 return diagnostics.empty();
238 }
239};
240
241/**
242 * Lex and parse, applying every check that does not need config.
243 *
244 * Stops at the first error rather than attempting recovery: this is a 200-line config language
245 * written a few lines at a time, and a cascade of speculative follow-on errors would bury the one
246 * that matters.
247 */
248ParseResult parse(const std::string& source);
249
250/**
251 * What a script's slugs are allowed to name, built from the loaded config.
252 *
253 * Maps rather than sets so a diagnostic can name the canonical spelling a slug resolves to, which
254 * is what the operator sees elsewhere in the UI.
255 */
257 std::map<std::string, std::string> actuators; // slug -> canonical name
258 std::map<std::string, std::string> sensors;
259 std::map<std::string, std::string> states;
260 /** State slugs reachable from the state that owns this script, per state_transitions.csv.
261 * Every transition_to target is checked against this — not just the configured fallbacks —
262 * because a script that runs 25 s and THEN cannot leave is worse than one that never starts.
263 */
264 std::set<std::string> allowed_transitions;
265};
266
267/** Config-dependent checks. Run after a successful parse; returns empty when the script is good. */
268std::vector<Diagnostic> validate(const ScriptProgram& program, const SlugTables& tables);
269
270/**
271 * Canonical name -> slug: trim, uppercase, collapse internal whitespace runs to a single '_'.
272 *
273 * The same rule the TS side already uses for sensor entity names
274 * (backend/src/sensor-config.ts:104), so the two agree by construction.
275 */
276std::string slugify(const std::string& name);
277
278/** True when a slug is usable as a bare identifier: ^[A-Z][A-Z0-9_]*$. */
279bool is_valid_slug(const std::string& slug);
280
281/**
282 * A stable s-expression rendering of the parsed program.
283 *
284 * This exists for the conformance corpus. Two implementations can agree that a script is
285 * "accepted" while disagreeing about precedence — `a - b - c`, `not a and b`, `-x * y` — and a
286 * precedence disagreement between the validator the operator sees and the interpreter that opens
287 * the valve is the worst bug this feature can have. Pinning the shape catches it.
288 */
289std::string print_canonical(const ScriptProgram& program);
290
291} // namespace script
292} // namespace fsw
State
Definition main.cpp:72
const char * diag_name(Diag d)
Definition Parser.cpp:56
constexpr uint32_t kMaxExprDepth
Ns
Which table a slug is resolved against.
std::string print_canonical(const ScriptProgram &program)
A stable s-expression rendering of the parsed program.
Definition Parser.cpp:927
constexpr uint32_t kMaxLines
const char * op_name(Op op)
Definition Parser.cpp:22
std::string slugify(const std::string &name)
Canonical name -> slug: trim, uppercase, collapse internal whitespace runs to a single '_'.
Definition Slug.cpp:8
constexpr uint32_t kMaxVariables
constexpr uint32_t kMaxStatements
Diag
Diagnostic codes.
ParseResult parse(const std::string &source)
Lex and parse, applying every check that does not need config.
Definition Parser.cpp:813
bool is_valid_slug(const std::string &slug)
True when a slug is usable as a bare identifier: ^[A-Z][A-Z0-9_]*$.
Definition Slug.cpp:29
constexpr uint32_t kMaxNestingDepth
const char * ns_name(Ns ns)
Definition Parser.cpp:10
constexpr uint32_t kMaxIndentUnit
Widest indent unit accepted.
std::vector< Diagnostic > validate(const ScriptProgram &program, const SlugTables &tables)
Config-dependent checks.
Definition Validate.cpp:69
constexpr size_t kMaxSourceBytes
Motorized ball valve position: raw ADC counts -> % open.
std::string message
Human wording, including the offending text.
std::vector< Diagnostic > diagnostics
A parsed script, as a flat arena.
std::vector< Stmt > stmts
std::vector< Expr > exprs
std::vector< int32_t > block_items
Statement indices, grouped into contiguous [begin, end) block ranges.
std::vector< std::string > variables
std::vector< SlugRef > slugs
Every slug reference in SOURCE ORDER, with spans.
One slug reference, with its source span so a rename can rewrite exactly this token.
What a script's slugs are allowed to name, built from the loaded config.
std::set< std::string > allowed_transitions
State slugs reachable from the state that owns this script, per state_transitions....
std::map< std::string, std::string > actuators
std::map< std::string, std::string > sensors
std::map< std::string, std::string > states
int32_t body_begin
If/While body, and If's else arm: [begin, end) into ScriptProgram::block_items.
int32_t expr
Assign value, Delay seconds, If/While condition.
int32_t slug
OpenValve/CloseValve/TransitionTo target.
int32_t var
Assign: index into ScriptProgram::variables.
static SlugTables tables()
The tables every case validates against.