Skip to main content

aver/ir/hir/
dump.rs

1//! Textual dump for the resolved HIR. Mirrors `ir::dump` (which
2//! renders pre-resolve `Vec<TopLevel>`) but emits opaque `FnId` /
3//! `CtorId` / `TypeId` markers in expression positions so the
4//! Phase E migration is visually verifiable from the CLI.
5//!
6//! The format is Aver-like surface syntax with explicit identity
7//! markers wherever the resolver classified a name:
8//!
9//!   `<fn:N>(args)`         — `Call(ResolvedCallee::Fn(FnId(N)), …)`
10//!   `<slot:N>(args)`       — `Call(ResolvedCallee::LocalSlot { slot: N, … }, …)`
11//!   `<builtin:NS.method>`  — `Call(ResolvedCallee::Builtin("NS.method"), …)`
12//!   `<intrinsic:__name>`   — `Call(ResolvedCallee::Intrinsic(_), …)`
13//!   `<unresolved>(args)`   — `Call(ResolvedCallee::Unresolved { … }, …)`
14//!   `<ctor:M@type:K>name`  — `Ctor(ResolvedCtor::User { ctor_id: M, type_id: K, name })`
15//!   `Result.Ok` etc.       — `Ctor(ResolvedCtor::Builtin(BuiltinCtor::ResultOk))`
16//!   `<type:K>name(...)`    — `RecordCreate { type_id: Some(K), type_name: name }`
17//!   `<tail:fn:N>(args)`    — `TailCall { target: FnId(N), … }`
18//!
19//! Top-level scaffolding (module headers, fn sigs, stmt list) keeps
20//! the same shape as the pre-resolve dump so a side-by-side `diff`
21//! between `--emit-ir-after=resolve` and
22//! `--emit-ir-after=name_resolve` highlights only the parts the
23//! resolver touched.
24
25use 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
33/// Render every top-level item in `items`, separated by blank
34/// lines. Passes the original `Passthrough` items through the
35/// pre-resolve dump unchanged so a mixed dump still reads
36/// uniformly.
37pub 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            // Reuse the pre-resolve dump so passthrough items
61            // (verify / decision / type defs) still render in a
62            // recognisable form. Cost: borrows the existing
63            // single-item helper indirectly via `dump_items` on a
64            // one-element slice.
65            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
132/// Render an expression to a single line.
133pub 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        // helper has FnId(0), main has FnId(1) by source order.
358        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        // Verify / decision / type def items render via the
450        // pre-resolve dump (no Phase-E markers).
451        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}