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}