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/// Let the program's `extern name` take a value of any type, converted
85/// in front of each reader by a converter node (input_variance.md §6).
86///
87/// The extern is declared `dyn`; the compiler places one converter per
88/// type its readers read, reports each in the compile log, and a value
89/// the converter cannot convert fails the converter, attributed as any
90/// node's failure. For one input a host knows varies, without opening
91/// every input the author left untyped. A name that is not a top-level
92/// `extern` is an error: a coordinate is positioned with `set_inputs`
93/// and never varies in type.
94pub fn convert_input(file: &mut PolydatFile, name: &str) -> Result<(), String> {
95 for stmt in &mut file.statements {
96 if let Statement::ExternPort(port) = stmt
97 && port.name == name
98 {
99 port.typ = crate::ast::PortType::Dyn.to_keyword().to_string();
100 return Ok(());
101 }
102 }
103 let externs: Vec<&str> = file
104 .statements
105 .iter()
106 .filter_map(|s| match s {
107 Statement::ExternPort(p) => Some(p.name.as_str()),
108 _ => None,
109 })
110 .collect();
111 Err(format!(
112 "cannot convert '{name}': no extern by that name; declared externs: {}",
113 if externs.is_empty() {
114 "(none)".to_string()
115 } else {
116 externs.join(", ")
117 }
118 ))
119}
120
121/// Split `name=value` text into its parts.
122pub fn parse_assignment(text: &str) -> Result<(String, String), String> {
123 let (name, value) = text
124 .split_once('=')
125 .ok_or_else(|| format!("expected NAME=VALUE, got '{text}'"))?;
126 let name = name.trim();
127 if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
128 return Err(format!("'{name}' is not a valid wire name in '{text}'"));
129 }
130 Ok((name.to_string(), value.trim().to_string()))
131}
132
133/// Add `tiles` a host built from what it holds to the program, as
134/// `tile` statements at its top level.
135///
136/// A tile a host supplies is the same thing as a tile the author
137/// wrote: it is one `tile` statement either way, and from here on the
138/// program's own typing, lifecycle classification and engine selection
139/// apply to it exactly as they apply to the author's. That is why this
140/// is a transform and not a compile entry point — a host that also
141/// wants [`assign_values`], or a rewrite of its own, applies them to
142/// the same tree in whatever order it means, and compiles once.
143///
144/// A tile whose name the program already declares is an error naming
145/// it, rather than a second declaration for the compiler to resolve.
146/// The check reaches module and `for` bodies, so a host cannot shadow
147/// a nested tile without knowing it.
148pub fn add_tiles(file: &mut PolydatFile, tiles: Vec<TileDef>) -> Result<(), String> {
149 let mut declared: Vec<String> = Vec::new();
150 each_tile::<std::convert::Infallible>(&mut file.statements, &mut |tile| {
151 declared.push(tile.name.clone());
152 Ok(())
153 })
154 .expect("the closure never fails");
155 for tile in &tiles {
156 if declared.contains(&tile.name) {
157 return Err(format!(
158 "the program already declares a tile named '{}'; a host tile cannot \
159 replace one the program declares. Rename the host tile, or rewrite \
160 the declared one in place with `tile_named`.",
161 tile.name,
162 ));
163 }
164 declared.push(tile.name.clone());
165 }
166 file.statements
167 .extend(tiles.into_iter().map(Statement::Tile));
168 Ok(())
169}
170
171// ── Addressing a subtree ────────────────────────────────────────────
172//
173// A transform reads and rewrites the parsed program, never its source
174// text: the text is what produced the tree and has no authority over
175// it afterwards. These walk the two nesting axes so a transform can be
176// written once and applied to a whole program or to one part of it.
177//
178// Statements nest through module bodies and `for` bodies; a template's
179// pieces nest through projection bodies and branch arms. Each walker
180// takes the slice to walk rather than the file, so the caller chooses
181// the subtree: pass `&mut file.statements` for the program, a module's
182// `body` for that module, or one tile's `pieces` for that tile.
183
184/// Apply `f` to every statement in `statements`, then to every
185/// statement nested in a module body or a `for` body, depth first.
186///
187/// `f` sees a statement before its own nested bodies are walked, so a
188/// transform may rewrite a statement and have the walk continue into
189/// what it wrote.
190pub fn each_statement<E>(
191 statements: &mut [Statement],
192 f: &mut impl FnMut(&mut Statement) -> Result<(), E>,
193) -> Result<(), E> {
194 for stmt in statements.iter_mut() {
195 f(stmt)?;
196 match stmt {
197 Statement::ModuleDef(m) => each_statement(&mut m.body, f)?,
198 Statement::For(s) => each_statement(&mut s.body, f)?,
199 _ => {}
200 }
201 }
202 Ok(())
203}
204
205/// Apply `f` to every tile the subtree declares, module and `for`
206/// bodies included.
207pub fn each_tile<E>(
208 statements: &mut [Statement],
209 f: &mut impl FnMut(&mut TileDef) -> Result<(), E>,
210) -> Result<(), E> {
211 each_statement(statements, &mut |stmt| match stmt {
212 Statement::Tile(t) => f(t),
213 _ => Ok(()),
214 })
215}
216
217/// The tile bound to `name` in the subtree, for a transform qualified
218/// to one definition.
219pub fn tile_named<'a>(statements: &'a mut [Statement], name: &str) -> Option<&'a mut TileDef> {
220 for stmt in statements.iter_mut() {
221 match stmt {
222 Statement::Tile(t) if t.name == name => return Some(t),
223 Statement::ModuleDef(m) => {
224 if let Some(t) = tile_named(&mut m.body, name) {
225 return Some(t);
226 }
227 }
228 Statement::For(s) => {
229 if let Some(t) = tile_named(&mut s.body, name) {
230 return Some(t);
231 }
232 }
233 _ => {}
234 }
235 }
236 None
237}
238
239/// Apply `f` to every piece of a template, then to every piece nested
240/// in a projection body or a branch arm, depth first.
241///
242/// Rewriting a piece rewrites the tile: a tile's text is rendered from
243/// its pieces, so what the program projects after a transform is what
244/// it renders, with no second copy to keep in step.
245pub fn each_piece<E>(
246 pieces: &mut [TilePiece],
247 f: &mut impl FnMut(&mut TilePiece) -> Result<(), E>,
248) -> Result<(), E> {
249 for piece in pieces.iter_mut() {
250 f(piece)?;
251 match piece {
252 TilePiece::Projection { body, .. } => each_piece(body, f)?,
253 TilePiece::Branch {
254 then, otherwise, ..
255 } => {
256 each_piece(then, f)?;
257 if let Some(arm) = otherwise {
258 each_piece(arm, f)?;
259 }
260 }
261 TilePiece::Static(_) | TilePiece::Hole(_) => {}
262 }
263 }
264 Ok(())
265}
266
267/// What a scope of a parsed program binds, by name.
268///
269/// A host that rewrites a program before compiling it often needs to
270/// know what the program will call things — which wires a scope has,
271/// so an added binding can name them. The compiled program knows
272/// exactly; a host that only has the source would otherwise work it
273/// out again, and a second implementation of "what does this scope
274/// bind" is a second answer to a question with one.
275///
276/// `declared_wires` is that answer, read from the AST. It is checked
277/// against the compiler's own in the suite.
278#[derive(Debug, Clone, Default, PartialEq, Eq)]
279pub struct ScopeWires {
280 /// Names bound in this scope, in declaration order: bindings,
281 /// tiles, and — inside a traversal body — the element names the
282 /// traversal's source binds.
283 pub names: Vec<String>,
284}
285
286impl ScopeWires {
287 fn push(&mut self, name: &str) {
288 if !name.starts_with("__") && !self.names.iter().any(|n| n == name) {
289 self.names.push(name.to_string());
290 }
291 }
292
293 /// Whether this scope binds `name`.
294 pub fn binds(&self, name: &str) -> bool {
295 self.names.iter().any(|n| n == name)
296 }
297}
298
299/// Every scope of a parsed program, by name.
300#[derive(Debug, Clone, Default, PartialEq, Eq)]
301pub struct DeclaredWires {
302 /// The root scope's bindings and tiles.
303 pub root: ScopeWires,
304 /// The names the root declares as inputs or extern ports. Each is
305 /// a slot and a passthrough output under the same name, so it can
306 /// be read like a binding; it is listed separately because a host
307 /// selecting "the program's outputs" usually means the bindings.
308 pub inputs: Vec<String>,
309 /// One entry per top-level `for` statement, in source order.
310 pub traversal_bodies: Vec<ScopeWires>,
311}
312
313/// Read [`DeclaredWires`] from a parsed program.
314///
315/// A traversal body's element names come from its source: a
316/// comprehension binds its own, a bare producer name or a derivation
317/// of one binds the producer's, which is resolved against the
318/// bindings of the enclosing scope.
319pub fn declared_wires(file: &PolydatFile) -> DeclaredWires {
320 let mut out = DeclaredWires::default();
321 for stmt in &file.statements {
322 match stmt {
323 Statement::Binding(b) => {
324 for t in &b.targets {
325 out.root.push(t);
326 }
327 }
328 Statement::Tile(t) => out.root.push(&t.name),
329 Statement::InputDecl(i) => out.inputs.push(i.name.clone()),
330 Statement::ExternPort(p) => out.inputs.push(p.name.clone()),
331 _ => {}
332 }
333 }
334 // Bodies in a second pass, because a body sees the scope that
335 // encloses it: an outer wire the body names cascades in as an
336 // input of the child program and is readable there under the same
337 // name. Its own names come first, so a listing reads body-first.
338 for stmt in &file.statements {
339 if let Statement::For(f) = stmt {
340 let mut body = ScopeWires::default();
341 for name in source_element_names(&f.source, file) {
342 body.push(&name);
343 }
344 collect_scope(&f.body, file, &mut body);
345 for name in out.root.names.iter().chain(out.inputs.iter()) {
346 body.push(name);
347 }
348 out.traversal_bodies.push(body);
349 }
350 }
351 out
352}
353
354/// The bindings and tiles of a statement list, appended to `into`.
355fn collect_scope(body: &[Statement], file: &PolydatFile, into: &mut ScopeWires) {
356 for stmt in body {
357 match stmt {
358 Statement::Binding(b) => {
359 for t in &b.targets {
360 into.push(t);
361 }
362 }
363 Statement::Tile(t) => into.push(&t.name),
364 Statement::For(f) => {
365 // A nested traversal is its own scope; its element
366 // names and bindings do not reach this one.
367 let _ = (f, file);
368 }
369 _ => {}
370 }
371 }
372}
373
374/// The element names a `for` source binds.
375fn source_element_names(source: &crate::dsl::ast::ForSource, file: &PolydatFile) -> Vec<String> {
376 use crate::dsl::ast::ForSourceKind;
377 match &source.kind {
378 ForSourceKind::Comprehension(c) => {
379 c.coordinate_specs().into_iter().map(|(v, _)| v).collect()
380 }
381 ForSourceKind::Producer(name) => producer_element_names(name, file),
382 ForSourceKind::Derived { base, .. } => producer_element_names(base, file),
383 }
384}
385
386/// The element names of the producer bound to `name` at the root:
387/// `name := for <comprehension>`, or a derivation of another producer,
388/// which is followed to its base.
389fn producer_element_names(name: &str, file: &PolydatFile) -> Vec<String> {
390 let mut seen = 0usize;
391 let mut target = name.to_string();
392 // A derivation chain is finite; the bound is the statement count,
393 // so a cycle a malformed program could write ends the walk rather
394 // than spinning.
395 while seen <= file.statements.len() {
396 seen += 1;
397 let found = file.statements.iter().find_map(|s| match s {
398 Statement::Binding(b) if b.targets.contains(&target) => match &b.value {
399 Expr::For(src) => Some(src),
400 _ => None,
401 },
402 _ => None,
403 });
404 let Some(src) = found else { return Vec::new() };
405 use crate::dsl::ast::ForSourceKind;
406 match &src.kind {
407 ForSourceKind::Comprehension(c) => {
408 return c.coordinate_specs().into_iter().map(|(v, _)| v).collect();
409 }
410 ForSourceKind::Producer(next) | ForSourceKind::Derived { base: next, .. } => {
411 target = next.clone();
412 }
413 }
414 }
415 Vec::new()
416}