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).
204 lines
7.0 KiB
Zig
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,
|
|
};
|
|
}
|