1use crate::ast::{
30 Arg, BinOpKind, Binding, BindingModifier, CallExpr, CursorDecl, Expr, ExternPort, ForStmt,
31 ModuleDef, PolydatFile, Statement, TileBodyKind, TileDef, TileOptions, WireModifier,
32};
33
34pub fn pp_file(file: &PolydatFile) -> String {
37 let mut out = String::new();
38 for stmt in &file.statements {
39 out.push_str(&pp_statement(stmt));
40 out.push('\n');
41 }
42 out
43}
44
45pub fn pp_statement(stmt: &Statement) -> String {
47 match stmt {
48 Statement::InputDecl(d) => match &d.ty {
49 Some(ty) => format!("input {}: {}", d.name, ty),
50 None => format!("input {}", d.name),
51 },
52 Statement::Binding(b) => pp_binding(b),
53 Statement::ModuleDef(m) => pp_module_def(m),
54 Statement::ExternPort(p) => pp_extern_port(p),
55 Statement::Cursor(c) => pp_cursor(c),
56 Statement::Pragma { name, .. } => format!("pragma {name}"),
57 Statement::For(f) => pp_for_stmt(f, 0),
58 Statement::Tile(t) => pp_tile(t),
59 }
60}
61
62fn pp_tile(t: &TileDef) -> String {
63 let mut out = format!("tile {}", t.name);
64 if let Some(enc) = &t.encoding {
65 out.push_str(&format!(" : {enc}"));
66 }
67 let defaults = TileOptions::default();
68 let mut opts = Vec::new();
69 if t.options.open != defaults.open || t.options.close != defaults.close {
70 opts.push(format!(
71 "delims \"{}\" \"{}\"",
72 escape_string(&t.options.open),
73 escape_string(&t.options.close)
74 ));
75 }
76 if t.options.sigil != defaults.sigil {
77 opts.push(format!("sigil \"{}\"", escape_string(&t.options.sigil)));
78 }
79 if t.options.strict {
80 opts.push("strict".to_string());
81 }
82 if t.options.in_string {
83 opts.push("instring".to_string());
84 }
85 if !opts.is_empty() {
86 out.push_str(&format!(" ({})", opts.join(", ")));
87 }
88 match t.body_kind {
90 TileBodyKind::Block => {
91 out.push_str(" := ");
92 out.push_str(&t.body);
93 }
94 TileBodyKind::Heredoc => {
95 out.push_str(" := <<<\n");
96 out.push_str(&t.body);
97 out.push_str("\n>>>");
98 }
99 TileBodyKind::Literal => out.push_str(&format!(" := \"{}\"", escape_string(&t.body))),
100 }
101 out
102}
103
104fn pp_for_stmt(f: &ForStmt, indent: usize) -> String {
105 let pad = " ".repeat(indent + 1);
106 let mut body = String::new();
107 for s in &f.body {
108 body.push_str(&pad);
109 body.push_str(&match s {
110 Statement::For(inner) => pp_for_stmt(inner, indent + 1),
111 other => pp_statement(other),
112 });
113 body.push('\n');
114 }
115 format!(
116 "for {} {{\n{}{}}}",
117 f.source.text,
118 body,
119 " ".repeat(indent)
120 )
121}
122
123pub fn pp_expr(expr: &Expr) -> String {
126 match expr {
127 Expr::Ident(name, _) => name.clone(),
128 Expr::IntLit(v, _) => v.to_string(),
129 Expr::FloatLit(v, _) => format_float(*v),
130 Expr::StringLit(s, _) => format!("\"{}\"", escape_string(s)),
131 Expr::ArrayLit(elts, _) => {
132 let parts: Vec<String> = elts.iter().map(pp_expr).collect();
133 format!("[{}]", parts.join(", "))
134 }
135 Expr::Call(c) => pp_call(c),
136 Expr::BinOp(lhs, op, rhs) => {
137 format!("({} {} {})", pp_expr(lhs), pp_binop(*op), pp_expr(rhs))
138 }
139 Expr::UnaryNeg(e, _) => format!("(-{})", pp_expr(e)),
140 Expr::UnaryBitNot(e, _) => format!("(!{})", pp_expr(e)),
141 Expr::FieldAccess { source, field, .. } => format!("{source}.{field}"),
142 Expr::Cast(e, ty, _) => format!("({} as {})", pp_expr(e), ty.to_keyword()),
143 Expr::For(source) => format!("for {}", source.text),
144 }
145}
146
147fn pp_binding(b: &Binding) -> String {
148 let mut target = if b.targets.len() == 1 {
149 b.targets[0].clone()
150 } else {
151 format!("({})", b.targets.join(", "))
152 };
153 if let Some(ty) = &b.type_annotation {
156 target = format!("{target}: {ty}");
157 }
158 let prefix = pp_modifier_prefix(b.modifier);
159 if prefix.is_empty() {
160 format!("{} := {}", target, pp_expr(&b.value))
161 } else {
162 format!("{} {} := {}", prefix, target, pp_expr(&b.value))
163 }
164}
165
166fn pp_extern_port(p: &ExternPort) -> String {
167 if let Some(default) = &p.default {
168 format!("extern {}: {} = {}", p.name, p.typ, pp_expr(default))
169 } else {
170 format!("extern {}: {}", p.name, p.typ)
171 }
172}
173
174fn pp_cursor(c: &CursorDecl) -> String {
175 let mut out = format!("cursor {} = {}", c.name, pp_expr(&c.constructor));
176 if let Some(over) = &c.over {
177 out.push_str(" over ");
178 out.push_str(&pp_expr(over));
179 }
180 out
181}
182
183fn pp_module_def(m: &ModuleDef) -> String {
184 let params: Vec<String> = m
185 .params
186 .iter()
187 .map(|p| format!("{}: {}", p.name, p.typ))
188 .collect();
189 let outputs: Vec<String> = m
190 .outputs
191 .iter()
192 .map(|p| format!("{}: {}", p.name, p.typ))
193 .collect();
194 let mut body = String::new();
195 for s in &m.body {
196 body.push_str(" ");
197 body.push_str(&pp_statement(s));
198 body.push('\n');
199 }
200 format!(
201 "{}({}) -> ({}) := {{\n{}}}",
202 m.name,
203 params.join(", "),
204 outputs.join(", "),
205 body
206 )
207}
208
209fn pp_call(c: &CallExpr) -> String {
210 let args: Vec<String> = c.args.iter().map(pp_arg).collect();
211 format!("{}({})", c.func, args.join(", "))
212}
213
214fn pp_arg(arg: &Arg) -> String {
215 match arg {
216 Arg::Positional(e) => pp_expr(e),
217 Arg::Named(name, e) => format!("{}: {}", name, pp_expr(e)),
218 }
219}
220
221fn pp_modifier_prefix(m: BindingModifier) -> String {
222 let mut parts: Vec<&str> = Vec::new();
223 if m.has(WireModifier::Const) {
224 parts.push("const");
225 }
226 if m.has(WireModifier::Shared) {
227 parts.push("shared");
228 }
229 if m.has(WireModifier::Volatile) {
230 parts.push("volatile");
231 }
232 parts.join(" ")
233}
234
235fn pp_binop(op: BinOpKind) -> &'static str {
236 match op {
237 BinOpKind::Add => "+",
238 BinOpKind::Sub => "-",
239 BinOpKind::Mul => "*",
240 BinOpKind::Div => "/",
241 BinOpKind::Mod => "%",
242 BinOpKind::Pow => "**",
243 BinOpKind::BitAnd => "&",
244 BinOpKind::BitOr => "|",
245 BinOpKind::BitXor => "^",
246 BinOpKind::Shl => "<<",
247 BinOpKind::Shr => ">>",
248 BinOpKind::Eq => "==",
249 BinOpKind::Ne => "!=",
250 BinOpKind::Lt => "<",
251 BinOpKind::Gt => ">",
252 BinOpKind::Le => "<=",
253 BinOpKind::Ge => ">=",
254 BinOpKind::And => "&&",
255 BinOpKind::Or => "||",
256 }
257}
258
259fn escape_string(s: &str) -> String {
260 let mut out = String::with_capacity(s.len());
261 for c in s.chars() {
262 match c {
263 '\\' => out.push_str("\\\\"),
264 '"' => out.push_str("\\\""),
265 '\n' => out.push_str("\\n"),
266 '\t' => out.push_str("\\t"),
267 '\r' => out.push_str("\\r"),
268 c => out.push(c),
269 }
270 }
271 out
272}
273
274fn format_float(v: f64) -> String {
275 if v.is_finite() && v == v.trunc() && v.abs() < 1e18 {
276 format!("{v:.1}")
277 } else {
278 format!("{v}")
279 }
280}
281
282#[cfg(test)]
283mod tests {
284 use super::*;
285 use crate::{lexer, parser};
286
287 fn parse(src: &str) -> PolydatFile {
288 let tokens = lexer::lex(src).expect("lex");
289 parser::parse(tokens).expect("parse")
290 }
291
292 fn round_trip(src: &str) {
293 let ast1 = parse(src);
294 let printed = pp_file(&ast1);
295 let ast2 = parse(&printed);
296 let printed2 = pp_file(&ast2);
297 assert_eq!(
298 printed, printed2,
299 "second-pass print should be idempotent.\n\
300 original source:\n{src}\n\n\
301 first print:\n{printed}\n\n\
302 second print:\n{printed2}"
303 );
304 }
305
306 #[test]
307 fn round_trip_simple_const() {
308 round_trip("const x := 42\n");
309 }
310
311 #[test]
312 fn round_trip_string_const() {
313 round_trip("const dataset := \"sift1m\"\n");
314 }
315
316 #[test]
317 fn round_trip_init_binding() {
318 round_trip("const prebuffer := dataset_prebuffer(\"example\")\n");
319 }
320
321 #[test]
322 fn round_trip_function_call() {
323 round_trip("ratio := mod(cycle, 100)\n");
324 }
325
326 #[test]
327 fn round_trip_named_args() {
328 round_trip("v := dist_normal(mean: 72.0, stddev: 5.0)\n");
329 }
330
331 #[test]
332 fn round_trip_binop() {
333 round_trip("y := (x + 1)\n");
334 }
335
336 #[test]
337 fn round_trip_inputs() {
338 round_trip("input (cycle: u64, thread: u64)\n");
339 }
340
341 #[test]
342 fn round_trip_extern() {
343 round_trip("extern dataset: String\n");
344 }
345
346 #[test]
347 fn round_trip_tuple_destructure() {
348 round_trip("(a, b) := unpack(cycle)\n");
349 }
350
351 #[test]
352 fn round_trip_workload_typical() {
353 let src = "\
355const dataset := \"sift1m\"
356const prefix := \"vec_default\"
357profiles := matching_profiles(dataset, prefix)
358table := first(profiles)
359";
360 round_trip(src);
361 }
362
363 #[test]
364 fn round_trip_string_escapes() {
365 round_trip("const s := \"hello \\\"world\\\"\"\n");
366 }
367
368 #[test]
369 fn round_trip_array_literal() {
370 round_trip("const weights := [60.0, 20.0, 15.0, 5.0]\n");
371 }
372
373 #[test]
374 fn round_trip_cursor() {
375 round_trip("cursor users = range(0, 1000000)\n");
376 }
377
378 #[test]
379 fn round_trip_cursor_with_over() {
380 round_trip("cursor q = range(0, 100) over p\n");
384 }
385
386 #[test]
387 fn pp_cursor_emits_over_clause() {
388 let ast = parse("cursor q = range(0, 100) over p\n");
389 let printed = pp_file(&ast);
390 assert!(
391 printed.contains(" over p"),
392 "projected cursor must retain its `over` clause, got:\n{printed}"
393 );
394 }
395}