metrale_circuit/
runtime.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2
3//! 2026-09-30: Runtime routes. A runtime route is a condition the engine checks at run time,
4//! not a policy setting. When it holds, a step leaves the arm its plan names and runs another.
5//! A FUSIONS.toml declares each route (`[[runtime]]`) with the settings its arm plans as. A
6//! plan is rendered, estimated and executed with the arm of every route that applies beside it.
7//!
8//! Owner: metrale-circuit.
9//! Invariants:
10//! - A route applies to a run when its `modes` and `rows` contain the run and the policy states
11//!   every `when` setting at that value. Its arm is fused under the policy with `plans_as`
12//!   substituted, and is kept only when its digest differs from the primary plan's.
13//! - `plans_as` changes only settings that `when` fixes, each to another value. A route
14//!   re-plans an arm the policy selected; it never adds a setting.
15//! - Route ids are unique and never a rule's id.
16
17use std::collections::{BTreeMap, BTreeSet};
18
19use serde::Deserialize;
20
21use crate::fuser::{AvailableKernels, FuseError, FusionPlan, Policy, fuse};
22use crate::ir::Circuit;
23use crate::rules::{Mode, Rule, RuleError, parse_file};
24
25/// 2026-09-30: One `[[runtime]]` entry.
26#[derive(Debug, Clone, PartialEq, Eq)]
27pub struct RuntimeRoute {
28    /// 2026-09-30: Id, unique in its rule set.
29    pub id: String,
30    /// 2026-09-30: The policy settings under which the engine checks the condition.
31    pub when: BTreeMap<String, String>,
32    /// 2026-09-30: Modes the check runs in.
33    pub modes: BTreeSet<Mode>,
34    /// 2026-09-30: Padded rows the check runs at, inclusive.
35    pub rows: (u64, u64),
36    /// 2026-09-30: The settings the route's arm is planned under.
37    pub plans_as: BTreeMap<String, String>,
38    /// 2026-09-30: When the engine takes the route, in words.
39    pub why: String,
40    /// 2026-09-30: Where the engine makes the check.
41    pub cite: String,
42}
43
44/// 2026-09-30: A FUSIONS.toml's rules and routes.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct RuleSet {
47    /// 2026-09-30: The rules, in file order.
48    pub rules: Vec<Rule>,
49    /// 2026-09-30: The routes, in file order.
50    pub runtime: Vec<RuntimeRoute>,
51}
52
53/// 2026-09-30: `[[runtime]]` as written.
54#[derive(Debug, Deserialize)]
55#[serde(deny_unknown_fields)]
56pub(crate) struct RouteFile {
57    pub(crate) id: String,
58    when: BTreeMap<String, String>,
59    modes: Vec<String>,
60    rows: [u64; 2],
61    plans_as: BTreeMap<String, String>,
62    why: String,
63    cite: String,
64}
65
66/// 2026-09-30: Parse FUSIONS.toml text into its rules and routes.
67pub fn parse_rule_set(text: &str) -> Result<RuleSet, RuleError> {
68    let (rules, files) = parse_file(text)?;
69    let mut seen = BTreeSet::new();
70    let mut runtime = Vec::with_capacity(files.len());
71    for f in files {
72        if !seen.insert(f.id.clone()) {
73            return Err(bad(&f.id, "duplicate id".into()));
74        }
75        runtime.push(route(f)?);
76    }
77    check_ids(&rules, &runtime)?;
78    Ok(RuleSet { rules, runtime })
79}
80
81/// 2026-09-30: No route shares an id with a rule.
82pub fn check_ids(rules: &[Rule], runtime: &[RuntimeRoute]) -> Result<(), RuleError> {
83    match runtime.iter().find(|r| rules.iter().any(|x| x.id == r.id)) {
84        Some(r) => Err(bad(&r.id, "shares its id with a rule".into())),
85        None => Ok(()),
86    }
87}
88
89fn bad(id: &str, detail: String) -> RuleError {
90    RuleError::Runtime {
91        route: id.to_string(),
92        detail,
93    }
94}
95
96fn route(f: RouteFile) -> Result<RuntimeRoute, RuleError> {
97    if f.plans_as.is_empty() {
98        return Err(bad(&f.id, "`plans_as` is empty".into()));
99    }
100    for (k, v) in &f.plans_as {
101        match f.when.get(k) {
102            Some(w) if w != v => {}
103            Some(_) => {
104                return Err(bad(
105                    &f.id,
106                    format!("`plans_as` keeps `{k}` at its `when` value"),
107                ));
108            }
109            None => {
110                return Err(bad(
111                    &f.id,
112                    format!("`plans_as` sets `{k}`, which `when` does not fix"),
113                ));
114            }
115        }
116    }
117    let modes = f
118        .modes
119        .iter()
120        .map(|m| Mode::parse(m).ok_or_else(|| bad(&f.id, format!("unknown mode `{m}`"))))
121        .collect::<Result<BTreeSet<Mode>, RuleError>>()?;
122    if modes.is_empty() {
123        return Err(bad(&f.id, "no modes".into()));
124    }
125    if f.rows[0] == 0 || f.rows[0] > f.rows[1] {
126        return Err(bad(&f.id, format!("rows {:?}", f.rows)));
127    }
128    if f.why.trim().is_empty() || f.cite.trim().is_empty() {
129        return Err(bad(&f.id, "`why` and `cite` must be stated".into()));
130    }
131    Ok(RuntimeRoute {
132        id: f.id,
133        when: f.when,
134        modes,
135        rows: (f.rows[0], f.rows[1]),
136        plans_as: f.plans_as,
137        why: f.why,
138        cite: f.cite,
139    })
140}
141
142/// 2026-09-30: `own` over `base`: a route of the same id replaces the base's, others are added.
143pub fn overlay(base: &mut Vec<RuntimeRoute>, own: Vec<RuntimeRoute>) {
144    for r in own {
145        match base.iter_mut().find(|b| b.id == r.id) {
146            Some(slot) => *slot = r,
147            None => base.push(r),
148        }
149    }
150}
151
152impl RuntimeRoute {
153    /// 2026-09-30: The engine makes this check on `mode` at `rows` under `policy`.
154    pub fn applies(&self, policy: &Policy, mode: Mode, rows: u64) -> bool {
155        self.modes.contains(&mode)
156            && (self.rows.0..=self.rows.1).contains(&rows)
157            && self
158                .when
159                .iter()
160                .all(|(k, v)| policy.settings.get(k) == Some(v))
161    }
162
163    /// 2026-09-30: `policy` with `plans_as` substituted.
164    pub fn policy(&self, policy: &Policy) -> Policy {
165        let mut out = policy.clone();
166        for (k, v) in &self.plans_as {
167            out.settings.insert(k.clone(), v.clone());
168        }
169        out
170    }
171
172    /// 2026-09-30: `k=v` for each `plans_as` setting.
173    pub fn plans_as_text(&self) -> String {
174        let s: Vec<String> = self
175            .plans_as
176            .iter()
177            .map(|(k, v)| format!("{k}={v}"))
178            .collect();
179        s.join(" ")
180    }
181}
182
183/// 2026-09-30: The arm of each route in `runtime` that applies to `mode` at `rows` and plans
184/// differently from `primary`, fused with `rules` and `available` as the primary was.
185pub fn route_arms(
186    circuit: &Circuit,
187    set: (&[Rule], &[RuntimeRoute]),
188    available: &AvailableKernels,
189    policy: &Policy,
190    primary: &FusionPlan,
191) -> Result<Vec<(RuntimeRoute, FusionPlan)>, FuseError> {
192    let (rules, runtime) = set;
193    let (mode, rows) = (primary.mode, primary.rows);
194    let mut out = Vec::new();
195    for r in runtime.iter().filter(|r| r.applies(policy, mode, rows)) {
196        let arm = fuse(circuit, rules, available, &r.policy(policy), mode, rows)?;
197        if arm.digest != primary.digest {
198            out.push((r.clone(), arm));
199        }
200    }
201    Ok(out)
202}