Skip to content

Commit cc8f3b9

Browse files
tbitcsoz-agent
andcommitted
fix(compiler+benchmarks): emit compute expressions, fix fact indices, add QEMU support
Compiler fixes: - canonical.py: extract compute expressions from rule 'then.compute' blocks into a flat CanonicalModel.expressions list with resolved fact IDs (UINT16_MAX sentinel for literal operands). Annotate each rule with _expr_start/_expr_count for the emitter. - emit_c.py: add _EXPR_OP_MAP, emit model_expressions[] table, wire .expr_count and .expressions into the ARBITER_model struct, add .expr_start/.expr_count to each rule entry. Benchmark fixes: - Remove hand-coded fact enums that used the wrong alphabetical ordering. Replace with #defines aliasing generated ARBITER_FACT_* constants from arbiter_model.h. Fixes type mismatch warnings and wrong Kalman output. - Increase BENCH_ITERATIONS 1k->100k for measurable ns counts on native_sim. QEMU support: - Add qemu_x86 to platform_allow in both benchmark testcase.yaml files. Local: west build -p -b qemu_x86 app/tests/benchmarks/kalman_benchmark Tests (26 passing): - tests/python/test_emit_c.py: 14 new tests for expression emission. Regenerated arbiter_model.{c,h} for all 19 models with fixed compiler. Co-Authored-By: Oz <oz-agent@warp.dev>
1 parent 7e51157 commit cc8f3b9

27 files changed

Lines changed: 940 additions & 195 deletions

File tree

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,6 @@ coverage.xml
4646
# specsmith machine-local state
4747
.chronomemory/
4848
.specsmith/*.bak
49+
50+
twister-results-*/
51+

python/arbiter/canonical.py

Lines changed: 94 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ class CanonicalModel:
2020
conditions: list[dict[str, Any]]
2121
actions: list[dict[str, Any]]
2222
modes: list[dict[str, Any]]
23+
expressions: list[dict[str, Any]] = field(default_factory=list)
2324
hazards: list[dict[str, Any]] = field(default_factory=list)
2425
safety_goals: list[dict[str, Any]] = field(default_factory=list)
2526
model_hash: str = ""
@@ -46,6 +47,10 @@ def max_conditions(self) -> int:
4647
def max_actions(self) -> int:
4748
return len(self.actions)
4849

50+
@property
51+
def max_expressions(self) -> int:
52+
return len(self.expressions)
53+
4954

