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/// 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}