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}