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, TileOptions};
14
15/// Give every tile that declares no delimiters or sigil of its own the
16/// host's defaults (SRD 114 §5.6, §10), re-reading its body under them.
17///
18/// A tile that names any option keeps all of them; the transform only
19/// fills in what the author left to the default. Tiles inside module
20/// bodies are rewritten too, since they compile in this program.
21pub fn apply_tile_defaults(file: &mut PolydatFile, defaults: &TileOptions) -> Result<(), String> {
22    fn visit(statements: &mut [Statement], defaults: &TileOptions) -> Result<(), String> {
23        let stock = TileOptions::default();
24        for stmt in statements.iter_mut() {
25            match stmt {
26                Statement::Tile(t) => {
27                    let untouched = t.options.open == stock.open
28                        && t.options.close == stock.close
29                        && t.options.sigil == stock.sigil;
30                    if !untouched {
31                        continue;
32                    }
33                    let strict = t.options.strict;
34                    t.options = defaults.clone();
35                    t.options.strict = strict || defaults.strict;
36                    t.pieces = super::tile::parse_template(&t.body, &t.options, t.span)
37                        .map_err(|e| format!("tile '{}' under host delimiters: {e}", t.name))?;
38                }
39                Statement::ModuleDef(m) => visit(&mut m.body, defaults)?,
40                _ => {}
41            }
42        }
43        Ok(())
44    }
45    visit(&mut file.statements, defaults)
46}
47
48/// Assign `name=value` text to externs and inputs by rewriting their
49/// declarations.
50///
51/// - An `extern name: T` gets `"value"` as its default. The compiler
52///   fuses the string literal to `T` through the same coercions that
53///   apply when a str wire feeds a typed port, so a bad value is a
54///   compile error carrying the program's own diagnostic.
55/// - An `input name: T` becomes `extern name: T = "value"`: for this
56///   run the coordinate is fixed, so it is no longer a coordinate.
57/// - A name that is neither is an error listing what the program
58///   declares.
59///
60/// The transform is order preserving and leaves every other statement
61/// untouched.
62pub fn assign_values(
63    file: &mut PolydatFile,
64    assignments: &[(String, String)],
65) -> Result<(), String> {
66    for (name, raw) in assignments {
67        let mut found = false;
68        for stmt in file.statements.iter_mut() {
69            match stmt {
70                Statement::ExternPort(port) if &port.name == name => {
71                    port.default = Some(Expr::StringLit(raw.clone(), port.span));
72                    found = true;
73                }
74                Statement::InputDecl(decl) if &decl.name == name => {
75                    let span = decl.span;
76                    let typ = decl.ty.clone().unwrap_or_else(|| "u64".to_string());
77                    *stmt = Statement::ExternPort(ExternPort {
78                        name: name.clone(),
79                        typ,
80                        default: Some(Expr::StringLit(raw.clone(), span)),
81                        span,
82                    });
83                    found = true;
84                }
85                _ => {}
86            }
87        }
88        if !found {
89            let declared: Vec<&str> = file
90                .statements
91                .iter()
92                .filter_map(|s| match s {
93                    Statement::ExternPort(p) => Some(p.name.as_str()),
94                    Statement::InputDecl(d) => Some(d.name.as_str()),
95                    _ => None,
96                })
97                .collect();
98            return Err(format!(
99                "cannot assign '{name}': no extern or input by that name; declared: {}",
100                if declared.is_empty() {
101                    "(none)".to_string()
102                } else {
103                    declared.join(", ")
104                }
105            ));
106        }
107    }
108    Ok(())
109}
110
111/// Split `name=value` text into its parts.
112pub fn parse_assignment(text: &str) -> Result<(String, String), String> {
113    let (name, value) = text
114        .split_once('=')
115        .ok_or_else(|| format!("expected NAME=VALUE, got '{text}'"))?;
116    let name = name.trim();
117    if name.is_empty() || !name.chars().all(|c| c.is_ascii_alphanumeric() || c == '_') {
118        return Err(format!("'{name}' is not a valid wire name in '{text}'"));
119    }
120    Ok((name.to_string(), value.trim().to_string()))
121}