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//! SRD-84 Part 3 — 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//! (SRD-84 Part 2) — **without** concatenating source strings. The
21//! return type is bound at the call site via the SRD-80b [`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;
33use crate::kernel::{Dataflow, Metadata, PolydatKernel};
34
35/// A caller-native expression stub: a named binding over a polydat
36/// expression, optionally type-coerced (via the SRD-84 Part 1b `as`
37/// cast) and `volatile`.
38pub struct ExprStub {
39    name: String,
40    expr: Expr,
41    modifier: BindingModifier,
42}
43
44impl ExprStub {
45    /// Build a stub from an already-constructed expression.
46    pub fn new(name: impl Into<String>, expr: Expr) -> Self {
47        Self {
48            name: name.into(),
49            expr,
50            modifier: BindingModifier::default(),
51        }
52    }
53
54    /// Build a stub by parsing a single expression from source — the
55    /// *boundary parse* for user-authored predicate text. Thereafter
56    /// the stub is grammar-safe (it flows as AST, never re-rendered to
57    /// a string).
58    pub fn parse(name: impl Into<String>, source: &str) -> Result<Self, String> {
59        let tokens = crate::dsl::lexer::lex(source)?;
60        let expr = crate::dsl::parser::parse_expression(tokens)?;
61        Ok(Self::new(name, expr))
62    }
63
64    /// Coerce the stub's value to `T`'s polydat type via the SRD-84
65    /// Part 1b `as <type>` cast — alignment-only, a no-op when the
66    /// expression is already `T::PORT`. The Rust generic *is* the
67    /// polydat target type.
68    pub fn returning<T: Wire>(mut self) -> Self {
69        self.expr = Expr::Cast(Box::new(self.expr), T::PORT, Span { line: 0, col: 0 });
70        self
71    }
72
73    /// Mark the binding `volatile` — re-evaluated on every pull (e.g. a
74    /// stop-condition predicate evaluated per trigger).
75    pub fn volatile(mut self) -> Self {
76        self.modifier.insert(WireModifier::Volatile);
77        self
78    }
79
80    /// The binding statement this stub becomes — drop it into a
81    /// `BodyFragment::Statements` and the kernel builder consumes it
82    /// directly, no re-parse.
83    pub fn into_statement(self) -> Statement {
84        Statement::Binding(Binding {
85            targets: vec![self.name],
86            value: self.expr,
87            modifier: self.modifier,
88            type_annotation: None,
89            span: Span { line: 0, col: 0 },
90        })
91    }
92}
93
94/// SRD-84 **shape 1** — grammar-safe *graph matter*: a bundle of
95/// statements the polydat kernel compiler turns into a kernel. Built
96/// programmatically (typed externs + `ExprStub` bindings), never from a
97/// source string. Feeds `PolydatMatter` / `BodyFragment::Statements`.
98#[derive(Default)]
99pub struct GraphMatter {
100    statements: Vec<Statement>,
101}
102
103impl GraphMatter {
104    /// An empty statement list.
105    pub fn new() -> Self {
106        Self::default()
107    }
108
109    /// Declare a typed `extern` wire (a runtime input the kernel reads),
110    /// **constructed** from the `Wire` type — not string-parsed. The
111    /// Rust generic *is* the polydat port type.
112    pub fn extern_wire<T: Wire>(&mut self, name: impl Into<String>) -> &mut Self {
113        self.extern_wire_typed(name, T::PORT)
114    }
115
116    /// [`Self::extern_wire`] with the port type as a runtime value, for
117    /// callers whose type comes from inspection (e.g. a parent scope's
118    /// [`SharedCellEntry`](crate::kernel::SharedCellEntry)
119    /// `port_type`) rather than a compile-time generic. The type must be
120    /// faithful: an extern that names an in-scope shared cell attaches to
121    /// it at subscope build, and the cell's own port type is the contract.
122    pub fn extern_wire_typed(&mut self, name: impl Into<String>, port: PortType) -> &mut Self {
123        let span = Span { line: 0, col: 0 };
124        let default = match port {
125            PortType::F64 => Expr::FloatLit(0.0, span),
126            _ => Expr::IntLit(0, span),
127        };
128        self.statements.push(Statement::ExternPort(ExternPort {
129            name: name.into(),
130            typ: port.to_keyword().to_string(),
131            default: Some(default),
132            span,
133        }));
134        self
135    }
136
137    /// Append an [`ExprStub`]'s binding statement.
138    pub fn bind(&mut self, stub: ExprStub) -> &mut Self {
139        self.statements.push(stub.into_statement());
140        self
141    }
142
143    /// The statements, for `PolydatMatter::builder().statements(...)` or
144    /// a `BodyFragment::Statements`.
145    pub fn into_statements(self) -> Vec<Statement> {
146        self.statements
147    }
148}
149
150/// SRD-84 **shape 2** — a polydat expression *bound to a parent
151/// kernel's lexical scope*. Compiled into a sub-context whose named
152/// output is the expression, evaluable many times against injected
153/// inputs. The return is whatever `Wire` type the bound stub was
154/// qualified with (`ExprStub::returning::<T>`), or its natural
155/// truthiness (`is_true`). A general-purpose, scope-bound, callable
156/// expression holder.
157pub struct ScopedExpr {
158    kernel: PolydatKernel,
159    output: String,
160}
161
162impl ScopedExpr {
163    /// Bind `matter` — which must define the named `output` (plus any
164    /// extern wires it reads) — into a sub-context of `parent`. The
165    /// expression is compiled once; call it repeatedly via `eval` /
166    /// `is_true` after `set`-ing its inputs.
167    pub fn bind(
168        parent: &PolydatKernel,
169        output: impl Into<String>,
170        matter: GraphMatter,
171    ) -> Result<Self, String> {
172        let pm = crate::kernel::subcontext::PolydatMatter::builder()
173            .statements(matter.into_statements())
174            .build()
175            .map_err(|e| format!("scoped-expr matter: {e:?}"))?;
176        let kernel = parent
177            .build_subscope(pm)
178            .map_err(|e| format!("scoped-expr subscope: {e:?}"))?;
179        Ok(Self {
180            kernel,
181            output: output.into(),
182        })
183    }
184
185    /// Set a runtime input wire by name before evaluating. No-op for a
186    /// name the expression doesn't read.
187    pub fn set(&mut self, name: &str, value: Value) -> &mut Self {
188        if let Some(idx) = self.kernel.find_input(name) {
189            let _ = self.kernel.set_wire_idx(idx, value);
190        }
191        self
192    }
193
194    /// The bound sub-context as a [`Dataflow`], for callers that inject
195    /// a batch of inputs through a `Dataflow`-based injector (e.g. a
196    /// runtime-state snapshot) before evaluating.
197    pub fn dataflow(&mut self) -> &mut PolydatKernel {
198        &mut self.kernel
199    }
200
201    /// Evaluate (pull) the bound expression's output.
202    pub fn eval(&mut self) -> Value {
203        self.kernel.pull(&self.output).clone()
204    }
205
206    /// Evaluate as a boolean — the default truthiness sense. Polydat
207    /// comparisons / `&&` / `||` yield `U64` `0/1` (not `Bool`), so
208    /// truthiness is "non-zero".
209    pub fn is_true(&mut self) -> bool {
210        match self.eval() {
211            Value::Bool(b) => b,
212            Value::F64(v) => v != 0.0,
213            v => v.as_u64() != 0,
214        }
215    }
216}
217
218#[cfg(test)]
219mod tests {
220    use super::*;
221    use crate::ast::PortType;
222
223    #[test]
224    fn parse_stub_builds_typed_volatile_binding() {
225        // A predicate parsed at the boundary, coerced to u64 truthiness,
226        // marked volatile — the stop-condition stub shape.
227        let stmt = ExprStub::parse("__pred", "op_count > 50")
228            .expect("parse")
229            .returning::<u64>()
230            .volatile()
231            .into_statement();
232        match stmt {
233            Statement::Binding(b) => {
234                assert_eq!(b.targets, vec!["__pred".to_string()]);
235                assert!(b.modifier.has(WireModifier::Volatile), "must be volatile");
236                // The value is `(<comparison>) as u64` — a grammar-safe
237                // Cast wrapping the parsed comparison, no string round-trip.
238                assert!(
239                    matches!(b.value, Expr::Cast(_, PortType::U64, _)),
240                    "value must be a Cast to U64, got {:?}",
241                    b.value
242                );
243            }
244            other => panic!("expected a Binding statement, got {other:?}"),
245        }
246    }
247
248    #[test]
249    fn returning_binds_the_rust_generic_as_the_polydat_type() {
250        // The Rust generic and the polydat target are the same: f64 here.
251        let stmt = ExprStub::parse("__m", "elapsed_ms")
252            .expect("parse")
253            .returning::<f64>()
254            .into_statement();
255        let Statement::Binding(b) = stmt else {
256            panic!("expected Binding")
257        };
258        assert!(matches!(b.value, Expr::Cast(_, PortType::F64, _)));
259        assert!(
260            !b.modifier.has(WireModifier::Volatile),
261            "no volatile unless requested"
262        );
263    }
264}