Skip to main content

brep_render/engine_state/
expressions.rs

1use super::*;
2
3// Expression edits rerun the current history prefix so parameters that reference
4// variables rebuild against the updated environment.
5impl EngineState {
6    /// The history's `expressions` source string (the variable sheet the panel's
7    /// editor binds to). Raw source text — despite the `_json` suffix it mirrors
8    /// the other engine readouts' naming; the verifier reads it verbatim.
9    pub fn expressions_json(&self) -> String {
10        self.history.expressions()
11    }
12
13    /// Replace the `expressions` source and re-run the rolled-to prefix so every
14    /// feature param referencing a variable (e.g. `sizeX = "boxW"`) rebuilds with
15    /// the new value — the panel's live-update path. Returns the build-report JSON.
16    pub fn set_expressions(&mut self, expressions: &str) -> String {
17        self.history.set_expressions(expressions);
18        self.rerun_history()
19    }
20
21    /// Show the model evaluated with `preview`'s expressions source instead of
22    /// the document's own, or (`None`) with the document's own again. Runs the
23    /// history when what the runs evaluate changes; re-arming the same source
24    /// runs nothing. Returns the build report.
25    ///
26    /// TRANSIENT by construction: the source is substituted into the run
27    /// REQUEST only (`Self::rerun_history`), never into the history, so the
28    /// document, its undo stacks, its dirty state and what Save writes are
29    /// untouched.
30    ///
31    /// The preview is armed against the history's current
32    /// [`edit_serial`](crate::history::History::edit_serial). The first run
33    /// after the user edits the model (or undoes, or redoes) drops it, so the
34    /// edit rebuilds against the document's own values and nothing previewed
35    /// is shown under it. A host that edits the document ITSELF while
36    /// previewing (the family table writing a cell) re-arms after the write.
37    pub fn set_expression_preview(&mut self, preview: Option<ExpressionPreview>) -> String {
38        let serial = self.history.edit_serial();
39        // What the display was last BUILT with: the stored preview even when
40        // an edit has made it stale, so ending a preview that a block write
41        // (which runs nothing) left on screen still rebuilds.
42        let before = self.expression_preview.as_ref().map(|p| p.expressions.clone());
43        let after = preview.as_ref().map(|p| p.expressions.clone());
44        self.expression_preview = preview.map(|p| ExpressionPreview { serial, ..p });
45        if before == after {
46            return self.history_report.clone();
47        }
48        self.rerun_history()
49    }
50
51    /// Re-arm the stored preview at the history's current edit serial,
52    /// running nothing: for a host that has just edited the document itself
53    /// (the family table committing a cell) and keeps previewing.
54    pub fn rearm_expression_preview(&mut self) {
55        let serial = self.history.edit_serial();
56        if let Some(preview) = self.expression_preview.as_mut() {
57            preview.serial = serial;
58        }
59    }
60
61    /// The preview in force: `None` when there is none, or when the user has
62    /// edited the model since it was armed (the next run drops it).
63    pub fn expression_preview(&self) -> Option<&ExpressionPreview> {
64        self.live_expression_preview()
65    }
66
67    fn live_expression_preview(&self) -> Option<&ExpressionPreview> {
68        self.expression_preview
69            .as_ref()
70            .filter(|p| p.serial == self.history.edit_serial())
71    }
72
73    /// Drop a preview the user has edited the model under.
74    pub(super) fn drop_stale_expression_preview(&mut self) {
75        if self.live_expression_preview().is_none() {
76            self.expression_preview = None;
77        }
78    }
79
80    /// The rolled-to prefix as a run REQUEST: [`History::prefix_request`]
81    /// with the live preview's expressions in place of the document's. Every
82    /// execution of the model — the display run and the main-side re-runs
83    /// that measure, export or pick against it — builds from this, so what is
84    /// measured is what is shown.
85    ///
86    /// [`History::prefix_request`]: crate::history::History::prefix_request
87    pub(super) fn run_request_value(&self) -> serde_json::Value {
88        let mut request = self.history.prefix_request();
89        if let (Some(preview), Some(object)) = (self.live_expression_preview(), request.as_object_mut()) {
90            object.insert("expressions".into(), serde_json::Value::String(preview.expressions.clone()));
91        }
92        request
93    }
94
95    /// The history's `configurator` object (typed named inputs) as JSON — a
96    /// read/display surface for the panel; deeper configurator editing is deferred.
97    pub fn configurator_json(&self) -> String {
98        self.history.configurator().to_string()
99    }
100
101    /// The parsed variable list for the sheet's name/value view:
102    /// `[{ "name": "...", "expr": "..." }, …]` — one entry per `name = rhs;`
103    /// assignment in the expressions source, in source order. The RHS is shown
104    /// verbatim (its DEFINING expression); evaluating it to a live scalar needs the
105    /// kernel's private `Env`, so a computed value column is deferred — the text
106    /// editor + re-run is the source of truth for applied values.
107    pub fn expression_variables_json(&self) -> String {
108        let vars = parse_expression_variables(&self.history.expressions());
109        serde_json::to_string(&vars).unwrap_or_else(|_| "[]".to_string())
110    }
111}
112
113/// Parse the `name = rhs;` assignments from an expressions source into an ordered
114/// `[{name, expr}]` list for the sheet's variable view. Mirrors the kernel
115/// evaluator's statement grammar (`IDENT '=' expr ';'`) without reaching into its
116/// private `Env`: `//` line comments are stripped, statements split on `;`, and
117/// each `IDENT = rhs` yields one entry (a non-identifier LHS or empty RHS is
118/// skipped, matching what the evaluator would reject).
119fn parse_expression_variables(source: &str) -> Vec<serde_json::Value> {
120    // Strip `//` line comments line-by-line (the DSL has no strings, so a bare
121    // `//` always starts a comment), then split statements on ';'.
122    let mut cleaned = String::with_capacity(source.len());
123    for line in source.lines() {
124        let code = match line.find("//") {
125            Some(idx) => &line[..idx],
126            None => line,
127        };
128        cleaned.push_str(code);
129        cleaned.push('\n');
130    }
131    let mut out = Vec::new();
132    for stmt in cleaned.split(';') {
133        let stmt = stmt.trim();
134        if stmt.is_empty() {
135            continue;
136        }
137        let Some(eq) = stmt.find('=') else {
138            continue;
139        };
140        let name = stmt[..eq].trim();
141        let expr = stmt[eq + 1..].trim();
142        if name.is_empty() || expr.is_empty() || !is_identifier(name) {
143            continue;
144        }
145        out.push(serde_json::json!({ "name": name, "expr": expr }));
146    }
147    out
148}
149
150/// Whether `s` is a single expression-DSL identifier (`[A-Za-z_$][A-Za-z0-9_$]*`)
151/// — the valid LHS of an assignment statement.
152fn is_identifier(s: &str) -> bool {
153    let mut chars = s.chars();
154    match chars.next() {
155        Some(c) if c.is_ascii_alphabetic() || c == '_' || c == '$' => {}
156        _ => return false,
157    }
158    chars.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '$')
159}
160
161
162
163/// A transient expressions source for [`EngineState::set_expression_preview`].
164#[derive(Debug, Clone, PartialEq, Eq)]
165pub struct ExpressionPreview {
166    /// The whole expressions source the runs evaluate while it is in force.
167    pub expressions: String,
168    /// What is being previewed, for the working indicator (a part number).
169    pub label: String,
170    /// The history's edit serial it was armed at (set by the engine).
171    pub serial: u64,
172}
173
174impl ExpressionPreview {
175    pub fn new(expressions: impl Into<String>, label: impl Into<String>) -> Self {
176        Self { expressions: expressions.into(), label: label.into(), serial: 0 }
177    }
178}