1use intent_parser::ast;
4
5use crate::format_type;
6
7pub fn render(file: &ast::File) -> String {
9 let mut body = String::new();
10 body.push_str(&format!("<h1>{}</h1>\n", esc(&file.module.name)));
11
12 if let Some(doc) = &file.doc {
13 body.push_str("<p class=\"doc\">");
14 body.push_str(&esc(&doc.lines.join(" ")));
15 body.push_str("</p>\n");
16 }
17
18 if !file.imports.is_empty() {
19 body.push_str("<section class=\"imports\">\n<h2>Imports</h2>\n<ul>\n");
20 for use_decl in &file.imports {
21 if let Some(item) = &use_decl.item {
22 body.push_str(&format!(
23 "<li><code>{}.{}</code></li>\n",
24 esc(&use_decl.module_name),
25 esc(item)
26 ));
27 } else {
28 body.push_str(&format!(
29 "<li><code>{}</code></li>\n",
30 esc(&use_decl.module_name)
31 ));
32 }
33 }
34 body.push_str("</ul>\n</section>\n");
35 }
36
37 for item in &file.items {
38 match item {
39 ast::TopLevelItem::Entity(e) => render_entity(&mut body, e),
40 ast::TopLevelItem::Action(a) => render_action(&mut body, a),
41 ast::TopLevelItem::Invariant(i) => render_invariant(&mut body, i),
42 ast::TopLevelItem::EdgeCases(ec) => render_edge_cases(&mut body, ec),
43 ast::TopLevelItem::Test(_) => {} }
45 }
46
47 format!(
48 "<!DOCTYPE html>\n<html lang=\"en\">\n<head>\n<meta charset=\"utf-8\">\n\
49 <title>{title}</title>\n<style>\n{css}\n</style>\n</head>\n\
50 <body>\n{body}</body>\n</html>\n",
51 title = esc(&file.module.name),
52 css = CSS,
53 body = body,
54 )
55}
56
57fn render_entity(out: &mut String, entity: &ast::EntityDecl) {
58 out.push_str(&format!(
59 "<section class=\"entity\">\n<h2>Entity: {}</h2>\n",
60 esc(&entity.name)
61 ));
62 render_doc(out, &entity.doc);
63 if !entity.fields.is_empty() {
64 out.push_str("<table>\n<thead><tr><th>Field</th><th>Type</th></tr></thead>\n<tbody>\n");
65 for field in &entity.fields {
66 out.push_str(&format!(
67 "<tr><td><code>{}</code></td><td><code>{}</code></td></tr>\n",
68 esc(&field.name),
69 esc(&format_type(&field.ty))
70 ));
71 }
72 out.push_str("</tbody>\n</table>\n");
73 }
74 out.push_str("</section>\n");
75}
76
77fn render_action(out: &mut String, action: &ast::ActionDecl) {
78 out.push_str(&format!(
79 "<section class=\"action\">\n<h2>Action: {}</h2>\n",
80 esc(&action.name)
81 ));
82 render_doc(out, &action.doc);
83 if !action.params.is_empty() {
84 out.push_str("<h3>Parameters</h3>\n<ul>\n");
85 for p in &action.params {
86 out.push_str(&format!(
87 "<li><code>{}</code>: <code>{}</code></li>\n",
88 esc(&p.name),
89 esc(&format_type(&p.ty))
90 ));
91 }
92 out.push_str("</ul>\n");
93 }
94 if let Some(req) = &action.requires {
95 out.push_str("<h3>Requires</h3>\n<ul class=\"constraints\">\n");
96 for cond in &req.conditions {
97 out.push_str(&format!(
98 "<li><code>{}</code></li>\n",
99 esc(&format_expr(cond))
100 ));
101 }
102 out.push_str("</ul>\n");
103 }
104 if let Some(ens) = &action.ensures {
105 out.push_str("<h3>Ensures</h3>\n<ul class=\"constraints\">\n");
106 for item in &ens.items {
107 match item {
108 ast::EnsuresItem::Expr(e) => {
109 out.push_str(&format!("<li><code>{}</code></li>\n", esc(&format_expr(e))));
110 }
111 ast::EnsuresItem::When(w) => {
112 out.push_str(&format!(
113 "<li>when <code>{}</code> ⇒ <code>{}</code></li>\n",
114 esc(&format_expr(&w.condition)),
115 esc(&format_expr(&w.consequence))
116 ));
117 }
118 }
119 }
120 out.push_str("</ul>\n");
121 }
122 if let Some(props) = &action.properties {
123 out.push_str("<h3>Properties</h3>\n<table>\n<thead><tr><th>Key</th><th>Value</th></tr></thead>\n<tbody>\n");
124 for entry in &props.entries {
125 out.push_str(&format!(
126 "<tr><td><code>{}</code></td><td><code>{}</code></td></tr>\n",
127 esc(&entry.key),
128 esc(&format_prop_value(&entry.value))
129 ));
130 }
131 out.push_str("</tbody>\n</table>\n");
132 }
133 out.push_str("</section>\n");
134}
135
136fn render_invariant(out: &mut String, inv: &ast::InvariantDecl) {
137 out.push_str(&format!(
138 "<section class=\"invariant\">\n<h2>Invariant: {}</h2>\n",
139 esc(&inv.name)
140 ));
141 render_doc(out, &inv.doc);
142 out.push_str(&format!(
143 "<pre><code>{}</code></pre>\n",
144 esc(&format_expr(&inv.body))
145 ));
146 out.push_str("</section>\n");
147}
148
149fn render_edge_cases(out: &mut String, ec: &ast::EdgeCasesDecl) {
150 out.push_str("<section class=\"edge-cases\">\n<h2>Edge Cases</h2>\n<ul>\n");
151 for rule in &ec.rules {
152 out.push_str(&format!(
153 "<li>when <code>{}</code> ⇒ <code>{}({})</code></li>\n",
154 esc(&format_expr(&rule.condition)),
155 esc(&rule.action.name),
156 esc(&format_call_args(&rule.action.args))
157 ));
158 }
159 out.push_str("</ul>\n</section>\n");
160}
161
162fn render_doc(out: &mut String, doc: &Option<ast::DocBlock>) {
163 if let Some(d) = doc {
164 out.push_str("<p class=\"doc\">");
165 out.push_str(&esc(&d.lines.join(" ")));
166 out.push_str("</p>\n");
167 }
168}
169
170fn format_expr(expr: &ast::Expr) -> String {
173 match &expr.kind {
174 ast::ExprKind::Implies(l, r) => format!("{} => {}", format_expr(l), format_expr(r)),
175 ast::ExprKind::Or(l, r) => format!("{} || {}", format_expr(l), format_expr(r)),
176 ast::ExprKind::And(l, r) => format!("{} && {}", format_expr(l), format_expr(r)),
177 ast::ExprKind::Not(e) => format!("!{}", format_expr(e)),
178 ast::ExprKind::Compare { left, op, right } => {
179 let op_str = match op {
180 ast::CmpOp::Eq => "==",
181 ast::CmpOp::Ne => "!=",
182 ast::CmpOp::Lt => "<",
183 ast::CmpOp::Gt => ">",
184 ast::CmpOp::Le => "<=",
185 ast::CmpOp::Ge => ">=",
186 };
187 format!("{} {} {}", format_expr(left), op_str, format_expr(right))
188 }
189 ast::ExprKind::Arithmetic { left, op, right } => {
190 let op_str = match op {
191 ast::ArithOp::Add => "+",
192 ast::ArithOp::Sub => "-",
193 };
194 format!("{} {} {}", format_expr(left), op_str, format_expr(right))
195 }
196 ast::ExprKind::Old(e) => format!("old({})", format_expr(e)),
197 ast::ExprKind::Quantifier {
198 kind,
199 binding,
200 ty,
201 body,
202 } => {
203 let kw = match kind {
204 ast::QuantifierKind::Forall => "forall",
205 ast::QuantifierKind::Exists => "exists",
206 };
207 format!("{} {}: {} => {}", kw, binding, ty, format_expr(body))
208 }
209 ast::ExprKind::Call { name, args } => {
210 format!("{}({})", name, format_call_args(args))
211 }
212 ast::ExprKind::FieldAccess { root, fields } => {
213 format!("{}.{}", format_expr(root), fields.join("."))
214 }
215 ast::ExprKind::List(items) => {
216 let inner: Vec<_> = items.iter().map(format_expr).collect();
217 format!("[{}]", inner.join(", "))
218 }
219 ast::ExprKind::Ident(name) => name.clone(),
220 ast::ExprKind::Literal(lit) => format_literal(lit),
221 }
222}
223
224fn format_literal(lit: &ast::Literal) -> String {
225 match lit {
226 ast::Literal::Null => "null".to_string(),
227 ast::Literal::Bool(b) => b.to_string(),
228 ast::Literal::Int(n) => n.to_string(),
229 ast::Literal::Decimal(s) => s.clone(),
230 ast::Literal::String(s) => format!("\"{}\"", s),
231 }
232}
233
234fn format_call_args(args: &[ast::CallArg]) -> String {
235 args.iter()
236 .map(|a| match a {
237 ast::CallArg::Named { key, value, .. } => {
238 format!("{}: {}", key, format_expr(value))
239 }
240 ast::CallArg::Positional(e) => format_expr(e),
241 })
242 .collect::<Vec<_>>()
243 .join(", ")
244}
245
246fn format_prop_value(val: &ast::PropValue) -> String {
247 match val {
248 ast::PropValue::Literal(lit) => format_literal(lit),
249 ast::PropValue::Ident(name) => name.clone(),
250 ast::PropValue::List(items) => {
251 let inner: Vec<String> = items.iter().map(format_prop_value).collect();
252 format!("[{}]", inner.join(", "))
253 }
254 ast::PropValue::Object(fields) => {
255 let inner: Vec<String> = fields
256 .iter()
257 .map(|(k, v)| format!("{}: {}", k, format_prop_value(v)))
258 .collect();
259 format!("{{{}}}", inner.join(", "))
260 }
261 }
262}
263
264fn esc(s: &str) -> String {
267 s.replace('&', "&")
268 .replace('<', "<")
269 .replace('>', ">")
270 .replace('"', """)
271}
272
273const CSS: &str = "\
274body { font-family: system-ui, -apple-system, sans-serif; max-width: 48rem; margin: 2rem auto; padding: 0 1rem; color: #1a1a1a; line-height: 1.6; }
275h1 { border-bottom: 2px solid #333; padding-bottom: 0.3rem; }
276h2 { margin-top: 2rem; color: #2c5282; }
277h3 { color: #555; }
278.doc { color: #555; font-style: italic; }
279table { border-collapse: collapse; width: 100%; margin: 0.5rem 0; }
280th, td { border: 1px solid #ddd; padding: 0.4rem 0.8rem; text-align: left; }
281th { background: #f7f7f7; }
282code { background: #f5f5f5; padding: 0.1rem 0.3rem; border-radius: 3px; font-size: 0.9em; }
283pre { background: #f5f5f5; padding: 1rem; border-radius: 4px; overflow-x: auto; }
284pre code { background: none; padding: 0; }
285ul.constraints { list-style: none; padding-left: 1rem; }
286ul.constraints li::before { content: '\\2713 '; color: #38a169; }
287section { margin-bottom: 1.5rem; }
288.entity { border-left: 3px solid #3182ce; padding-left: 1rem; }
289.action { border-left: 3px solid #d69e2e; padding-left: 1rem; }
290.invariant { border-left: 3px solid #38a169; padding-left: 1rem; }
291.edge-cases { border-left: 3px solid #e53e3e; padding-left: 1rem; }";