use crate::dsl::ast::{
Arg, BindingModifier, CallExpr, Binding, Expr, GkFile,
ModuleDef, Statement, BinOpKind, ExternPort, CursorDecl, WireModifier,
};
pub fn pp_file(file: &GkFile) -> String {
let mut out = String::new();
for stmt in &file.statements {
out.push_str(&pp_statement(stmt));
out.push('\n');
}
out
}
pub fn pp_statement(stmt: &Statement) -> String {
match stmt {
Statement::InputDecl(d) => match &d.ty {
Some(ty) => format!("input {}: {}", d.name, ty),
None => format!("input {}", d.name),
},
Statement::Binding(b) => pp_binding(b),
Statement::ModuleDef(m) => pp_module_def(m),
Statement::ExternPort(p) => pp_extern_port(p),
Statement::Cursor(c) => pp_cursor(c),
Statement::Pragma { name, .. } => format!("pragma {name}"),
}
}
pub fn pp_expr(expr: &Expr) -> String {
match expr {
Expr::Ident(name, _) => name.clone(),
Expr::IntLit(v, _) => v.to_string(),
Expr::FloatLit(v, _) => format_float(*v),
Expr::StringLit(s, _) => format!("\"{}\"", escape_string(s)),
Expr::ArrayLit(elts, _) => {
let parts: Vec<String> = elts.iter().map(pp_expr).collect();
format!("[{}]", parts.join(", "))
}
Expr::Call(c) => pp_call(c),
Expr::BinOp(lhs, op, rhs) => {
format!("({} {} {})", pp_expr(lhs), pp_binop(*op), pp_expr(rhs))
}
Expr::UnaryNeg(e, _) => format!("(-{})", pp_expr(e)),
Expr::UnaryBitNot(e, _) => format!("(!{})", pp_expr(e)),
Expr::FieldAccess { source, field, .. } => format!("{source}.{field}"),
}
}
fn pp_binding(b: &Binding) -> String {
let target = if b.targets.len() == 1 {
b.targets[0].clone()
} else {
format!("({})", b.targets.join(", "))
};
let prefix = pp_modifier_prefix(b.modifier);
if prefix.is_empty() {
format!("{} := {}", target, pp_expr(&b.value))
} else {
format!("{} {} := {}", prefix, target, pp_expr(&b.value))
}
}
fn pp_extern_port(p: &ExternPort) -> String {
if let Some(default) = &p.default {
format!("extern {}: {} = {}", p.name, p.typ, pp_expr(default))
} else {
format!("extern {}: {}", p.name, p.typ)
}
}
fn pp_cursor(c: &CursorDecl) -> String {
format!("cursor {} = {}", c.name, pp_expr(&c.constructor))
}
fn pp_module_def(m: &ModuleDef) -> String {
let params: Vec<String> = m.params.iter()
.map(|p| format!("{}: {}", p.name, p.typ))
.collect();
let outputs: Vec<String> = m.outputs.iter()
.map(|p| format!("{}: {}", p.name, p.typ))
.collect();
let mut body = String::new();
for s in &m.body {
body.push_str(" ");
body.push_str(&pp_statement(s));
body.push('\n');
}
format!("{}({}) -> ({}) := {{\n{}}}", m.name, params.join(", "), outputs.join(", "), body)
}
fn pp_call(c: &CallExpr) -> String {
let args: Vec<String> = c.args.iter().map(pp_arg).collect();
format!("{}({})", c.func, args.join(", "))
}
fn pp_arg(arg: &Arg) -> String {
match arg {
Arg::Positional(e) => pp_expr(e),
Arg::Named(name, e) => format!("{}: {}", name, pp_expr(e)),
}
}
fn pp_modifier_prefix(m: BindingModifier) -> String {
let mut parts: Vec<&str> = Vec::new();
if m.has(WireModifier::Const) { parts.push("const"); }
if m.has(WireModifier::Shared) { parts.push("shared"); }
if m.has(WireModifier::Volatile) { parts.push("volatile"); }
parts.join(" ")
}
fn pp_binop(op: BinOpKind) -> &'static str {
match op {
BinOpKind::Add => "+",
BinOpKind::Sub => "-",
BinOpKind::Mul => "*",
BinOpKind::Div => "/",
BinOpKind::Mod => "%",
BinOpKind::Pow => "**",
BinOpKind::BitAnd => "&",
BinOpKind::BitOr => "|",
BinOpKind::BitXor => "^",
BinOpKind::Shl => "<<",
BinOpKind::Shr => ">>",
BinOpKind::Eq => "==",
BinOpKind::Ne => "!=",
BinOpKind::Lt => "<",
BinOpKind::Gt => ">",
BinOpKind::Le => "<=",
BinOpKind::Ge => ">=",
}
}
fn escape_string(s: &str) -> String {
let mut out = String::with_capacity(s.len());
for c in s.chars() {
match c {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\t' => out.push_str("\\t"),
'\r' => out.push_str("\\r"),
c => out.push(c),
}
}
out
}
fn format_float(v: f64) -> String {
if v.is_finite() && v == v.trunc() && v.abs() < 1e18 {
format!("{v:.1}")
} else {
format!("{v}")
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::dsl::{lexer, parser};
fn parse(src: &str) -> GkFile {
let tokens = lexer::lex(src).expect("lex");
parser::parse(tokens).expect("parse")
}
fn round_trip(src: &str) {
let ast1 = parse(src);
let printed = pp_file(&ast1);
let ast2 = parse(&printed);
let printed2 = pp_file(&ast2);
assert_eq!(
printed, printed2,
"second-pass print should be idempotent.\n\
original source:\n{src}\n\n\
first print:\n{printed}\n\n\
second print:\n{printed2}"
);
}
#[test]
fn round_trip_simple_const() {
round_trip("const x := 42\n");
}
#[test]
fn round_trip_string_const() {
round_trip("const dataset := \"sift1m\"\n");
}
#[test]
fn round_trip_init_binding() {
round_trip("const prebuffer := dataset_prebuffer(\"example\")\n");
}
#[test]
fn round_trip_function_call() {
round_trip("ratio := mod(cycle, 100)\n");
}
#[test]
fn round_trip_named_args() {
round_trip("v := dist_normal(mean: 72.0, stddev: 5.0)\n");
}
#[test]
fn round_trip_binop() {
round_trip("y := (x + 1)\n");
}
#[test]
fn round_trip_inputs() {
round_trip("input (cycle: u64, thread: u64)\n");
}
#[test]
fn round_trip_extern() {
round_trip("extern dataset: String\n");
}
#[test]
fn round_trip_tuple_destructure() {
round_trip("(a, b) := unpack(cycle)\n");
}
#[test]
fn round_trip_workload_typical() {
let src = "\
const dataset := \"sift1m\"
const prefix := \"vec_default\"
profiles := matching_profiles(dataset, prefix)
table := first(profiles)
";
round_trip(src);
}
#[test]
fn round_trip_string_escapes() {
round_trip("const s := \"hello \\\"world\\\"\"\n");
}
#[test]
fn round_trip_array_literal() {
round_trip("const weights := [60.0, 20.0, 15.0, 5.0]\n");
}
}