polydat_core/dsl/transform.rs
1// Copyright 2024-2026 Jonathan Shook
2// SPDX-License-Identifier: Apache-2.0
3
4//! Program transforms: host intentions expressed as changes to the
5//! program rather than as runtime behavior around it.
6//!
7//! A host that wants to inject a value, observe a scope, or otherwise
8//! shape a run does so by rewriting the program and compiling the
9//! result. The program's own typing, lifecycle classification, and
10//! engine selection then apply to the host's additions exactly as they
11//! apply to the author's.
12
13use super::ast::{Expr, ExternPort, PolydatFile, Statement, TileDef, TilePiece};
14
15/// Assign `name=value` text to externs and inputs by rewriting their
16/// declarations.
17///
18/// - An `extern name: T` gets `"value"` as its default. The compiler
19/// fuses the string literal to `T` through the same coercions that
20/// apply when a str wire feeds a typed port, so a bad value is a
21/// compile error carrying the program's own diagnostic.
22/// - An `input name: T` becomes `extern name: T = "value"`: for this
23/// run the coordinate is fixed, so it is no longer a coordinate.
24/// - A name that is neither is an error listing what the program
25/// declares.
26///
27/// The transform is order preserving and leaves every other statement
28/// untouched.
29pub fn assign_values(
30 file: &mut PolydatFile,
31 assignments: &[(String, String)],
32) -> Result<(), String> {
33 for (name, raw) in assignments {
34 let mut found = false;
35 // Written over the statement walker, so the transform is the
36 // rewrite and the walk is not its business. It assigns at this
37 // scope only: a name inside a module or a `for` body belongs to
38 // that scope, and the walker would reach it.
39 each_statement::<std::convert::Infallible>(&mut file.statements, &mut |stmt| {
40 match stmt {
41 Statement::ExternPort(port) if &port.name == name => {
42 port.default = Some(Expr::StringLit(raw.clone(), port.span));
43 found = true;
44 }
45 Statement::InputDecl(decl) if &decl.name == name => {
46 let span = decl.span;
47 let typ = decl.ty.clone().unwrap_or_else(|| "u64".to_string());
48 *stmt = Statement::ExternPort(ExternPort {
49 name: name.clone(),
50 typ,
51 default: Some(Expr::StringLit(raw.clone(), span)),
52 span,
53 });
54 found = true;
55 }
56 _ => {}
57 }
58 Ok(())
59 })
60 .expect("the closure never fails");
61 if !found {
62 let declared: Vec<&str> = file
63 .statements
64 .iter()
65 .filter_map(|s| match s {
66 Statement::ExternPort(p) => Some(p.name.as_str()),
67 Statement::InputDecl(d) => Some(d.name.as_str()),
68 _ => None,
69 })
70 .collect();
71 return Err(format!(
72 "cannot assign '{name}': no extern or input by that name; declared: {}",
73 if declared.is_empty() {
74 "(none)".to_string()
75 } else {
76 declared.join(", ")
77 }
78 ));
79 }
80 }
81 Ok(())
82}
83
84/// Split `name=value` text into its parts.
85pub fn parse_assignment(text: &str) -> Result<(String, String), String> {
86 let (name, value) = text
87 .split_once('=')
88 .ok_or_else(|| format!("expected NAME=VALUE, got '{text}'"))?;
89 let name = name.trim();
90 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
91 return Err(format!("'{name}' is not a valid wire name in '{text}'"));
92 }
93 Ok((name.to_string(), value.trim().to_string()))
94}
95
96/// Add `tiles` a host built from what it holds to the program, as
97/// `tile` statements at its top level.
98///
99/// A tile a host supplies is the same thing as a tile the author
100/// wrote: it is one `tile` statement either way, and from here on the
101/// program's own typing, lifecycle classification and engine selection
102/// apply to it exactly as they apply to the author's. That is why this
103/// is a transform and not a compile entry point — a host that also
104/// wants [`assign_values`], or a rewrite of its own, applies them to
105/// the same tree in whatever order it means, and compiles once.
106///
107/// A tile whose name the program already declares is an error naming
108/// it, rather than a second declaration for the compiler to resolve.
109/// The check reaches module and `for` bodies, so a host cannot shadow
110/// a nested tile without knowing it.
111pub fn add_tiles(file: &mut PolydatFile, tiles: Vec<TileDef>) -> Result<(), String> {
112 let mut declared: Vec<String> = Vec::new();
113 each_tile::<std::convert::Infallible>(&mut file.statements, &mut |tile| {
114 declared.push(tile.name.clone());
115 Ok(())
116 })
117 .expect("the closure never fails");
118 for tile in &tiles {
119 if declared.contains(&tile.name) {
120 return Err(format!(
121 "the program already declares a tile named '{}'; a host tile cannot \
122 replace one the program declares. Rename the host tile, or rewrite \
123 the declared one in place with `tile_named`.",
124 tile.name,
125 ));
126 }
127 declared.push(tile.name.clone());
128 }
129 file.statements
130 .extend(tiles.into_iter().map(Statement::Tile));
131 Ok(())
132}
133
134// ── Addressing a subtree ────────────────────────────────────────────
135//
136// A transform reads and rewrites the parsed program, never its source
137// text: the text is what produced the tree and has no authority over
138// it afterwards. These walk the two nesting axes so a transform can be
139// written once and applied to a whole program or to one part of it.
140//
141// Statements nest through module bodies and `for` bodies; a template's
142// pieces nest through projection bodies and branch arms. Each walker
143// takes the slice to walk rather than the file, so the caller chooses
144// the subtree: pass `&mut file.statements` for the program, a module's
145// `body` for that module, or one tile's `pieces` for that tile.
146
147/// Apply `f` to every statement in `statements`, then to every
148/// statement nested in a module body or a `for` body, depth first.
149///
150/// `f` sees a statement before its own nested bodies are walked, so a
151/// transform may rewrite a statement and have the walk continue into
152/// what it wrote.
153pub fn each_statement<E>(
154 statements: &mut [Statement],
155 f: &mut impl FnMut(&mut Statement) -> Result<(), E>,
156) -> Result<(), E> {
157 for stmt in statements.iter_mut() {
158 f(stmt)?;
159 match stmt {
160 Statement::ModuleDef(m) => each_statement(&mut m.body, f)?,
161 Statement::For(s) => each_statement(&mut s.body, f)?,
162 _ => {}
163 }
164 }
165 Ok(())
166}
167
168/// Apply `f` to every tile the subtree declares, module and `for`
169/// bodies included.
170pub fn each_tile<E>(
171 statements: &mut [Statement],
172 f: &mut impl FnMut(&mut TileDef) -> Result<(), E>,
173) -> Result<(), E> {
174 each_statement(statements, &mut |stmt| match stmt {
175 Statement::Tile(t) => f(t),
176 _ => Ok(()),
177 })
178}
179
180/// The tile bound to `name` in the subtree, for a transform qualified
181/// to one definition.
182pub fn tile_named<'a>(statements: &'a mut [Statement], name: &str) -> Option<&'a mut TileDef> {
183 for stmt in statements.iter_mut() {
184 match stmt {
185 Statement::Tile(t) if t.name == name => return Some(t),
186 Statement::ModuleDef(m) => {
187 if let Some(t) = tile_named(&mut m.body, name) {
188 return Some(t);
189 }
190 }
191 Statement::For(s) => {
192 if let Some(t) = tile_named(&mut s.body, name) {
193 return Some(t);
194 }
195 }
196 _ => {}
197 }
198 }
199 None
200}
201
202/// Apply `f` to every piece of a template, then to every piece nested
203/// in a projection body or a branch arm, depth first.
204///
205/// Rewriting a piece rewrites the tile: a tile's text is rendered from
206/// its pieces, so what the program projects after a transform is what
207/// it renders, with no second copy to keep in step.
208pub fn each_piece<E>(
209 pieces: &mut [TilePiece],
210 f: &mut impl FnMut(&mut TilePiece) -> Result<(), E>,
211) -> Result<(), E> {
212 for piece in pieces.iter_mut() {
213 f(piece)?;
214 match piece {
215 TilePiece::Projection { body, .. } => each_piece(body, f)?,
216 TilePiece::Branch {
217 then, otherwise, ..
218 } => {
219 each_piece(then, f)?;
220 if let Some(arm) = otherwise {
221 each_piece(arm, f)?;
222 }
223 }
224 TilePiece::Static(_) | TilePiece::Hole(_) => {}
225 }
226 }
227 Ok(())
228}
229
230/// What a scope of a parsed program binds, by name.
231///
232/// A host that rewrites a program before compiling it often needs to
233/// know what the program will call things — which wires a scope has,
234/// so an added binding can name them. The compiled program knows
235/// exactly; a host that only has the source would otherwise work it
236/// out again, and a second implementation of "what does this scope
237/// bind" is a second answer to a question with one.
238///
239/// `declared_wires` is that answer, read from the AST. It is checked
240/// against the compiler's own in the suite.
241#[derive(Debug, Clone, Default, PartialEq, Eq)]
242pub struct ScopeWires {
243 /// Names bound in this scope, in declaration order: bindings,
244 /// tiles, and — inside a traversal body — the element names the
245 /// traversal's source binds.
246 pub names: Vec<String>,
247}
248
249impl ScopeWires {
250 fn push(&mut self, name: &str) {
251 if !name.starts_with("__") && !self.names.iter().any(|n| n == name) {
252 self.names.push(name.to_string());
253 }
254 }
255
256 /// Whether this scope binds `name`.
257 pub fn binds(&self, name: &str) -> bool {
258 self.names.iter().any(|n| n == name)
259 }
260}
261
262/// Every scope of a parsed program, by name.
263#[derive(Debug, Clone, Default, PartialEq, Eq)]
264pub struct DeclaredWires {
265 /// The root scope's bindings and tiles.
266 pub root: ScopeWires,
267 /// The names the root declares as inputs or extern ports. Each is
268 /// a slot and a passthrough output under the same name, so it can
269 /// be read like a binding; it is listed separately because a host
270 /// selecting "the program's outputs" usually means the bindings.
271 pub inputs: Vec<String>,
272 /// One entry per top-level `for` statement, in source order.
273 pub traversal_bodies: Vec<ScopeWires>,
274}
275
276/// Read [`DeclaredWires`] from a parsed program.
277///
278/// A traversal body's element names come from its source: a
279/// comprehension binds its own, a bare producer name or a derivation
280/// of one binds the producer's, which is resolved against the
281/// bindings of the enclosing scope.
282pub fn declared_wires(file: &PolydatFile) -> DeclaredWires {
283 let mut out = DeclaredWires::default();
284 for stmt in &file.statements {
285 match stmt {
286 Statement::Binding(b) => {
287 for t in &b.targets {
288 out.root.push(t);
289 }
290 }
291 Statement::Tile(t) => out.root.push(&t.name),
292 Statement::InputDecl(i) => out.inputs.push(i.name.clone()),
293 Statement::ExternPort(p) => out.inputs.push(p.name.clone()),
294 _ => {}
295 }
296 }
297 // Bodies in a second pass, because a body sees the scope that
298 // encloses it: an outer wire the body names cascades in as an
299 // input of the child program and is readable there under the same
300 // name. Its own names come first, so a listing reads body-first.
301 for stmt in &file.statements {
302 if let Statement::For(f) = stmt {
303 let mut body = ScopeWires::default();
304 for name in source_element_names(&f.source, file) {
305 body.push(&name);
306 }
307 collect_scope(&f.body, file, &mut body);
308 for name in out.root.names.iter().chain(out.inputs.iter()) {
309 body.push(name);
310 }
311 out.traversal_bodies.push(body);
312 }
313 }
314 out
315}
316
317/// The bindings and tiles of a statement list, appended to `into`.
318fn collect_scope(body: &[Statement], file: &PolydatFile, into: &mut ScopeWires) {
319 for stmt in body {
320 match stmt {
321 Statement::Binding(b) => {
322 for t in &b.targets {
323 into.push(t);
324 }
325 }
326 Statement::Tile(t) => into.push(&t.name),
327 Statement::For(f) => {
328 // A nested traversal is its own scope; its element
329 // names and bindings do not reach this one.
330 let _ = (f, file);
331 }
332 _ => {}
333 }
334 }
335}
336
337/// The element names a `for` source binds.
338fn source_element_names(source: &crate::dsl::ast::ForSource, file: &PolydatFile) -> Vec<String> {
339 use crate::dsl::ast::ForSourceKind;
340 match &source.kind {
341 ForSourceKind::Comprehension(c) => {
342 c.coordinate_specs().into_iter().map(|(v, _)| v).collect()
343 }
344 ForSourceKind::Producer(name) => producer_element_names(name, file),
345 ForSourceKind::Derived { base, .. } => producer_element_names(base, file),
346 }
347}
348
349/// The element names of the producer bound to `name` at the root:
350/// `name := for <comprehension>`, or a derivation of another producer,
351/// which is followed to its base.
352fn producer_element_names(name: &str, file: &PolydatFile) -> Vec<String> {
353 let mut seen = 0usize;
354 let mut target = name.to_string();
355 // A derivation chain is finite; the bound is the statement count,
356 // so a cycle a malformed program could write ends the walk rather
357 // than spinning.
358 while seen <= file.statements.len() {
359 seen += 1;
360 let found = file.statements.iter().find_map(|s| match s {
361 Statement::Binding(b) if b.targets.contains(&target) => match &b.value {
362 Expr::For(src) => Some(src),
363 _ => None,
364 },
365 _ => None,
366 });
367 let Some(src) = found else { return Vec::new() };
368 use crate::dsl::ast::ForSourceKind;
369 match &src.kind {
370 ForSourceKind::Comprehension(c) => {
371 return c.coordinate_specs().into_iter().map(|(v, _)| v).collect();
372 }
373 ForSourceKind::Producer(next) | ForSourceKind::Derived { base: next, .. } => {
374 target = next.clone();
375 }
376 }
377 }
378 Vec::new()
379}