Parser-only second step of the error-handling stream. No sema/codegen.
- token: 4 keywords — `raise`, `try`, `catch`, `onfail`.
- ast: RaiseStmt, TryExpr, CatchExpr {operand, binding?, body, is_match_body},
OnFailStmt {binding?, body}.
- parser:
- `try` is a unary prefix (binds tighter than `or`; right-recursive so it
stacks under `xx`/`@`/etc).
- `or` is already left-associative (precedence-climbing loop) — no change.
- `catch` is a postfix with four body shapes (no-binding block / block /
bare-expr / `== { case }` match-body, the latter reusing parseMatchBody
with the binding as subject).
- `raise EXPR;` and `onfail [e] { } | onfail EXPR;` statements; `error`
parses in expression position so `raise error.X` works; raise rejected
in expression position and inside an onfail body (in_onfail_body flag).
- consumer-aware `|>`: pipes the LHS into the head call through a
try/catch/or wrapper, preserving the wrapper.
- print: printExpr + match-arm printing for round-trips (anyerror!void to
break the printExpr<->printMatchArms inferred-error-set loop).
- sema/lsp: exhaustive switch arms for the 4 nodes + 4 keyword tokens.
- tests: ~22 inline parser tests (precedence, all catch forms, both
rejections, pipe cases, round-trip prints incl. match-body).
zig build, zig build test (264), and 254/254 examples green.
202 lines
6.9 KiB
Zig
202 lines
6.9 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.writeByte(' ');
|
|
try writer.writeAll(bnd);
|
|
}
|
|
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.writeByte(' ');
|
|
try writer.writeAll(bnd);
|
|
}
|
|
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,
|
|
};
|
|
}
|