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}