Skip to main content

intent_render/
format.rs

1//! Format a parsed intent specification back to canonical `.intent` source.
2//!
3//! Parses then pretty-prints with consistent indentation and spacing.
4
5use intent_parser::ast;
6
7use crate::format_type;
8
9/// Format an AST [`File`] back to canonical `.intent` source.
10pub fn format(file: &ast::File) -> String {
11    let mut out = String::new();
12    out.push_str(&format!("module {}\n", file.module.name));
13
14    if let Some(doc) = &file.doc {
15        out.push('\n');
16        for line in &doc.lines {
17            out.push_str(&format!("--- {}\n", line.trim()));
18        }
19    }
20
21    for item in &file.items {
22        out.push('\n');
23        match item {
24            ast::TopLevelItem::Entity(e) => fmt_entity(&mut out, e),
25            ast::TopLevelItem::Action(a) => fmt_action(&mut out, a),
26            ast::TopLevelItem::Invariant(i) => fmt_invariant(&mut out, i),
27            ast::TopLevelItem::EdgeCases(ec) => fmt_edge_cases(&mut out, ec),
28        }
29    }
30
31    out
32}
33
34fn fmt_doc(out: &mut String, doc: &Option<ast::DocBlock>) {
35    if let Some(doc) = doc {
36        for line in &doc.lines {
37            out.push_str(&format!("  --- {}\n", line.trim()));
38        }
39        out.push('\n');
40    }
41}
42
43fn fmt_entity(out: &mut String, entity: &ast::EntityDecl) {
44    out.push_str(&format!("entity {} {{\n", entity.name));
45    fmt_doc(out, &entity.doc);
46    for field in &entity.fields {
47        out.push_str(&format!("  {}: {}\n", field.name, format_type(&field.ty)));
48    }
49    out.push_str("}\n");
50}
51
52fn fmt_action(out: &mut String, action: &ast::ActionDecl) {
53    out.push_str(&format!("action {} {{\n", action.name));
54    fmt_doc(out, &action.doc);
55
56    for param in &action.params {
57        out.push_str(&format!("  {}: {}\n", param.name, format_type(&param.ty)));
58    }
59
60    if let Some(req) = &action.requires {
61        out.push_str("\n  requires {\n");
62        for cond in &req.conditions {
63            out.push_str(&format!("    {}\n", fmt_expr(cond)));
64        }
65        out.push_str("  }\n");
66    }
67
68    if let Some(ens) = &action.ensures {
69        out.push_str("\n  ensures {\n");
70        for item in &ens.items {
71            match item {
72                ast::EnsuresItem::Expr(expr) => {
73                    out.push_str(&format!("    {}\n", fmt_expr(expr)));
74                }
75                ast::EnsuresItem::When(w) => {
76                    out.push_str(&format!(
77                        "    when {} => {}\n",
78                        fmt_expr(&w.condition),
79                        fmt_expr(&w.consequence)
80                    ));
81                }
82            }
83        }
84        out.push_str("  }\n");
85    }
86
87    if let Some(props) = &action.properties {
88        out.push_str("\n  properties {\n");
89        for entry in &props.entries {
90            out.push_str(&format!(
91                "    {}: {}\n",
92                entry.key,
93                fmt_prop_value(&entry.value)
94            ));
95        }
96        out.push_str("  }\n");
97    }
98
99    out.push_str("}\n");
100}
101
102fn fmt_invariant(out: &mut String, inv: &ast::InvariantDecl) {
103    out.push_str(&format!("invariant {} {{\n", inv.name));
104    fmt_doc(out, &inv.doc);
105    out.push_str(&format!("  {}\n", fmt_expr(&inv.body)));
106    out.push_str("}\n");
107}
108
109fn fmt_edge_cases(out: &mut String, ec: &ast::EdgeCasesDecl) {
110    out.push_str("edge_cases {\n");
111    for rule in &ec.rules {
112        out.push_str(&format!(
113            "  when {} => {}({})\n",
114            fmt_expr(&rule.condition),
115            rule.action.name,
116            fmt_call_args(&rule.action.args),
117        ));
118    }
119    out.push_str("}\n");
120}
121
122fn fmt_expr(expr: &ast::Expr) -> String {
123    match &expr.kind {
124        ast::ExprKind::Implies(l, r) => format!("{} => {}", fmt_expr(l), fmt_expr(r)),
125        ast::ExprKind::Or(l, r) => format!("{} || {}", fmt_expr(l), fmt_expr(r)),
126        ast::ExprKind::And(l, r) => format!("{} && {}", fmt_expr(l), fmt_expr(r)),
127        ast::ExprKind::Not(e) => format!("!{}", fmt_expr(e)),
128        ast::ExprKind::Compare { left, op, right } => {
129            let op_str = match op {
130                ast::CmpOp::Eq => "==",
131                ast::CmpOp::Ne => "!=",
132                ast::CmpOp::Lt => "<",
133                ast::CmpOp::Gt => ">",
134                ast::CmpOp::Le => "<=",
135                ast::CmpOp::Ge => ">=",
136            };
137            format!("{} {} {}", fmt_expr(left), op_str, fmt_expr(right))
138        }
139        ast::ExprKind::Arithmetic { left, op, right } => {
140            let op_str = match op {
141                ast::ArithOp::Add => "+",
142                ast::ArithOp::Sub => "-",
143            };
144            format!("{} {} {}", fmt_expr(left), op_str, fmt_expr(right))
145        }
146        ast::ExprKind::Old(e) => format!("old({})", fmt_expr(e)),
147        ast::ExprKind::Quantifier {
148            kind,
149            binding,
150            ty,
151            body,
152        } => {
153            let kw = match kind {
154                ast::QuantifierKind::Forall => "forall",
155                ast::QuantifierKind::Exists => "exists",
156            };
157            format!("{} {}: {} => {}", kw, binding, ty, fmt_expr(body))
158        }
159        ast::ExprKind::Call { name, args } => {
160            format!("{}({})", name, fmt_call_args(args))
161        }
162        ast::ExprKind::FieldAccess { root, fields } => {
163            format!("{}.{}", fmt_expr(root), fields.join("."))
164        }
165        ast::ExprKind::List(items) => {
166            let inner: Vec<_> = items.iter().map(fmt_expr).collect();
167            format!("[{}]", inner.join(", "))
168        }
169        ast::ExprKind::Ident(name) => name.clone(),
170        ast::ExprKind::Literal(lit) => fmt_literal(lit),
171    }
172}
173
174fn fmt_literal(lit: &ast::Literal) -> String {
175    match lit {
176        ast::Literal::Null => "null".to_string(),
177        ast::Literal::Bool(b) => b.to_string(),
178        ast::Literal::Int(n) => n.to_string(),
179        ast::Literal::Decimal(s) => s.clone(),
180        ast::Literal::String(s) => format!("\"{}\"", s),
181    }
182}
183
184fn fmt_call_args(args: &[ast::CallArg]) -> String {
185    args.iter()
186        .map(|a| match a {
187            ast::CallArg::Named { key, value, .. } => {
188                format!("{}: {}", key, fmt_expr(value))
189            }
190            ast::CallArg::Positional(e) => fmt_expr(e),
191        })
192        .collect::<Vec<_>>()
193        .join(", ")
194}
195
196fn fmt_prop_value(val: &ast::PropValue) -> String {
197    match val {
198        ast::PropValue::Literal(lit) => fmt_literal(lit),
199        ast::PropValue::Ident(s) => s.clone(),
200        ast::PropValue::List(items) => {
201            let inner: Vec<_> = items.iter().map(fmt_prop_value).collect();
202            format!("[{}]", inner.join(", "))
203        }
204        ast::PropValue::Object(fields) => {
205            let inner: Vec<_> = fields
206                .iter()
207                .map(|(k, v)| format!("{}: {}", k, fmt_prop_value(v)))
208                .collect();
209            format!("{{ {} }}", inner.join(", "))
210        }
211    }
212}