5055
def canonicalize(data: dict[str, Any]) -> CanonicalModel:
5156
"""Canonicalize a parsed ARB model.
@@ -70,17 +75,33 @@ def canonicalize(data: dict[str, Any]) -> CanonicalModel:
7075
actions = sorted(data.get("actions", []), key=lambda a: a.get("id", ""))
7176
action_id_map = {a["id"]: i for i, a in enumerate(actions)}
7277

73-
# Sort rules by id and flatten conditions
74-
rules = sorted(data.get("rules", []), key=lambda r: r.get("id", ""))
75-
rule_id_map = {r["id"]: i for i, r in enumerate(rules)}
78+
# Sort rules by id and flatten conditions + expressions
79+
rules_raw = sorted(data.get("rules", []), key=lambda r: r.get("id", ""))
80+
rule_id_map = {r["id"]: i for i, r in enumerate(rules_raw)}
7681

77-
# Flatten condition trees from all rules
82+
# Flatten condition trees and compute expressions from all rules.
83+
# Annotate each rule dict with expr_start / expr_count so the emitter
84+
# can write them into the rules table without a second pass.
7885
conditions: list[dict[str, Any]] = []
79-
for rule in rules:
86+
expressions: list[dict[str, Any]] = []
87+
rules: list[dict[str, Any]] = []
88+
for rule in rules_raw:
8089
when = rule.get("when", {})
8190
if isinstance(when, dict):
8291
_flatten_conditions(when, conditions, fact_id_map)
8392

93+
expr_start = len(expressions)
94+
rule_exprs = _flatten_expressions(
95+
rule.get("then", {}), fact_id_map
96+
)
97+
expressions.extend(rule_exprs)
98+
99+
# Attach start/count to a copy of the rule so we don't mutate input.
100+
annotated = dict(rule)
101+
annotated["_expr_start"] = expr_start
102+
annotated["_expr_count"] = len(rule_exprs)
103+
rules.append(annotated)
104+
84105
model = CanonicalModel(
85106
name=data.get("model", "unnamed"),
86107
arb_version=data.get("arb_version", 0.1),
@@ -89,6 +110,7 @@ def canonicalize(data: dict[str, Any]) -> CanonicalModel:
89110
conditions=conditions,
90111
actions=actions,
91112
modes=modes,
113+
expressions=expressions,
92114
hazards=data.get("hazards", []),
93115
safety_goals=data.get("safety_goals", []),
94116
fact_id_map=fact_id_map,
@@ -106,6 +128,73 @@ def canonicalize(data: dict[str, Any]) -> CanonicalModel:
106128
return model
107129

108130

131+
_UINT16_MAX = 65535 # UINT16_MAX sentinel for "use literal"
132+
133+
_EXPR_OP_ALIASES: dict[str, str] = {
134+
"assign": "assign", "add": "add", "sub": "sub", "mul": "mul",
135+
"div": "div", "mod": "mod", "abs": "abs", "negate": "negate",
136+
"min": "min", "max": "max", "clamp": "clamp",
137+
"shift_r": "shift_r", "shift_l": "shift_l",
138+
"scale": "scale", "accumulate": "accumulate",
139+
}
140+
141+
142+
def _flatten_expressions(
143+
then: Any,
144+
fact_id_map: dict[str, int],
145+
) -> list[dict[str, Any]]:
146+
"""Extract compute expressions from a rule's then block.
147+
148+
Each entry maps to an ``ARBITER_expr_def`` with resolved fact indices.
149+
``UINT16_MAX`` (65535) signals "use the literal instead of a fact".
150+
"""
151+
if not isinstance(then, dict):
152+
return []
153+
compute = then.get("compute", [])
154+
if not isinstance(compute, list):
155+
return []
156+
157+
out: list[dict[str, Any]] = []
158+
for expr in compute:
159+
if not isinstance(expr, dict):
160+
continue
161+
162+
target = expr.get("target", "")
163+
target_id = fact_id_map.get(target, 0)
164+
165+
# Left operand
166+
left_name = expr.get("left")
167+
if left_name is not None and left_name in fact_id_map:
168+
left_fact_id = fact_id_map[left_name]
169+
left_literal: int = 0
170+
else:
171+
left_fact_id = _UINT16_MAX
172+
left_literal = int(expr.get("left_literal", 0))
173+
174+
# Right operand
175+
right_name = expr.get("right")
176+
if right_name is not None and right_name in fact_id_map:
177+
right_fact_id = fact_id_map[right_name]
178+
right_literal: int = 0
179+
else:
180+
right_fact_id = _UINT16_MAX
181+
right_literal = int(expr.get("right_literal", 0))
182+
183+
op = _EXPR_OP_ALIASES.get(expr.get("op", "assign"), "assign")
184+
scale = int(expr.get("scale", 1))
185+
186+
out.append({
187+
"target_fact_id": target_id,
188+
"op": op,
189+
"left_fact_id": left_fact_id,
190+
"left_literal": left_literal,
191+
"right_fact_id": right_fact_id,
192+
"right_literal": right_literal,
193+
"scale": scale,
194+
})
195+
return out
196+
197+
109198
def _flatten_conditions(
110199
when: dict[str, Any],
111200
conditions: list[dict[str, Any]],

python/arbiter/emit_c.py

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,17 @@
3333
"raise_fault": "ARBITER_ACTION_RAISE_FAULT", "clear_fault": "ARBITER_ACTION_CLEAR_FAULT",
3434
}
3535

36+
_EXPR_OP_MAP = {
37+
"add": "ARBITER_EXPR_ADD", "sub": "ARBITER_EXPR_SUB",
38+
"mul": "ARBITER_EXPR_MUL", "div": "ARBITER_EXPR_DIV",
39+
"mod": "ARBITER_EXPR_MOD", "abs": "ARBITER_EXPR_ABS",
40+
"negate": "ARBITER_EXPR_NEGATE", "min": "ARBITER_EXPR_MIN",
41+
"max": "ARBITER_EXPR_MAX", "clamp": "ARBITER_EXPR_CLAMP",
42+
"shift_r": "ARBITER_EXPR_SHIFT_R", "shift_l": "ARBITER_EXPR_SHIFT_L",
43+
"scale": "ARBITER_EXPR_SCALE", "accumulate": "ARBITER_EXPR_ACCUMULATE",
44+
"assign": "ARBITER_EXPR_ASSIGN",
45+
}
46+
3647

3748
def _c_str(s: str | None) -> str:
3849
return f'"{s}"' if s else "NULL"
@@ -190,10 +201,13 @@ def emit_c_source(model: CanonicalModel, header_name: str = "arbiter_model.h",
190201
raw_expl = then.get("explanation") if isinstance(then, dict) else None
191202
explanation = _c_str(raw_expl) if emit_trace_strings else "NULL"
192203

204+
expr_start = r.get("_expr_start", 0)
205+
expr_count = r.get("_expr_count", 0)
193206
lines.append(
194207
f"\t{{ .id = {i}, .rule_class = {rclass}, "
195208
f".condition_start = {cond_offset}, .condition_count = {cond_count}, "
196209
f".action_start = {action_start}, .action_count = {action_count}, "
210+
f".expr_start = {expr_start}, .expr_count = {expr_count}, "
197211
f".safety_goal_id = UINT16_MAX, .set_mode = {set_mode}, "
198212
f".safety_critical = {safety_critical}, "
199213
f".name = {name}, .explanation = {explanation} }},"
@@ -205,6 +219,26 @@ def emit_c_source(model: CanonicalModel, header_name: str = "arbiter_model.h",
205219
lines.append("};")
206220
lines.append("")
207221

222+
# Expressions table
223+
expressions = getattr(model, "expressions", [])
224+
if expressions:
225+
lines.append("static const struct ARBITER_expr_def model_expressions[] = {")
226+
for e in expressions:
227+
op_enum = _EXPR_OP_MAP.get(e.get("op", "assign"), "ARBITER_EXPR_ASSIGN")
228+
lines.append(
229+
f"\t{{ .target_fact_id = {e['target_fact_id']}, "
230+
f".op = {op_enum}, "
231+
f".left_fact_id = {e['left_fact_id']}, "
232+
f".left_literal = {e['left_literal']}, "
233+
f".right_fact_id = {e['right_fact_id']}, "
234+
f".right_literal = {e['right_literal']}, "
235+
f".scale = {e['scale']} }},"
236+
)
237+
lines.append("};")
238+
else:
239+
lines.append("static const struct ARBITER_expr_def *model_expressions = NULL;")
240+
lines.append("")
241+
208242
# Mode names
209243
if model.modes and emit_trace_strings:
210244
lines.append("static const char *model_mode_names[] = {")
@@ -222,6 +256,7 @@ def emit_c_source(model: CanonicalModel, header_name: str = "arbiter_model.h",
222256
schema_hex = model.schema_hash[:64].ljust(64, "0")
223257
schema_bytes = ", ".join(f"0x{schema_hex[j:j+2]}" for j in range(0, 64, 2))
224258

259+
expr_count_total = len(getattr(model, "expressions", []))
225260
lines.extend([
226261
"const struct ARBITER_model ARBITER_generated_model = {",
227262
f'\t.name = "{model.name}",',
@@ -231,11 +266,13 @@ def emit_c_source(model: CanonicalModel, header_name: str = "arbiter_model.h",
231266
f"\t.rule_count = {model.max_rules},",
232267
f"\t.condition_count = {model.max_conditions},",
233268
f"\t.action_count = {model.max_actions},",
269+
f"\t.expr_count = {expr_count_total},",
234270
f"\t.mode_count = {len([m for m in model.modes if isinstance(m, dict)])},",
235271
"\t.facts = model_facts,",
236272
"\t.rules = model_rules,",
237273
"\t.conditions = model_conditions,",
238274
"\t.actions = model_actions,",
275+
"\t.expressions = model_expressions,",
239276
"\t.mode_names = model_mode_names,",
240277
"};",
241278
"",

samples/access_control/src/arbiter_model.c

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -46,18 +46,35 @@ static const struct ARBITER_action_def model_actions[] = {
4646
};
4747

4848
static const struct ARBITER_rule_def model_rules[] = {
49-
{ .id = 0, .rule_class = ARBITER_RULE_INFERENCE, .condition_start = 0, .condition_count = 1, .action_start = 0, .action_count = 0, .safety_goal_id = UINT16_MAX, .set_mode = 1, .safety_critical = false, .name = "01_cors.preflight", .explanation = "OPTIONS request — CORS preflight." },
50-
{ .id = 1, .rule_class = ARBITER_RULE_INFERENCE, .condition_start = 1, .condition_count = 2, .action_start = 0, .action_count = 0, .safety_goal_id = UINT16_MAX, .set_mode = UINT16_MAX, .safety_critical = false, .name = "02_cors.allow_origin", .explanation = "Same-origin or allowed origin — CORS pass." },
51-
{ .id = 2, .rule_class = ARBITER_RULE_CONSTRAINT, .condition_start = 3, .condition_count = 1, .action_start = 0, .action_count = 0, .safety_goal_id = UINT16_MAX, .set_mode = 2, .safety_critical = false, .name = "03_cors.block_origin", .explanation = "Blocked origin — 403." },
52-
{ .id = 3, .rule_class = ARBITER_RULE_CONSTRAINT, .condition_start = 4, .condition_count = 1, .action_start = 0, .action_count = 1, .safety_goal_id = UINT16_MAX, .set_mode = 3, .safety_critical = false, .name = "10_rate.check", .explanation = "Client > 60 req/min — 429 Too Many Requests." },
53-
{ .id = 4, .rule_class = ARBITER_RULE_INFERENCE, .condition_start = 5, .condition_count = 1, .action_start = 0, .action_count = 0, .safety_goal_id = UINT16_MAX, .set_mode = 0, .safety_critical = false, .name = "20_auth.public_path", .explanation = "Public path — no auth required." },
54-
{ .id = 5, .rule_class = ARBITER_RULE_INFERENCE, .condition_start = 6, .condition_count = 2, .action_start = 0, .action_count = 0, .safety_goal_id = UINT16_MAX, .set_mode = 0, .safety_critical = false, .name = "21_auth.health_check", .explanation = "Health check GET — always allowed." },
55-
{ .id = 6, .rule_class = ARBITER_RULE_CONSTRAINT, .condition_start = 8, .condition_count = 2, .action_start = 0, .action_count = 0, .safety_goal_id = UINT16_MAX, .set_mode = 2, .safety_critical = false, .name = "22_auth.no_token", .explanation = "Auth-required path but no token — 401." },
56-
{ .id = 7, .rule_class = ARBITER_RULE_CONSTRAINT, .condition_start = 10, .condition_count = 2, .action_start = 0, .action_count = 0, .safety_goal_id = UINT16_MAX, .set_mode = 2, .safety_critical = false, .name = "23_auth.invalid_token", .explanation = "Invalid/expired token — 401." },
57-
{ .id = 8, .rule_class = ARBITER_RULE_CONSTRAINT, .condition_start = 12, .condition_count = 2, .action_start = 0, .action_count = 0, .safety_goal_id = UINT16_MAX, .set_mode = 2, .safety_critical = false, .name = "24_auth.admin_check", .explanation = "Admin path requires admin role — 403." },
58-
{ .id = 9, .rule_class = ARBITER_RULE_INFERENCE, .condition_start = 14, .condition_count = 3, .action_start = 0, .action_count = 0, .safety_goal_id = UINT16_MAX, .set_mode = 0, .safety_critical = false, .name = "25_auth.valid_token", .explanation = "Valid token, authorized — allow." },
59-
{ .id = 10, .rule_class = ARBITER_RULE_INFERENCE, .condition_start = 17, .condition_count = 1, .action_start = 0, .action_count = 0, .safety_goal_id = UINT16_MAX, .set_mode = UINT16_MAX, .safety_critical = false, .name = "30_risk.compute", .explanation = NULL },
60-
{ .id = 11, .rule_class = ARBITER_RULE_CONSTRAINT, .condition_start = 18, .condition_count = 1, .action_start = 0, .action_count = 0, .safety_goal_id = UINT16_MAX, .set_mode = 2, .safety_critical = false, .name = "40_payload.too_large", .explanation = "Payload > 64KB — rejected." },
49+
{ .id = 0, .rule_class = ARBITER_RULE_INFERENCE, .condition_start = 0, .condition_count = 1, .action_start = 0, .action_count = 0, .expr_start = 0, .expr_count = 2, .safety_goal_id = UINT16_MAX, .set_mode = 1, .safety_critical = false, .name = "01_cors.preflight", .explanation = "OPTIONS request — CORS preflight." },
50+
{ .id = 1, .rule_class = ARBITER_RULE_INFERENCE, .condition_start = 1, .condition_count = 2, .action_start = 0, .action_count = 0, .expr_start = 2, .expr_count = 1, .safety_goal_id = UINT16_MAX, .set_mode = UINT16_MAX, .safety_critical = false, .name = "02_cors.allow_origin", .explanation = "Same-origin or allowed origin — CORS pass." },
51+
{ .id = 2, .rule_class = ARBITER_RULE_CONSTRAINT, .condition_start = 3, .condition_count = 1, .action_start = 0, .action_count = 0, .expr_start = 3, .expr_count = 1, .safety_goal_id = UINT16_MAX, .set_mode = 2, .safety_critical = false, .name = "03_cors.block_origin", .explanation = "Blocked origin — 403." },
52+
{ .id = 3, .rule_class = ARBITER_RULE_CONSTRAINT, .condition_start = 4, .condition_count = 1, .action_start = 0, .action_count = 1, .expr_start = 4, .expr_count = 1, .safety_goal_id = UINT16_MAX, .set_mode = 3, .safety_critical = false, .name = "10_rate.check", .explanation = "Client > 60 req/min — 429 Too Many Requests." },
53+
{ .id = 4, .rule_class = ARBITER_RULE_INFERENCE, .condition_start = 5, .condition_count = 1, .action_start = 0, .action_count = 0, .expr_start = 5, .expr_count = 1, .safety_goal_id = UINT16_MAX, .set_mode = 0, .safety_critical = false, .name = "20_auth.public_path", .explanation = "Public path — no auth required." },
54+
{ .id = 5, .rule_class = ARBITER_RULE_INFERENCE, .condition_start = 6, .condition_count = 2, .action_start = 0, .action_count = 0, .expr_start = 6, .expr_count = 1, .safety_goal_id = UINT16_MAX, .set_mode = 0, .safety_critical = false, .name = "21_auth.health_check", .explanation = "Health check GET — always allowed." },
55+
{ .id = 6, .rule_class = ARBITER_RULE_CONSTRAINT, .condition_start = 8, .condition_count = 2, .action_start = 0, .action_count = 0, .expr_start = 7, .expr_count = 1, .safety_goal_id = UINT16_MAX, .set_mode = 2, .safety_critical = false, .name = "22_auth.no_token", .explanation = "Auth-required path but no token — 401." },
56+
{ .id = 7, .rule_class = ARBITER_RULE_CONSTRAINT, .condition_start = 10, .condition_count = 2, .action_start = 0, .action_count = 0, .expr_start = 8, .expr_count = 1, .safety_goal_id = UINT16_MAX, .set_mode = 2, .safety_critical = false, .name = "23_auth.invalid_token", .explanation = "Invalid/expired token — 401." },
57+
{ .id = 8, .rule_class = ARBITER_RULE_CONSTRAINT, .condition_start = 12, .condition_count = 2, .action_start = 0, .action_count = 0, .expr_start = 9, .expr_count = 1, .safety_goal_id = UINT16_MAX, .set_mode = 2, .safety_critical = false, .name = "24_auth.admin_check", .explanation = "Admin path requires admin role — 403." },
58+
{ .id = 9, .rule_class = ARBITER_RULE_INFERENCE, .condition_start = 14, .condition_count = 3, .action_start = 0, .action_count = 0, .expr_start = 10, .expr_count = 1, .safety_goal_id = UINT16_MAX, .set_mode = 0, .safety_critical = false, .name = "25_auth.valid_token", .explanation = "Valid token, authorized — allow." },
59+
{ .id = 10, .rule_class = ARBITER_RULE_INFERENCE, .condition_start = 17, .condition_count = 1, .action_start = 0, .action_count = 0, .expr_start = 11, .expr_count = 2, .safety_goal_id = UINT16_MAX, .set_mode = UINT16_MAX, .safety_critical = false, .name = "30_risk.compute", .explanation = NULL },
60+
{ .id = 11, .rule_class = ARBITER_RULE_CONSTRAINT, .condition_start = 18, .condition_count = 1, .action_start = 0, .action_count = 0, .expr_start = 13, .expr_count = 1, .safety_goal_id = UINT16_MAX, .set_mode = 2, .safety_critical = false, .name = "40_payload.too_large", .explanation = "Payload > 64KB — rejected." },
61+
};
62+
63+
static const struct ARBITER_expr_def model_expressions[] = {
64+
{ .target_fact_id = 3, .op = ARBITER_EXPR_ASSIGN, .left_fact_id = 65535, .left_literal = 1, .right_fact_id = 65535, .right_literal = 0, .scale = 1 },
65+
{ .target_fact_id = 2, .op = ARBITER_EXPR_ASSIGN, .left_fact_id = 65535, .left_literal = 0, .right_fact_id = 65535, .right_literal = 0, .scale = 1 },
66+
{ .target_fact_id = 2, .op = ARBITER_EXPR_ASSIGN, .left_fact_id = 65535, .left_literal = 1, .right_fact_id = 65535, .right_literal = 0, .scale = 1 },
67+
{ .target_fact_id = 0, .op = ARBITER_EXPR_ASSIGN, .left_fact_id = 65535, .left_literal = 3, .right_fact_id = 65535, .right_literal = 0, .scale = 1 },
68+
{ .target_fact_id = 0, .op = ARBITER_EXPR_ASSIGN, .left_fact_id = 65535, .left_literal = 4, .right_fact_id = 65535, .right_literal = 0, .scale = 1 },
69+
{ .target_fact_id = 0, .op = ARBITER_EXPR_ASSIGN, .left_fact_id = 65535, .left_literal = 1, .right_fact_id = 65535, .right_literal = 0, .scale = 1 },
70+
{ .target_fact_id = 0, .op = ARBITER_EXPR_ASSIGN, .left_fact_id = 65535, .left_literal = 1, .right_fact_id = 65535, .right_literal = 0, .scale = 1 },
71+
{ .target_fact_id = 0, .op = ARBITER_EXPR_ASSIGN, .left_fact_id = 65535, .left_literal = 2, .right_fact_id = 65535, .right_literal = 0, .scale = 1 },
72+
{ .target_fact_id = 0, .op = ARBITER_EXPR_ASSIGN, .left_fact_id = 65535, .left_literal = 2, .right_fact_id = 65535, .right_literal = 0, .scale = 1 },
73+
{ .target_fact_id = 0, .op = ARBITER_EXPR_ASSIGN, .left_fact_id = 65535, .left_literal = 3, .right_fact_id = 65535, .right_literal = 0, .scale = 1 },
74+
{ .target_fact_id = 0, .op = ARBITER_EXPR_ASSIGN, .left_fact_id = 65535, .left_literal = 1, .right_fact_id = 65535, .right_literal = 0, .scale = 1 },
75+
{ .target_fact_id = 1, .op = ARBITER_EXPR_SCALE, .left_fact_id = 4, .left_literal = 0, .right_fact_id = 65535, .right_literal = 100, .scale = 120 },
76+
{ .target_fact_id = 1, .op = ARBITER_EXPR_CLAMP, .left_fact_id = 1, .left_literal = 0, .right_fact_id = 65535, .right_literal = 0, .scale = 100 },
77+
{ .target_fact_id = 0, .op = ARBITER_EXPR_ASSIGN, .left_fact_id = 65535, .left_literal = 3, .right_fact_id = 65535, .right_literal = 0, .scale = 1 },
6178
};
6279

6380
static const char *model_mode_names[] = {
@@ -75,10 +92,12 @@ const struct ARBITER_model ARBITER_generated_model = {
7592
.rule_count = 12,
7693
.condition_count = 19,
7794
.action_count = 1,
95+
.expr_count = 14,
7896
.mode_count = 4,
7997
.facts = model_facts,
8098
.rules = model_rules,
8199
.conditions = model_conditions,
82100
.actions = model_actions,
101+
.expressions = model_expressions,
83102
.mode_names = model_mode_names,
84103
};

0 commit comments

Comments
 (0)