1use std::fmt::Write;
26
27use super::{
28 BuiltinCtor, ResolvedCallee, ResolvedCtor, ResolvedExpr, ResolvedFnBody, ResolvedFnDef,
29 ResolvedMatchArm, ResolvedPattern, ResolvedStmt, ResolvedStrPart, ResolvedTopLevel,
30};
31use crate::ast::{BinOp, Literal, Spanned};
32
33pub fn dump_resolved_program(items: &[ResolvedTopLevel]) -> String {
38 let mut out = String::new();
39 let mut first = true;
40 for item in items {
41 if !first {
42 out.push('\n');
43 }
44 first = false;
45 dump_resolved_top_level(item, &mut out);
46 }
47 out
48}
49
50fn dump_resolved_top_level(item: &ResolvedTopLevel, out: &mut String) {
51 match item {
52 ResolvedTopLevel::Module(m) => {
53 writeln!(out, "module {}", m.name).ok();
54 if !m.depends.is_empty() {
55 writeln!(out, " depends [{}]", m.depends.join(", ")).ok();
56 }
57 }
58 ResolvedTopLevel::FnDef(fd) => dump_resolved_fn_def(fd, out),
59 ResolvedTopLevel::Passthrough(item) => {
60 let rendered = crate::ir::dump::dump_items(std::slice::from_ref(item), None);
66 out.push_str(rendered.trim_end());
67 out.push('\n');
68 }
69 }
70}
71
72fn dump_resolved_fn_def(fd: &ResolvedFnDef, out: &mut String) {
73 let params: Vec<String> = fd
74 .params
75 .iter()
76 .map(|(name, ty)| format!("{name}: {}", ty.display()))
77 .collect();
78 let effects = if fd.effects.is_empty() {
79 String::new()
80 } else {
81 let names: Vec<&str> = fd.effects.iter().map(|e| e.node.as_str()).collect();
82 format!(" ! [{}]", names.join(", "))
83 };
84 writeln!(
85 out,
86 "fn <fn:{}>{}({}) -> {}{}",
87 fd.fn_id.0,
88 fd.name,
89 params.join(", "),
90 fd.return_type.display(),
91 effects,
92 )
93 .ok();
94 dump_resolved_fn_body(&fd.body, out);
95}
96
97fn dump_resolved_fn_body(body: &ResolvedFnBody, out: &mut String) {
98 match body {
99 ResolvedFnBody::Block(stmts) => {
100 for stmt in stmts {
101 dump_resolved_stmt(stmt, 1, out);
102 }
103 }
104 }
105}
106
107fn dump_resolved_stmt(stmt: &ResolvedStmt, indent: usize, out: &mut String) {
108 let pad = " ".repeat(indent);
109 match stmt {
110 ResolvedStmt::Binding {
111 name,
112 ty_ann,
113 value,
114 } => {
115 let ann = match ty_ann {
116 Some(t) => format!(": {}", t.display()),
117 None => String::new(),
118 };
119 writeln!(
120 out,
121 "{pad}{name}{ann} = {}",
122 dump_resolved_expr(&value.node)
123 )
124 .ok();
125 }
126 ResolvedStmt::Expr(expr) => {
127 writeln!(out, "{pad}{}", dump_resolved_expr(&expr.node)).ok();
128 }
129 }
130}
131
132pub fn dump_resolved_expr(expr: &ResolvedExpr) -> String {
134 match expr {
135 ResolvedExpr::Literal(l) => dump_literal(l),
136 ResolvedExpr::Ident(name) => name.clone(),
137 ResolvedExpr::Resolved { slot, name, .. } => format!("<slot:{slot}:{name}>"),
138 ResolvedExpr::Attr(obj, field) => {
139 format!("{}.{field}", dump_resolved_expr(&obj.node))
140 }
141 ResolvedExpr::Call(callee, args) => {
142 let args_str = render_args(args);
143 format!("{}({args_str})", dump_resolved_callee(callee))
144 }
145 ResolvedExpr::BinOp(op, l, r) => format!(
146 "({} {} {})",
147 dump_resolved_expr(&l.node),
148 dump_binop(*op),
149 dump_resolved_expr(&r.node)
150 ),
151 ResolvedExpr::Neg(inner) => format!("-{}", dump_resolved_expr(&inner.node)),
152 ResolvedExpr::Match { subject, arms } => {
153 let mut s = format!("match {}", dump_resolved_expr(&subject.node));
154 for arm in arms {
155 s.push_str(&format!(" | {}", dump_resolved_arm(arm)));
156 }
157 s
158 }
159 ResolvedExpr::Ctor(ctor, args) => {
160 let args_str = render_args(args);
161 if args_str.is_empty() {
162 dump_resolved_ctor(ctor)
163 } else {
164 format!("{}({args_str})", dump_resolved_ctor(ctor))
165 }
166 }
167 ResolvedExpr::ErrorProp(inner) => format!("{}?", dump_resolved_expr(&inner.node)),
168 ResolvedExpr::InterpolatedStr(parts) => {
169 let pieces: Vec<String> = parts
170 .iter()
171 .map(|p| match p {
172 ResolvedStrPart::Literal(s) => s.clone(),
173 ResolvedStrPart::Parsed(e) => {
174 format!("${{{}}}", dump_resolved_expr(&e.node))
175 }
176 })
177 .collect();
178 format!("\"{}\"", pieces.join(""))
179 }
180 ResolvedExpr::List(items) => format!("[{}]", render_args(items)),
181 ResolvedExpr::Tuple(items) => format!("({})", render_args(items)),
182 ResolvedExpr::MapLiteral(pairs) => {
183 let pieces: Vec<String> = pairs
184 .iter()
185 .map(|(k, v)| {
186 format!(
187 "{} => {}",
188 dump_resolved_expr(&k.node),
189 dump_resolved_expr(&v.node)
190 )
191 })
192 .collect();
193 format!("{{{}}}", pieces.join(", "))
194 }
195 ResolvedExpr::RecordCreate {
196 type_id,
197 type_name,
198 fields,
199 } => {
200 let marker = match type_id {
201 Some(id) => format!("<type:{}>", id.0),
202 None => "<type:?>".to_string(),
203 };
204 let pieces: Vec<String> = fields
205 .iter()
206 .map(|(n, e)| format!("{n} = {}", dump_resolved_expr(&e.node)))
207 .collect();
208 format!("{marker}{type_name}({})", pieces.join(", "))
209 }
210 ResolvedExpr::RecordUpdate {
211 type_id,
212 type_name,
213 base,
214 updates,
215 } => {
216 let marker = match type_id {
217 Some(id) => format!("<type:{}>", id.0),
218 None => "<type:?>".to_string(),
219 };
220 let pieces: Vec<String> = updates
221 .iter()
222 .map(|(n, e)| format!("{n} = {}", dump_resolved_expr(&e.node)))
223 .collect();
224 format!(
225 "{marker}{type_name}.update({}, {})",
226 dump_resolved_expr(&base.node),
227 pieces.join(", ")
228 )
229 }
230 ResolvedExpr::TailCall { target, args } => {
231 format!("<tail:fn:{}>({})", target.0, render_args(args))
232 }
233 ResolvedExpr::IndependentProduct(items, unwrap) => {
234 let suffix = if *unwrap { "?!" } else { "!" };
235 format!("({}){suffix}", render_args(items))
236 }
237 }
238}
239
240fn render_args(args: &[Spanned<ResolvedExpr>]) -> String {
241 args.iter()
242 .map(|a| dump_resolved_expr(&a.node))
243 .collect::<Vec<_>>()
244 .join(", ")
245}
246
247fn dump_resolved_callee(callee: &ResolvedCallee) -> String {
248 match callee {
249 ResolvedCallee::Fn(id) => format!("<fn:{}>", id.0),
250 ResolvedCallee::Builtin(name) => format!("<builtin:{name}>"),
251 ResolvedCallee::Intrinsic(kind) => format!("<intrinsic:{}>", kind.name()),
252 ResolvedCallee::LocalSlot { slot, name, .. } => format!("<slot:{slot}:{name}>"),
253 ResolvedCallee::Unresolved { callee } => {
254 format!("<unresolved:{}>", dump_resolved_expr(&callee.node))
255 }
256 }
257}
258
259fn dump_resolved_ctor(ctor: &ResolvedCtor) -> String {
260 match ctor {
261 ResolvedCtor::User {
262 ctor_id,
263 type_id,
264 name,
265 } => format!("<ctor:{}@type:{}>{name}", ctor_id.0, type_id.0),
266 ResolvedCtor::Builtin(BuiltinCtor::ResultOk) => "Result.Ok".to_string(),
267 ResolvedCtor::Builtin(BuiltinCtor::ResultErr) => "Result.Err".to_string(),
268 ResolvedCtor::Builtin(BuiltinCtor::OptionSome) => "Option.Some".to_string(),
269 ResolvedCtor::Builtin(BuiltinCtor::OptionNone) => "Option.None".to_string(),
270 ResolvedCtor::Unresolved { name } => format!("<unresolved-ctor:{name}>"),
271 }
272}
273
274fn dump_resolved_arm(arm: &ResolvedMatchArm) -> String {
275 format!(
276 "{} -> {}",
277 dump_resolved_pattern(&arm.pattern),
278 dump_resolved_expr(&arm.body.node)
279 )
280}
281
282fn dump_resolved_pattern(pat: &ResolvedPattern) -> String {
283 match pat {
284 ResolvedPattern::Wildcard => "_".to_string(),
285 ResolvedPattern::Literal(l) => dump_literal(l),
286 ResolvedPattern::Ident(name) => name.clone(),
287 ResolvedPattern::EmptyList => "[]".to_string(),
288 ResolvedPattern::Cons(head, tail) => format!("[{head}, ..{tail}]"),
289 ResolvedPattern::Tuple(items) => {
290 let pieces: Vec<String> = items.iter().map(dump_resolved_pattern).collect();
291 format!("({})", pieces.join(", "))
292 }
293 ResolvedPattern::Ctor(ctor, bindings) => {
294 let head = dump_resolved_ctor(ctor);
295 if bindings.is_empty() {
296 head
297 } else {
298 format!("{head}({})", bindings.join(", "))
299 }
300 }
301 }
302}
303
304fn dump_literal(l: &Literal) -> String {
305 match l {
306 Literal::Int(i) => i.to_string(),
307 Literal::BigInt(s) => s.clone(),
308 Literal::Float(f) => format!("{f}"),
309 Literal::Str(s) => format!("\"{s}\""),
310 Literal::Bool(b) => b.to_string(),
311 Literal::Unit => "()".to_string(),
312 }
313}
314
315fn dump_binop(op: BinOp) -> &'static str {
316 match op {
317 BinOp::Add => "+",
318 BinOp::Sub => "-",
319 BinOp::Mul => "*",
320 BinOp::Div => "/",
321 BinOp::Eq => "==",
322 BinOp::Neq => "!=",
323 BinOp::Lt => "<",
324 BinOp::Lte => "<=",
325 BinOp::Gt => ">",
326 BinOp::Gte => ">=",
327 }
328}
329
330#[cfg(test)]
331mod tests {
332 use super::*;
333 use crate::ir::SymbolTable;
334 use crate::ir::hir::resolve_program;
335 use crate::source::parse_source;
336 use crate::tco;
337
338 fn dump(src: &str) -> String {
339 let mut items = parse_source(src).expect("parse");
340 tco::transform_program(&mut items);
341 let symbols = SymbolTable::build(&items, &[]);
342 let resolved = resolve_program(&symbols, &items);
343 dump_resolved_program(&resolved)
344 }
345
346 #[test]
347 fn dump_includes_fn_id_marker_on_call() {
348 let out = dump(
349 r#"
350fn helper(n: Int) -> Int
351 n + 1
352
353fn main() -> Int
354 helper(7)
355"#,
356 );
357 assert!(
359 out.contains("fn <fn:0>helper"),
360 "expected fn-id marker on helper signature, got:\n{out}"
361 );
362 assert!(
363 out.contains("<fn:0>(7)"),
364 "expected helper call marker in main body, got:\n{out}"
365 );
366 }
367
368 #[test]
369 fn dump_includes_builtin_ctor_marker() {
370 let out = dump(
371 r#"
372fn make() -> Result<Int, String>
373 Result.Ok(42)
374"#,
375 );
376 assert!(
377 out.contains("Result.Ok(42)"),
378 "expected Result.Ok builtin ctor render, got:\n{out}"
379 );
380 }
381
382 #[test]
383 fn dump_includes_user_ctor_marker() {
384 let out = dump(
385 r#"
386type Shape
387 Circle(Float)
388
389fn make() -> Shape
390 Shape.Circle(1.0)
391"#,
392 );
393 assert!(
394 out.contains("<ctor:") && out.contains("@type:") && out.contains(">Circle"),
395 "expected `<ctor:N@type:K>Circle(...)`, got:\n{out}"
396 );
397 }
398
399 #[test]
400 fn dump_includes_record_type_id_marker() {
401 let out = dump(
402 r#"
403record Point
404 x: Int
405 y: Int
406
407fn origin() -> Point
408 Point(x = 0, y = 0)
409"#,
410 );
411 assert!(
412 out.contains("<type:") && out.contains(">Point(x = 0, y = 0)"),
413 "expected `<type:K>Point(x = 0, y = 0)`, got:\n{out}"
414 );
415 }
416
417 #[test]
418 fn dump_includes_tail_call_marker() {
419 let out = dump(
420 r#"
421fn count(n: Int, acc: Int) -> Int
422 match n
423 0 -> acc
424 _ -> count(n - 1, acc + 1)
425"#,
426 );
427 assert!(
428 out.contains("<tail:fn:"),
429 "expected tail-call marker on recursive arm, got:\n{out}"
430 );
431 }
432
433 #[test]
434 fn dump_renders_builtin_namespace_method_call() {
435 let out = dump(
436 r#"
437fn abs_neg() -> Int
438 Int.abs(-3)
439"#,
440 );
441 assert!(
442 out.contains("<builtin:Int.abs>(-3)"),
443 "expected builtin namespace method marker, got:\n{out}"
444 );
445 }
446
447 #[test]
448 fn dump_passes_through_non_fn_items() {
449 let out = dump(
452 r#"
453type Tag
454 On
455 Off
456
457fn use_it() -> Int
458 1
459"#,
460 );
461 assert!(
462 out.contains("type Tag"),
463 "TypeDef should pass through: {out}"
464 );
465 assert!(out.contains("fn <fn:"), "FnDef should be promoted: {out}");
466 }
467}