ERR/E0.2: raise / try / catch / onfail + precedence + consumer-aware pipe (parser)
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.
This commit is contained in:
38
src/ast.zig
38
src/ast.zig
@@ -58,6 +58,10 @@ pub const Node = struct {
|
||||
many_pointer_type_expr: ManyPointerTypeExpr,
|
||||
optional_type_expr: OptionalTypeExpr,
|
||||
error_type_expr: ErrorTypeExpr,
|
||||
raise_stmt: RaiseStmt,
|
||||
try_expr: TryExpr,
|
||||
catch_expr: CatchExpr,
|
||||
onfail_stmt: OnFailStmt,
|
||||
pack_index_type_expr: PackIndexTypeExpr,
|
||||
comptime_pack_ref: ComptimePackRef,
|
||||
force_unwrap: ForceUnwrap,
|
||||
@@ -407,6 +411,40 @@ pub const DeferStmt = struct {
|
||||
expr: *Node,
|
||||
};
|
||||
|
||||
// ── Error handling (ERR stream) ──────────────────────────────────────────
|
||||
|
||||
/// `raise EXPR;` — terminates control flow like `return`, populating the
|
||||
/// error channel. `tag` is a tag-typed expression: `error.X` (a field
|
||||
/// access on the `error` keyword) or a tag-bound variable (`raise e`).
|
||||
pub const RaiseStmt = struct {
|
||||
tag: *Node,
|
||||
};
|
||||
|
||||
/// `try X` — a failable attempt. Unary prefix, binds tighter than any
|
||||
/// binary operator. Sema (E1.4) rejects a non-failable operand.
|
||||
pub const TryExpr = struct {
|
||||
operand: *Node,
|
||||
};
|
||||
|
||||
/// `X catch [e] BODY` — inline failure handler (postfix). The binding is a
|
||||
/// bare name (no parens) and optional. Body is a block, a bare expression,
|
||||
/// or — when `is_match_body` — a `match_expr` from the `== { case ... }`
|
||||
/// sugar (whose subject is the binding).
|
||||
pub const CatchExpr = struct {
|
||||
operand: *Node,
|
||||
binding: ?[]const u8 = null,
|
||||
body: *Node,
|
||||
is_match_body: bool = false,
|
||||
};
|
||||
|
||||
/// `onfail [e] BODY` — cleanup run on error-exit of the enclosing block.
|
||||
/// Binding optional (bare name). Body is a block (`onfail [e] { ... }`) or
|
||||
/// a bare expression (`onfail EXPR;`).
|
||||
pub const OnFailStmt = struct {
|
||||
binding: ?[]const u8 = null,
|
||||
body: *Node,
|
||||
};
|
||||
|
||||
pub const PushStmt = struct {
|
||||
context_expr: *Node,
|
||||
body: *Node,
|
||||
|
||||
@@ -1653,6 +1653,10 @@ pub const Server = struct {
|
||||
.kw_false,
|
||||
.kw_enum,
|
||||
.kw_error,
|
||||
.kw_raise,
|
||||
.kw_try,
|
||||
.kw_catch,
|
||||
.kw_onfail,
|
||||
.kw_case,
|
||||
.kw_break,
|
||||
.kw_continue,
|
||||
|
||||
363
src/parser.zig
363
src/parser.zig
@@ -28,6 +28,10 @@ pub const Parser = struct {
|
||||
/// while parsing a `for` range bound so `for 0..N (i)` reads `N` as the
|
||||
/// end and leaves `(i)` for the cursor rather than parsing `N(i)`.
|
||||
suppress_call: bool = false,
|
||||
/// When true (set while parsing an `onfail` body), a `raise` statement is
|
||||
/// rejected — an error during cleanup has no propagation target. E1.7
|
||||
/// extends this to the full {try, return, break, continue} set.
|
||||
in_onfail_body: bool = false,
|
||||
|
||||
pub fn init(allocator: std.mem.Allocator, source: [:0]const u8) Parser {
|
||||
var lexer = Lexer.init(source);
|
||||
@@ -2011,6 +2015,42 @@ pub const Parser = struct {
|
||||
return try self.createNode(start, .{ .defer_stmt = .{ .expr = deferred } });
|
||||
}
|
||||
|
||||
// Raise statement: raise <expr>;
|
||||
if (self.current.tag == .kw_raise) {
|
||||
const start = self.current.loc.start;
|
||||
if (self.in_onfail_body) {
|
||||
return self.fail("`raise` is not allowed inside an `onfail` body — an error during cleanup has no propagation target");
|
||||
}
|
||||
self.advance();
|
||||
const tag_expr = try self.parseExpr();
|
||||
try self.expect(.semicolon);
|
||||
return try self.createNode(start, .{ .raise_stmt = .{ .tag = tag_expr } });
|
||||
}
|
||||
|
||||
// Onfail statement: onfail { body } | onfail e { body } | onfail <expr>;
|
||||
// A binding is present only when an identifier is immediately followed
|
||||
// by `{`; otherwise the text after `onfail` is the (no-binding) body.
|
||||
if (self.current.tag == .kw_onfail) {
|
||||
const start = self.current.loc.start;
|
||||
self.advance();
|
||||
var binding: ?[]const u8 = null;
|
||||
if (self.current.tag == .identifier and self.peekNext() == .l_brace) {
|
||||
binding = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
}
|
||||
const saved_onfail = self.in_onfail_body;
|
||||
self.in_onfail_body = true;
|
||||
defer self.in_onfail_body = saved_onfail;
|
||||
const body: *Node = if (self.current.tag == .l_brace)
|
||||
try self.parseBlock()
|
||||
else blk: {
|
||||
const e = try self.parseExpr();
|
||||
try self.expect(.semicolon);
|
||||
break :blk e;
|
||||
};
|
||||
return try self.createNode(start, .{ .onfail_stmt = .{ .binding = binding, .body = body } });
|
||||
}
|
||||
|
||||
// Break statement: break;
|
||||
if (self.current.tag == .kw_break) {
|
||||
const start = self.current.loc.start;
|
||||
@@ -2146,23 +2186,37 @@ pub const Parser = struct {
|
||||
var lhs = initial_lhs;
|
||||
|
||||
while (true) {
|
||||
// Pipe operator: desugar a |> f(args) → f(a, args), a |> f → f(a)
|
||||
// Pipe operator: desugar a |> f(args) → f(a, args), a |> f → f(a).
|
||||
// Consumer-aware: the piped LHS goes into the RHS's HEAD call,
|
||||
// looking THROUGH a `try` prefix / `catch` postfix / `or` fallback
|
||||
// and leaving the wrapper intact:
|
||||
// a |> try f(x) → try f(a, x)
|
||||
// a |> f(x) catch e {...} → f(a, x) catch e {...}
|
||||
// a |> f(x) or default → f(a, x) or default (only f gets a)
|
||||
if (self.current.tag == .pipe_arrow and Prec.pipe >= min_prec) {
|
||||
self.advance();
|
||||
// Parse the RHS as a full call expression (higher precedence)
|
||||
const rhs = try self.parseBinary(Prec.pipe + 1);
|
||||
// Desugar based on RHS shape
|
||||
if (rhs.data == .call) {
|
||||
// a |> f(args) → f(a, args...)
|
||||
// Walk through error-handling wrappers to the head call node.
|
||||
var head = rhs;
|
||||
const head_call: ?*Node = while (true) {
|
||||
switch (head.data) {
|
||||
.call => break head,
|
||||
.try_expr => head = head.data.try_expr.operand,
|
||||
.catch_expr => head = head.data.catch_expr.operand,
|
||||
.binary_op => |bo| {
|
||||
if (bo.op == .or_op) head = bo.lhs else break null;
|
||||
},
|
||||
else => break null,
|
||||
}
|
||||
};
|
||||
if (head_call) |cn| {
|
||||
// a |> ...f(args)... → ...f(a, args)... (wrapper preserved;
|
||||
// mutating the head call in place updates the wrapper).
|
||||
var new_args = std.ArrayList(*Node).empty;
|
||||
try new_args.append(self.allocator, lhs);
|
||||
for (rhs.data.call.args) |arg| {
|
||||
try new_args.append(self.allocator, arg);
|
||||
}
|
||||
lhs = try self.createNode(lhs.span.start, .{ .call = .{
|
||||
.callee = rhs.data.call.callee,
|
||||
.args = try new_args.toOwnedSlice(self.allocator),
|
||||
} });
|
||||
for (cn.data.call.args) |arg| try new_args.append(self.allocator, arg);
|
||||
cn.data.call.args = try new_args.toOwnedSlice(self.allocator);
|
||||
lhs = rhs;
|
||||
} else {
|
||||
// a |> f → f(a)
|
||||
const args = try self.allocator.alloc(*Node, 1);
|
||||
@@ -2252,6 +2306,16 @@ pub const Parser = struct {
|
||||
const operand = try self.parseUnary();
|
||||
return try self.createNode(start, .{ .unary_op = .{ .op = .address_of, .operand = operand } });
|
||||
}
|
||||
// `try X` — failable-attempt prefix. Joins the unary tier (binds
|
||||
// tighter than any binary op incl. `or`); right-recursive so prefixes
|
||||
// stack by adjacency (`xx try foo()` = `xx (try foo())`). Failability
|
||||
// of the operand is a sema check (E1.4), not a parse-time restriction.
|
||||
if (self.current.tag == .kw_try) {
|
||||
const start = self.current.loc.start;
|
||||
self.advance();
|
||||
const operand = try self.parseUnary();
|
||||
return try self.createNode(start, .{ .try_expr = .{ .operand = operand } });
|
||||
}
|
||||
// cast(Type) expr — prefix operator with type parameter
|
||||
if (self.current.tag == .identifier and std.mem.eql(u8, self.tokenSlice(self.current), "cast")) {
|
||||
const saved_lexer = self.lexer;
|
||||
@@ -2403,6 +2467,38 @@ pub const Parser = struct {
|
||||
// Only if it's not != (bang_equal would have been lexed as a single token)
|
||||
self.advance();
|
||||
expr = try self.createNode(expr.span.start, .{ .force_unwrap = .{ .operand = expr } });
|
||||
} else if (self.current.tag == .kw_catch) {
|
||||
// `X catch [binding] BODY` — postfix failure handler.
|
||||
// Four shapes, disambiguated by peeking after `catch`:
|
||||
// catch { block } — no binding (braces required)
|
||||
// catch e { block } — binding + block body
|
||||
// catch e == { case ... } — binding + match body (sugar)
|
||||
// catch e EXPR — binding + bare-expression body
|
||||
self.advance(); // consume 'catch'
|
||||
var binding: ?[]const u8 = null;
|
||||
if (self.current.tag == .identifier) {
|
||||
binding = self.tokenSlice(self.current);
|
||||
self.advance();
|
||||
}
|
||||
var is_match_body = false;
|
||||
const body: *Node = if (self.current.tag == .l_brace)
|
||||
try self.parseBlock()
|
||||
else if (binding != null and self.current.tag == .equal_equal) blk: {
|
||||
const m_start = self.current.loc.start;
|
||||
self.advance(); // consume '=='
|
||||
is_match_body = true;
|
||||
const subject = try self.createNode(m_start, .{ .identifier = .{ .name = binding.? } });
|
||||
break :blk try self.parseMatchBody(subject, m_start);
|
||||
} else if (binding != null)
|
||||
try self.parseExpr()
|
||||
else
|
||||
return self.fail("`catch` without a binding requires a braced body: `catch { ... }`");
|
||||
expr = try self.createNode(expr.span.start, .{ .catch_expr = .{
|
||||
.operand = expr,
|
||||
.binding = binding,
|
||||
.body = body,
|
||||
.is_match_body = is_match_body,
|
||||
} });
|
||||
} else {
|
||||
break;
|
||||
}
|
||||
@@ -2661,6 +2757,14 @@ pub const Parser = struct {
|
||||
.hash_jni_env => {
|
||||
return try self.parseJniEnvBlock();
|
||||
},
|
||||
// `error` in expression position is the head of a tag literal
|
||||
// `error.X` (parsed as a field access); sema (E1) gives it meaning.
|
||||
.kw_error => {
|
||||
self.advance();
|
||||
return try self.createNode(start, .{ .identifier = .{ .name = "error" } });
|
||||
},
|
||||
.kw_raise => return self.fail("`raise` is a statement and cannot appear in expression position"),
|
||||
.kw_onfail => return self.fail("`onfail` is a statement and cannot appear in expression position"),
|
||||
else => {
|
||||
return self.fail("unexpected token in expression");
|
||||
},
|
||||
@@ -3997,3 +4101,238 @@ test "round-trip print: bare inferred and named error types" {
|
||||
try std.testing.expectEqualStrings("!ParseErr", aw.writer.toArrayList().items);
|
||||
}
|
||||
}
|
||||
|
||||
// ── ERR step E0.2 — raise / try / catch / onfail + precedence + pipe ──
|
||||
|
||||
/// Parse `src` (a single `f :: () { ... }` decl) and return its body's first
|
||||
/// statement node.
|
||||
fn e02FirstStmt(alloc: std.mem.Allocator, src: [:0]const u8) anyerror!*Node {
|
||||
var parser = Parser.init(alloc, src);
|
||||
const root = try parser.parse();
|
||||
return root.data.root.decls[0].data.fn_decl.body.data.block.stmts[0];
|
||||
}
|
||||
|
||||
/// Parse `src` (a `f :: () { v := EXPR; }` decl) and return the EXPR node.
|
||||
fn e02FirstValue(alloc: std.mem.Allocator, src: [:0]const u8) anyerror!*Node {
|
||||
const stmt = try e02FirstStmt(alloc, src);
|
||||
return stmt.data.var_decl.value.?;
|
||||
}
|
||||
|
||||
fn e02ExpectPrints(alloc: std.mem.Allocator, node: *const Node, expected: []const u8) !void {
|
||||
var aw = std.Io.Writer.Allocating.init(alloc);
|
||||
try print.printNode(node, &aw.writer);
|
||||
try std.testing.expectEqualStrings(expected, aw.writer.toArrayList().items);
|
||||
}
|
||||
|
||||
test "E0.2 try binds tighter than or: try foo() or try boo()" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const v = try e02FirstValue(arena.allocator(), "f :: () { v := try foo() or try boo(); }");
|
||||
try std.testing.expect(v.data == .binary_op);
|
||||
try std.testing.expect(v.data.binary_op.op == .or_op);
|
||||
try std.testing.expect(v.data.binary_op.lhs.data == .try_expr);
|
||||
try std.testing.expect(v.data.binary_op.rhs.data == .try_expr);
|
||||
}
|
||||
|
||||
test "E0.2 or is left-associative: a or b or c => (a or b) or c" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const v = try e02FirstValue(arena.allocator(), "f :: () { v := try a() or try b() or try c(); }");
|
||||
try std.testing.expect(v.data == .binary_op and v.data.binary_op.op == .or_op);
|
||||
// LHS is the nested (a or b); RHS is the final operand.
|
||||
try std.testing.expect(v.data.binary_op.lhs.data == .binary_op);
|
||||
try std.testing.expect(v.data.binary_op.lhs.data.binary_op.op == .or_op);
|
||||
try std.testing.expect(v.data.binary_op.rhs.data == .try_expr);
|
||||
}
|
||||
|
||||
test "E0.2 try prefix stacks under xx: xx try foo()" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const v = try e02FirstValue(arena.allocator(), "f :: () { v := xx try foo(); }");
|
||||
try std.testing.expect(v.data == .unary_op and v.data.unary_op.op == .xx);
|
||||
try std.testing.expect(v.data.unary_op.operand.data == .try_expr);
|
||||
}
|
||||
|
||||
test "E0.2 catch no binding, braced body" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch { }; }");
|
||||
try std.testing.expect(v.data == .catch_expr);
|
||||
try std.testing.expect(v.data.catch_expr.binding == null);
|
||||
try std.testing.expect(v.data.catch_expr.is_match_body == false);
|
||||
try std.testing.expect(v.data.catch_expr.body.data == .block);
|
||||
}
|
||||
|
||||
test "E0.2 catch with binding, block body" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch e { bar(); }; }");
|
||||
try std.testing.expect(v.data == .catch_expr);
|
||||
try std.testing.expectEqualStrings("e", v.data.catch_expr.binding.?);
|
||||
try std.testing.expect(v.data.catch_expr.body.data == .block);
|
||||
}
|
||||
|
||||
test "E0.2 catch with binding, bare-expression body" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch e bar(); }");
|
||||
try std.testing.expect(v.data == .catch_expr);
|
||||
try std.testing.expectEqualStrings("e", v.data.catch_expr.binding.?);
|
||||
try std.testing.expect(v.data.catch_expr.is_match_body == false);
|
||||
try std.testing.expect(v.data.catch_expr.body.data == .call);
|
||||
}
|
||||
|
||||
test "E0.2 catch match-body desugars to match_expr over the binding" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const v = try e02FirstValue(arena.allocator(), "f :: () { v := foo() catch e == { case .Empty: 0; else: 1; }; }");
|
||||
try std.testing.expect(v.data == .catch_expr);
|
||||
try std.testing.expect(v.data.catch_expr.is_match_body);
|
||||
try std.testing.expect(v.data.catch_expr.body.data == .match_expr);
|
||||
try std.testing.expectEqual(@as(usize, 2), v.data.catch_expr.body.data.match_expr.arms.len);
|
||||
// subject is the binding identifier
|
||||
try std.testing.expect(v.data.catch_expr.body.data.match_expr.subject.data == .identifier);
|
||||
try std.testing.expectEqualStrings("e", v.data.catch_expr.body.data.match_expr.subject.data.identifier.name);
|
||||
}
|
||||
|
||||
test "E0.2 catch over a parenthesized or-chain" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const v = try e02FirstValue(arena.allocator(), "f :: () { v := (try foo() or try boo()) catch e { }; }");
|
||||
try std.testing.expect(v.data == .catch_expr);
|
||||
try std.testing.expect(v.data.catch_expr.operand.data == .binary_op);
|
||||
try std.testing.expect(v.data.catch_expr.operand.data.binary_op.op == .or_op);
|
||||
}
|
||||
|
||||
test "E0.2 catch without binding and unbraced body is rejected" {
|
||||
// No binding (the token after `catch` is not an identifier) and no braces:
|
||||
// the no-binding form requires a braced body.
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () { v := foo() catch 42; }");
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
test "E0.2 raise error.X parses as raise_stmt over a field access" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const s = try e02FirstStmt(arena.allocator(), "f :: () { raise error.BadDigit; }");
|
||||
try std.testing.expect(s.data == .raise_stmt);
|
||||
try std.testing.expect(s.data.raise_stmt.tag.data == .field_access);
|
||||
try std.testing.expectEqualStrings("BadDigit", s.data.raise_stmt.tag.data.field_access.field);
|
||||
const obj = s.data.raise_stmt.tag.data.field_access.object;
|
||||
try std.testing.expect(obj.data == .identifier);
|
||||
try std.testing.expectEqualStrings("error", obj.data.identifier.name);
|
||||
}
|
||||
|
||||
test "E0.2 raise variable form" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const s = try e02FirstStmt(arena.allocator(), "f :: () { raise e; }");
|
||||
try std.testing.expect(s.data == .raise_stmt);
|
||||
try std.testing.expect(s.data.raise_stmt.tag.data == .identifier);
|
||||
}
|
||||
|
||||
test "E0.2 raise rejected in expression position" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () { x := 1 + raise error.X; }");
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
test "E0.2 raise rejected inside an onfail body" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
var parser = Parser.init(arena.allocator(), "f :: () { onfail { raise error.X; } }");
|
||||
try std.testing.expectError(error.ParseError, parser.parse());
|
||||
}
|
||||
|
||||
test "E0.2 onfail with binding and block body" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const s = try e02FirstStmt(arena.allocator(), "f :: () { onfail e { close(h); } }");
|
||||
try std.testing.expect(s.data == .onfail_stmt);
|
||||
try std.testing.expectEqualStrings("e", s.data.onfail_stmt.binding.?);
|
||||
try std.testing.expect(s.data.onfail_stmt.body.data == .block);
|
||||
}
|
||||
|
||||
test "E0.2 onfail no-binding block vs bare-expression body" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const block_body = try e02FirstStmt(arena.allocator(), "f :: () { onfail { close(h); } }");
|
||||
try std.testing.expect(block_body.data == .onfail_stmt);
|
||||
try std.testing.expect(block_body.data.onfail_stmt.binding == null);
|
||||
try std.testing.expect(block_body.data.onfail_stmt.body.data == .block);
|
||||
|
||||
const expr_body = try e02FirstStmt(arena.allocator(), "f :: () { onfail close(h); }");
|
||||
try std.testing.expect(expr_body.data == .onfail_stmt);
|
||||
try std.testing.expect(expr_body.data.onfail_stmt.binding == null);
|
||||
try std.testing.expect(expr_body.data.onfail_stmt.body.data == .call);
|
||||
}
|
||||
|
||||
test "E0.2 consumer-aware pipe: x |> try f() inserts x into the head call" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const v = try e02FirstValue(arena.allocator(), "f :: () { v := x |> try f(); }");
|
||||
try std.testing.expect(v.data == .try_expr);
|
||||
const call = v.data.try_expr.operand;
|
||||
try std.testing.expect(call.data == .call);
|
||||
try std.testing.expectEqual(@as(usize, 1), call.data.call.args.len);
|
||||
try std.testing.expect(call.data.call.args[0].data == .identifier);
|
||||
try std.testing.expectEqualStrings("x", call.data.call.args[0].data.identifier.name);
|
||||
}
|
||||
|
||||
test "E0.2 consumer-aware pipe: x |> f() catch e { } preserves the catch" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const v = try e02FirstValue(arena.allocator(), "f :: () { v := x |> g() catch e { }; }");
|
||||
try std.testing.expect(v.data == .catch_expr);
|
||||
try std.testing.expectEqualStrings("e", v.data.catch_expr.binding.?);
|
||||
try std.testing.expect(v.data.catch_expr.operand.data == .call);
|
||||
try std.testing.expectEqual(@as(usize, 1), v.data.catch_expr.operand.data.call.args.len);
|
||||
}
|
||||
|
||||
test "E0.2 consumer-aware pipe: x |> f() or d feeds only the head call" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const v = try e02FirstValue(arena.allocator(), "f :: () { v := x |> g() or d; }");
|
||||
try std.testing.expect(v.data == .binary_op and v.data.binary_op.op == .or_op);
|
||||
// LHS (head call g) receives x; RHS fallback `d` is untouched.
|
||||
try std.testing.expect(v.data.binary_op.lhs.data == .call);
|
||||
try std.testing.expectEqual(@as(usize, 1), v.data.binary_op.lhs.data.call.args.len);
|
||||
try std.testing.expect(v.data.binary_op.rhs.data == .identifier);
|
||||
try std.testing.expectEqualStrings("d", v.data.binary_op.rhs.data.identifier.name);
|
||||
}
|
||||
|
||||
test "E0.2 plain pipe still works: x |> f(a) => f(x, a)" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const v = try e02FirstValue(arena.allocator(), "f :: () { v := x |> g(a); }");
|
||||
try std.testing.expect(v.data == .call);
|
||||
try std.testing.expectEqual(@as(usize, 2), v.data.call.args.len);
|
||||
try std.testing.expectEqualStrings("x", v.data.call.args[0].data.identifier.name);
|
||||
try std.testing.expectEqualStrings("a", v.data.call.args[1].data.identifier.name);
|
||||
}
|
||||
|
||||
test "E0.2 round-trip print: try / or precedence / raise / catch / onfail" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const a = arena.allocator();
|
||||
try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := try foo(); }"), "try foo()");
|
||||
try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := try foo() or try boo(); }"), "try foo() or try boo()");
|
||||
try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { raise error.BadDigit; }"), "raise error.BadDigit");
|
||||
try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { raise e; }"), "raise e");
|
||||
try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := foo() catch e bar(); }"), "foo() catch e bar()");
|
||||
try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := foo() catch e { bar(); }; }"), "foo() catch e { bar(); }");
|
||||
try e02ExpectPrints(a, try e02FirstValue(a, "f :: () { v := foo() catch { bar(); }; }"), "foo() catch { bar(); }");
|
||||
try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { onfail close(h); }"), "onfail close(h)");
|
||||
try e02ExpectPrints(a, try e02FirstStmt(a, "f :: () { onfail e { close(h); } }"), "onfail e { close(h); }");
|
||||
}
|
||||
|
||||
test "E0.2 round-trip print: catch match-body form" {
|
||||
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
|
||||
defer arena.deinit();
|
||||
const a = arena.allocator();
|
||||
const v = try e02FirstValue(a, "f :: () { v := foo() catch e == { case .Empty: 0; else: 1; }; }");
|
||||
try e02ExpectPrints(a, v, "foo() catch e == { case .Empty: 0; else: 1; }");
|
||||
}
|
||||
|
||||
153
src/print.zig
153
src/print.zig
@@ -1,12 +1,12 @@
|
||||
//! Focused AST round-trip printer.
|
||||
//!
|
||||
//! Reconstructs sx source text from AST nodes. The scope is intentionally
|
||||
//! narrow: it covers the declaration and type-expression node kinds the
|
||||
//! error-handling (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 ERR steps extend it as new surface
|
||||
//! syntax lands.
|
||||
//! 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");
|
||||
@@ -14,10 +14,9 @@ const ast = @import("ast.zig");
|
||||
const Node = ast.Node;
|
||||
const Writer = *std.Io.Writer;
|
||||
|
||||
/// Print a declaration node back to source text. Currently handles
|
||||
/// `error { ... }` set declarations; falls through to `printType` for
|
||||
/// type-expression nodes.
|
||||
pub fn printNode(node: *const Node, writer: Writer) !void {
|
||||
/// 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);
|
||||
@@ -29,12 +28,101 @@ pub fn printNode(node: *const Node, writer: Writer) !void {
|
||||
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) !void {
|
||||
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| {
|
||||
@@ -68,3 +156,46 @@ pub fn printType(node: *const Node, writer: Writer) !void {
|
||||
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,
|
||||
};
|
||||
}
|
||||
|
||||
36
src/sema.zig
36
src/sema.zig
@@ -1130,6 +1130,29 @@ pub const Analyzer = struct {
|
||||
.defer_stmt => |ds| {
|
||||
try self.analyzeNode(ds.expr);
|
||||
},
|
||||
.raise_stmt => |rs| {
|
||||
try self.analyzeNode(rs.tag);
|
||||
},
|
||||
.try_expr => |te| {
|
||||
try self.analyzeNode(te.operand);
|
||||
},
|
||||
.catch_expr => |ce| {
|
||||
try self.analyzeNode(ce.operand);
|
||||
try self.pushScope();
|
||||
if (ce.binding) |bname| {
|
||||
try self.addSymbol(bname, .variable, null, node.span);
|
||||
}
|
||||
try self.analyzeNode(ce.body);
|
||||
self.popScope();
|
||||
},
|
||||
.onfail_stmt => |os| {
|
||||
try self.pushScope();
|
||||
if (os.binding) |bname| {
|
||||
try self.addSymbol(bname, .variable, null, node.span);
|
||||
}
|
||||
try self.analyzeNode(os.body);
|
||||
self.popScope();
|
||||
},
|
||||
.push_stmt => |ps| {
|
||||
try self.analyzeNode(ps.context_expr);
|
||||
try self.analyzeNode(ps.body);
|
||||
@@ -1587,6 +1610,19 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node {
|
||||
.defer_stmt => |ds| {
|
||||
if (findNodeAtOffset(ds.expr, offset)) |found| return found;
|
||||
},
|
||||
.raise_stmt => |rs| {
|
||||
if (findNodeAtOffset(rs.tag, offset)) |found| return found;
|
||||
},
|
||||
.try_expr => |te| {
|
||||
if (findNodeAtOffset(te.operand, offset)) |found| return found;
|
||||
},
|
||||
.catch_expr => |ce| {
|
||||
if (findNodeAtOffset(ce.operand, offset)) |found| return found;
|
||||
if (findNodeAtOffset(ce.body, offset)) |found| return found;
|
||||
},
|
||||
.onfail_stmt => |os| {
|
||||
if (findNodeAtOffset(os.body, offset)) |found| return found;
|
||||
},
|
||||
.push_stmt => |ps| {
|
||||
if (findNodeAtOffset(ps.context_expr, offset)) |found| return found;
|
||||
if (findNodeAtOffset(ps.body, offset)) |found| return found;
|
||||
|
||||
@@ -14,6 +14,10 @@ pub const Tag = enum {
|
||||
kw_false,
|
||||
kw_enum,
|
||||
kw_error, // error (error-set declaration)
|
||||
kw_raise, // raise (error propagation statement)
|
||||
kw_try, // try (failable-attempt prefix)
|
||||
kw_catch, // catch (failable handler postfix)
|
||||
kw_onfail, // onfail (error-exit cleanup statement)
|
||||
kw_case,
|
||||
kw_break,
|
||||
kw_continue,
|
||||
@@ -224,6 +228,10 @@ pub const keywords = std.StaticStringMap(Tag).initComptime(.{
|
||||
.{ "false", .kw_false },
|
||||
.{ "enum", .kw_enum },
|
||||
.{ "error", .kw_error },
|
||||
.{ "raise", .kw_raise },
|
||||
.{ "try", .kw_try },
|
||||
.{ "catch", .kw_catch },
|
||||
.{ "onfail", .kw_onfail },
|
||||
.{ "case", .kw_case },
|
||||
.{ "break", .kw_break },
|
||||
.{ "continue", .kw_continue },
|
||||
|
||||
Reference in New Issue
Block a user