Skip to main content

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}