Files
sx/src/print.zig
agra 83ec2536af lang: catch/onfail error bindings take parens
try foo() catch (e) { }   // legal
try foo() catch e { }     // parse error with a migration hint

Same capture style as the for-loop. All four catch shapes keep working
with the parenthesized binding — block, bare-expression body, and the
== match sugar — and the no-binding forms are unchanged. onfail follows
the same rule (onfail (e) { }); its expression-cleanup form is
disambiguated by the paren-group-before-brace lookahead, so
onfail (f()); stays an expression cleanup.

AST unchanged; the printer renders the parens; the #run escape help
text updated. Corpus migrated (57 catch + 3 onfail bindings, in-source
parser test strings, specs incl. grammar rules, readme untouched —
no catch examples there).

Regression: examples/1157-diagnostics-catch-binding-needs-parens.sx;
re-captured stderr for 1010/1013/1037/1123 (migrated source echoed in
carets + help text).
2026-06-10 23:05:02 +03:00

204 lines
7.0 KiB
Zig

//! Focused AST round-trip printer.
//!
//! Reconstructs sx source text from AST nodes. The scope is intentionally
//! narrow: it covers the declaration, type-expression, and (since ERR E0.2)
//! the error-handling expression/statement node kinds the ERR stream's parser
//! tests round-trip, and bails loudly (`error.UnsupportedNode`) on anything it
//! does not yet handle — so an unsupported node can never be silently
//! mis-printed (CLAUDE.md REJECTED PATTERNS: no silent arms). Later steps
//! extend it as new surface syntax lands.
const std = @import("std");
const ast = @import("ast.zig");
const Node = ast.Node;
const Writer = *std.Io.Writer;
/// Print a node back to source text. Routes declarations here, expressions /
/// statements to `printExpr`, and type expressions to `printType`.
pub fn printNode(node: *const Node, writer: Writer) anyerror!void {
switch (node.data) {
.error_set_decl => |d| {
try writer.writeAll(d.name);
try writer.writeAll(" :: error {");
for (d.tag_names, 0..) |tag, i| {
try writer.writeAll(if (i == 0) " " else ", ");
try writer.writeAll(tag);
}
if (d.tag_names.len > 0) try writer.writeByte(' ');
try writer.writeByte('}');
},
else => try printExpr(node, writer),
}
}
/// Print an expression or statement node back to source text.
pub fn printExpr(node: *const Node, writer: Writer) anyerror!void {
switch (node.data) {
.identifier => |id| try writer.writeAll(id.name),
.enum_literal => |el| {
try writer.writeByte('.');
try writer.writeAll(el.name);
},
.int_literal => |l| try writer.print("{d}", .{l.value}),
.bool_literal => |l| try writer.writeAll(if (l.value) "true" else "false"),
.string_literal => |l| {
try writer.writeByte('"');
try writer.writeAll(l.raw);
try writer.writeByte('"');
},
.call => |c| {
try printExpr(c.callee, writer);
try writer.writeByte('(');
for (c.args, 0..) |arg, i| {
if (i > 0) try writer.writeAll(", ");
try printExpr(arg, writer);
}
try writer.writeByte(')');
},
.field_access => |fa| {
try printExpr(fa.object, writer);
try writer.writeByte('.');
try writer.writeAll(fa.field);
},
.binary_op => |b| {
const op_str = binaryOpStr(b.op) orelse return error.UnsupportedNode;
try printExpr(b.lhs, writer);
try writer.writeByte(' ');
try writer.writeAll(op_str);
try writer.writeByte(' ');
try printExpr(b.rhs, writer);
},
.try_expr => |t| {
try writer.writeAll("try ");
try printExpr(t.operand, writer);
},
.catch_expr => |c| {
try printExpr(c.operand, writer);
try writer.writeAll(" catch");
if (c.binding) |bnd| {
try writer.writeAll(" (");
try writer.writeAll(bnd);
try writer.writeByte(')');
}
if (c.is_match_body) {
try writer.writeAll(" == ");
try printMatchArms(c.body, writer);
} else {
try writer.writeByte(' ');
try printExpr(c.body, writer);
}
},
.raise_stmt => |r| {
try writer.writeAll("raise ");
try printExpr(r.tag, writer);
},
.onfail_stmt => |o| {
try writer.writeAll("onfail");
if (o.binding) |bnd| {
try writer.writeAll(" (");
try writer.writeAll(bnd);
try writer.writeByte(')');
}
try writer.writeByte(' ');
try printExpr(o.body, writer);
},
.return_stmt => |r| {
try writer.writeAll("return");
if (r.value) |v| {
try writer.writeByte(' ');
try printExpr(v, writer);
}
},
.block => |blk| {
try writer.writeByte('{');
for (blk.stmts) |stmt| {
try writer.writeByte(' ');
try printExpr(stmt, writer);
try writer.writeByte(';');
}
try writer.writeAll(" }");
},
else => try printType(node, writer),
}
}
/// Print a type-expression node back to source text.
pub fn printType(node: *const Node, writer: Writer) anyerror!void {
switch (node.data) {
.type_expr => |t| try writer.writeAll(t.name),
.error_type_expr => |e| {
try writer.writeByte('!');
if (e.name) |n| try writer.writeAll(n);
},
.pointer_type_expr => |p| {
try writer.writeByte('*');
try printType(p.pointee_type, writer);
},
.optional_type_expr => |o| {
try writer.writeByte('?');
try printType(o.inner_type, writer);
},
.slice_type_expr => |s| {
try writer.writeAll("[]");
try printType(s.element_type, writer);
},
.many_pointer_type_expr => |m| {
try writer.writeAll("[*]");
try printType(m.element_type, writer);
},
.tuple_type_expr => |t| {
try writer.writeByte('(');
for (t.field_types, 0..) |ft, i| {
if (i > 0) try writer.writeAll(", ");
try printType(ft, writer);
}
try writer.writeByte(')');
},
else => return error.UnsupportedNode,
}
}
/// Print the `{ case ... }` arms of a `match_expr` (used for the catch
/// match-body form `catch e == { ... }`). The subject is implicit (the catch
/// binding), so only the arms are emitted. Each arm body must be a one-statement
/// block (the common case); anything else bails loudly.
fn printMatchArms(node: *const Node, writer: Writer) anyerror!void {
if (node.data != .match_expr) return error.UnsupportedNode;
try writer.writeByte('{');
for (node.data.match_expr.arms) |arm| {
try writer.writeByte(' ');
if (arm.pattern) |pat| {
try writer.writeAll("case ");
try printExpr(pat, writer);
try writer.writeAll(": ");
} else {
try writer.writeAll("else: ");
}
if (arm.body.data != .block or arm.body.data.block.stmts.len != 1) {
return error.UnsupportedNode;
}
try printExpr(arm.body.data.block.stmts[0], writer);
try writer.writeByte(';');
}
try writer.writeAll(" }");
}
fn binaryOpStr(op: ast.BinaryOp.Op) ?[]const u8 {
return switch (op) {
.or_op => "or",
.and_op => "and",
.add => "+",
.sub => "-",
.mul => "*",
.div => "/",
.eq => "==",
.neq => "!=",
.lt => "<",
.lte => "<=",
.gt => ">",
.gte => ">=",
else => null,
};
}