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