Skip to main content

polydat_core/dsl/
stub.rs

1// Copyright (c) nosqlbench
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7//     http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
12// implied. See the License for the specific language governing
13// permissions and limitations under the License.
14
15//! Caller-native, typed polydat expression stubs.
16//!
17//! Lets Rust code build a polydat binding from an expression and emit
18//! it as a [`Statement`] for a grammar-safe
19//! `crate::kernel::subcontext::module::BodyFragment::Statements`
20//! (subcontext_construction.md §2.1) — **without** concatenating source
21//! strings. The return type is bound at the call site via the [`Wire`]
22//! trait, so the Rust generic and the polydat target type are one and
23//! the same.
24//!
25//! Synthesizers (metrics, poll, stop conditions) build stubs; only
26//! user-authored predicate *text* is parsed, once, at the boundary
27//! ([`ExprStub::parse`]).
28
29use crate::ast::{PortType, Value};
30use crate::derive_support::Wire;
31use crate::dsl::ast::{Binding, BindingModifier, Expr, ExternPort, Statement, WireModifier};
32use crate::dsl::lexer::Span;
33
34/// A caller-native expression stub: a named binding over a polydat
35/// expression, optionally type-coerced (via the `as` cast,
36/// polydat_grammar.md §10) and `volatile`.
37pub struct ExprStub {
38    name: String,
39    expr: Expr,
40    modifier: BindingModifier,
41}
42
43impl ExprStub {
44    /// Build a stub from an already-constructed expression.
45    pub fn new(name: impl Into<String>, expr: Expr) -> Self {
46        Self {
47            name: name.into(),
48            expr,
49            modifier: BindingModifier::default(),
50        }
51    }
52
53    /// Build a stub by parsing a single expression from source — the
54    /// *boundary parse* for user-authored predicate text. Thereafter
55    /// the stub is grammar-safe (it flows as AST, never re-rendered to
56    /// a string).
57    pub fn parse(name: impl Into<String>, source: &str) -> Result<Self, String> {
58        let tokens = crate::dsl::lexer::lex(source)?;
59        let expr = crate::dsl::parser::parse_expression(tokens)?;
60        Ok(Self::new(name, expr))
61    }
62
63    /// Coerce the stub's value to `T`'s polydat type via the
64    /// `as <type>` cast (polydat_grammar.md §10) — alignment-only, a no-op when the
65    /// expression is already `T::PORT`. The Rust generic *is* the
66    /// polydat target type.
67    pub fn returning<T: Wire>(mut self) -> Self {
68        self.expr = Expr::Cast(Box::new(self.expr), T::PORT, Span { line: 0, col: 0 });
69        self
70    }
71
72    /// Mark the binding `volatile` — re-evaluated on every pull (e.g. a
73    /// stop-condition predicate evaluated per trigger).
74    pub fn volatile(mut self) -> Self {
75        self.modifier.insert(WireModifier::Volatile);
76        self
77    }
78
79    /// The binding statement this stub becomes — drop it into a
80    /// `BodyFragment::Statements` and the kernel builder consumes it
81    /// directly, no re-parse.
82    pub fn into_statement(self) -> Statement {
83        Statement::Binding(Binding {
84            targets: vec![self.name],
85            value: self.expr,
86            modifier: self.modifier,
87            type_annotation: None,
88            span: Span { line: 0, col: 0 },
89        })
90    }
91}
92
93/// **Shape 1** — grammar-safe *graph matter*: a bundle of
94/// statements the polydat kernel compiler turns into a kernel. Built
95/// programmatically (typed externs + `ExprStub` bindings), never from a
96/// source string. Feeds `PolydatMatter` / `BodyFragment::Statements`.
97#[derive(Default)]
98pub struct GraphMatter {
99    statements: Vec<Statement>,
100}
101
102impl GraphMatter {
103    /// An empty statement list.
104    pub fn new() -> Self {
105        Self::default()
106    }
107
108    /// Declare a typed `extern` wire (a runtime input the kernel reads),
109    /// **constructed** from the `Wire` type — not string-parsed. The
110    /// Rust generic *is* the polydat port type.
111    pub fn extern_wire<T: Wire>(&mut self, name: impl Into<String>) -> &mut Self {
112        self.extern_wire_typed(name, T::PORT)
113    }
114
115    /// [`Self::extern_wire`] with the port type as a runtime value, for
116    /// callers whose type comes from inspection (e.g. a parent scope's
117    /// [`SharedCellEntry`](crate::kernel::SharedCellEntry)
118    /// `port_type`) rather than a compile-time generic. The type must be
119    /// faithful: an extern that names an in-scope shared cell attaches to
120    /// it at subscope build, and the cell's own port type is the contract.
121    ///
122    /// The extern's default is its own type's zero value: `0` for
123    /// `u64`, `0.0` for `f64`, `""` for `str`, and `false` for `bool`.
124    /// A type with no literal form declares no default, so the extern
125    /// reads `None` until a value is set.
126    pub fn extern_wire_typed(&mut self, name: impl Into<String>, port: PortType) -> &mut Self {
127        let span = Span { line: 0, col: 0 };
128        let default = match port {
129            PortType::U64 => Some(Expr::IntLit(0, span)),
130            PortType::F64 => Some(Expr::FloatLit(0.0, span)),
131            PortType::Str => Some(Expr::StringLit(String::new(), span)),
132            PortType::Bool => Some(Expr::Ident("false".into(), span)),
133            _ => None,
134        };
135        self.statements.push(Statement::ExternPort(ExternPort {
136            name: name.into(),
137            typ: port.to_keyword().to_string(),
138            default,
139            span,
140        }));
141        self
142    }
143
144    /// Append an [`ExprStub`]'s binding statement.
145    pub fn bind(&mut self, stub: ExprStub) -> &mut Self {
146        self.statements.push(stub.into_statement());
147        self
148    }
149
150    /// The statements, for `PolydatMatter::builder().statements(...)` or
151    /// a `BodyFragment::Statements`.
152    pub fn into_statements(self) -> Vec<Statement> {
153        self.statements
154    }
155}
156
157/// **Shape 2** — a polydat expression *bound to a parent
158/// kernel's lexical scope*. Compiled into a sub-context whose named
159/// output is the expression, evaluable many times against injected
160/// inputs. The return is whatever `Wire` type the bound stub was
161/// qualified with (`ExprStub::returning::<T>`), or its natural
162/// truthiness (`is_true`). A general-purpose, scope-bound, callable
163/// expression holder.
164pub struct ScopedExpr {
165    kernel: Box<dyn crate::kernel::Kernel>,
166    output: String,
167}
168
169impl ScopedExpr {
170    /// Bind `matter` — which must define the named `output` (plus any
171    /// extern wires it reads) — into a sub-context of `parent`, on
172    /// `parent`'s engine. The expression is compiled once; call it
173    /// repeatedly via `eval` / `is_true` after `set`-ing its inputs.
174    pub fn bind(
175        parent: &dyn crate::kernel::Kernel,
176        output: impl Into<String>,
177        matter: GraphMatter,
178    ) -> Result<Self, String> {
179        let pm = crate::kernel::subcontext::PolydatMatter::builder()
180            .statements(matter.into_statements())
181            .build()
182            .map_err(|e| format!("scoped-expr matter: {e:?}"))?;
183        let kernel = pm
184            .build_under(parent)
185            .map_err(|e| format!("scoped-expr subscope: {e:?}"))?;
186        Ok(Self {
187            kernel,
188            output: output.into(),
189        })
190    }
191
192    /// Set a runtime input wire by name before evaluating, converted to
193    /// the wire's type by the one conversion rule
194    /// ([`crate::convert::to_port`]).
195    ///
196    /// Refused with the error [`Kernel::set_input`](crate::Kernel::set_input)
197    /// gives: [`WriteError::UnknownWire`](crate::kernel::WriteError) for a
198    /// name the expression has no input for,
199    /// [`WriteError::CoordinateSlot`](crate::kernel::WriteError) for a
200    /// coordinate, [`WriteError::TypeMismatch`](crate::kernel::WriteError)
201    /// for a value that does not convert to the wire's type, and
202    /// [`WriteError::ConstSlot`](crate::kernel::WriteError) for a const.
203    pub fn set(
204        &mut self,
205        name: &str,
206        value: Value,
207    ) -> Result<&mut Self, crate::kernel::WriteError> {
208        use crate::kernel::WriteError;
209        let Some(idx) = self.kernel.input_index(name) else {
210            return Err(WriteError::UnknownWire {
211                key: name.to_string(),
212                known: self.kernel.input_names(),
213            });
214        };
215        if idx < self.kernel.coord_count() {
216            return Err(WriteError::CoordinateSlot {
217                slot: name.to_string(),
218            });
219        }
220        let value = match self.kernel.input_port_type(name) {
221            Some(ty) => {
222                let got = value.port_type();
223                crate::convert::to_port(value, ty).map_err(|_| WriteError::TypeMismatch {
224                    slot: name.to_string(),
225                    expected: ty,
226                    got,
227                })?
228            }
229            None => value,
230        };
231        self.kernel.set_input_at(idx, value)?;
232        Ok(self)
233    }
234
235    /// The bound sub-context, for callers that write a batch of inputs
236    /// into it (e.g. a runtime-state snapshot) before evaluating.
237    pub fn kernel(&mut self) -> &mut dyn crate::kernel::Kernel {
238        self.kernel.as_mut()
239    }
240
241    /// Evaluate (pull) the bound expression's output.
242    pub fn eval(&mut self) -> Value {
243        self.kernel.pull(&self.output)
244    }
245
246    /// Evaluate as a boolean — the default truthiness sense. Polydat
247    /// comparisons / `&&` / `||` yield `U64` `0/1` (not `Bool`), so
248    /// truthiness is "non-zero".
249    pub fn is_true(&mut self) -> bool {
250        match self.eval() {
251            Value::Bool(b) => b,
252            Value::F64(v) => v != 0.0,
253            v => v.as_u64() != 0,
254        }
255    }
256}
257
258#[cfg(test)]
259mod tests {
260    use super::*;
261    use crate::ast::PortType;
262
263    #[test]
264    fn parse_stub_builds_typed_volatile_binding() {
265        // A predicate parsed at the boundary, coerced to u64 truthiness,
266        // marked volatile — the stop-condition stub shape.
267        let stmt = ExprStub::parse("__pred", "op_count > 50")
268            .expect("parse")
269            .returning::<u64>()
270            .volatile()
271            .into_statement();
272        match stmt {
273            Statement::Binding(b) => {
274                assert_eq!(b.targets, vec!["__pred".to_string()]);
275                assert!(b.modifier.has(WireModifier::Volatile), "must be volatile");
276                // The value is `(<comparison>) as u64` — a grammar-safe
277                // Cast wrapping the parsed comparison, no string round-trip.
278                assert!(
279                    matches!(b.value, Expr::Cast(_, PortType::U64, _)),
280                    "value must be a Cast to U64, got {:?}",
281                    b.value
282                );
283            }
284            other => panic!("expected a Binding statement, got {other:?}"),
285        }
286    }
287
288    #[test]
289    fn returning_binds_the_rust_generic_as_the_polydat_type() {
290        // The Rust generic and the polydat target are the same: f64 here.
291        let stmt = ExprStub::parse("__m", "elapsed_ms")
292            .expect("parse")
293            .returning::<f64>()
294            .into_statement();
295        let Statement::Binding(b) = stmt else {
296            panic!("expected Binding")
297        };
298        assert!(matches!(b.value, Expr::Cast(_, PortType::F64, _)));
299        assert!(
300            !b.modifier.has(WireModifier::Volatile),
301            "no volatile unless requested"
302        );
303    }
304}