ERR/E0.1: error-set decls + ! / !Named type exprs (parser)

Parser-only first step of the error-handling stream. No sema/codegen.

- token: `kw_error` keyword (`!` reuses existing `.bang`).
- ast: `ErrorSetDecl { name, tag_names }` + `ErrorTypeExpr { name: ?[] }`
  (null = inferred `!`, non-null = `!Named`); wired into Node.Data and
  declName.
- parser: `parseErrorSetDecl` (comma-separated tags, optional trailing
  comma/`;`) dispatched from parseConstBinding; `!` / `!Named` parsed in
  parseTypeExpr; result-list loop enforces error type as trailing-only;
  hasFnBodyAfterArrow skips `.bang` so failable-return fns are recognised.
- print: new focused AST round-trip printer (decls + type exprs); loud
  `error.UnsupportedNode` otherwise. Registered in root.zig.
- sema/lsp: exhaustive switch arms for the two new nodes.
- tests: 11 inline parser unit tests (shapes + 3 round-trip prints + 2
  trailing-position rejections).

zig build, zig build test, and 254/254 examples green.
This commit is contained in:
agra
2026-05-31 16:40:22 +03:00
parent f7f9def0e7
commit e88ee66953
7 changed files with 300 additions and 1 deletions

View File

@@ -6,6 +6,7 @@ const ast = @import("ast.zig");
const Node = ast.Node;
const Type = @import("types.zig").Type;
const errors = @import("errors.zig");
const print = @import("print.zig");
pub const Parser = struct {
lexer: Lexer,
@@ -208,6 +209,11 @@ pub const Parser = struct {
return self.parseEnumDecl(name, start_pos);
}
// Error-set declaration: name :: error { TagA, TagB }
if (self.current.tag == .kw_error) {
return self.parseErrorSetDecl(name, start_pos);
}
// Struct declaration
if (self.current.tag == .kw_struct) {
return self.parseStructDecl(name, start_pos);
@@ -413,6 +419,20 @@ pub const Parser = struct {
fn parseTypeExpr(self: *Parser) anyerror!*Node {
const start = self.current.loc.start;
// Error channel type: bare `!` (inferred set) or `!Named` (named set).
// Legal only as the trailing element of a multi-return result list
// (enforced by the parenthesized-list loop below) or as a bare
// failable return type. Sema (E1) restricts it to return positions.
if (self.current.tag == .bang) {
self.advance(); // skip '!'
var set_name: ?[]const u8 = null;
if (self.current.tag == .identifier) {
set_name = self.tokenSlice(self.current);
self.advance();
}
return try self.createNode(start, .{ .error_type_expr = .{ .name = set_name } });
}
// Optional type: ?T
if (self.current.tag == .question) {
self.advance(); // skip '?'
@@ -514,11 +534,17 @@ pub const Parser = struct {
var param_types = std.ArrayList(*Node).empty;
var param_names = std.ArrayList(?[]const u8).empty;
var has_names = false;
// An error channel type (`!` / `!Named`) is only valid as the
// trailing element of a result list. Reject any element after it.
var saw_error_type = false;
while (self.current.tag != .r_paren and self.current.tag != .eof) {
if (param_types.items.len > 0) {
try self.expect(.comma);
if (self.current.tag == .r_paren) break; // trailing comma ok
}
if (saw_error_type) {
return self.fail("error type '!' must be the last element of a result list");
}
// Pack expansion in a tuple/function type: `(..F(Ts))` /
// `(..F(Ts.Arg))` / `(..Ts)`. Reuses `spread_expr`; its operand
// is the per-element type expression (e.g. `F(Ts)`), carrying any
@@ -543,7 +569,9 @@ pub const Parser = struct {
} else {
try param_names.append(self.allocator, null);
}
try param_types.append(self.allocator, try self.parseTypeExpr());
const elem = try self.parseTypeExpr();
if (elem.data == .error_type_expr) saw_error_type = true;
try param_types.append(self.allocator, elem);
}
try self.expect(.r_paren);
if (self.current.tag == .arrow) {
@@ -804,6 +832,31 @@ pub const Parser = struct {
} });
}
fn parseErrorSetDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
self.advance(); // skip 'error'
try self.expect(.l_brace);
var tag_names = std.ArrayList([]const u8).empty;
while (self.current.tag != .r_brace and self.current.tag != .eof) {
if (tag_names.items.len > 0) {
try self.expect(.comma);
if (self.current.tag == .r_brace) break; // trailing comma ok
}
if (self.current.tag != .identifier) {
return self.fail("expected error tag name");
}
try tag_names.append(self.allocator, self.tokenSlice(self.current));
self.advance();
}
try self.expect(.r_brace);
// Accept an optional trailing `;` — error-set decls read like value
// bindings and are commonly written `Foo :: error { ... };`.
if (self.current.tag == .semicolon) self.advance();
return try self.createNode(start_pos, .{ .error_set_decl = .{
.name = name,
.tag_names = try tag_names.toOwnedSlice(self.allocator),
} });
}
fn parseUnionDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
self.advance(); // skip 'union'
try self.expect(.l_brace);
@@ -3228,6 +3281,7 @@ pub const Parser = struct {
self.current.tag == .l_paren or self.current.tag == .r_paren or
self.current.tag == .comma or self.current.tag == .int_literal or
self.current.tag == .star or self.current.tag == .question or
self.current.tag == .bang or
self.current.tag == .colon or self.current.tag == .arrow)
{
self.advance();
@@ -3799,3 +3853,147 @@ test "parse pack expansion: call-arg spread q(..xs) reuses spread_expr" {
try std.testing.expectEqual(@as(usize, 1), call.data.call.args.len);
try std.testing.expect(call.data.call.args[0].data == .spread_expr);
}
// ── ERR step E0.1 — `error { ... }` decls + `!` / `!Named` type exprs ──
test "parse error-set decl: tags collected" {
const source = "ParseErr :: error { BadDigit, Overflow, Empty }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
try std.testing.expectEqual(@as(usize, 1), root.data.root.decls.len);
const decl = root.data.root.decls[0];
try std.testing.expect(decl.data == .error_set_decl);
try std.testing.expectEqualStrings("ParseErr", decl.data.error_set_decl.name);
const tags = decl.data.error_set_decl.tag_names;
try std.testing.expectEqual(@as(usize, 3), tags.len);
try std.testing.expectEqualStrings("BadDigit", tags[0]);
try std.testing.expectEqualStrings("Overflow", tags[1]);
try std.testing.expectEqualStrings("Empty", tags[2]);
}
test "parse error-set decl: single tag, trailing comma, trailing semicolon" {
const source = "E :: error { Only, };";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
const decl = root.data.root.decls[0];
try std.testing.expect(decl.data == .error_set_decl);
const tags = decl.data.error_set_decl.tag_names;
try std.testing.expectEqual(@as(usize, 1), tags.len);
try std.testing.expectEqualStrings("Only", tags[0]);
}
test "parse bare failable return: inferred `!`" {
const source = "f :: () -> ! { 0; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
try std.testing.expect(rt.data == .error_type_expr);
try std.testing.expect(rt.data.error_type_expr.name == null);
}
test "parse bare failable return: named `!Foo`" {
const source = "f :: () -> !ParseErr { 0; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
try std.testing.expect(rt.data == .error_type_expr);
try std.testing.expectEqualStrings("ParseErr", rt.data.error_type_expr.name.?);
}
test "parse multi-return with inferred `!` as trailing element" {
const source = "f :: () -> (s32, !) { 0; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
try std.testing.expect(rt.data == .tuple_type_expr);
const fields = rt.data.tuple_type_expr.field_types;
try std.testing.expectEqual(@as(usize, 2), fields.len);
try std.testing.expect(fields[0].data == .type_expr);
try std.testing.expectEqualStrings("s32", fields[0].data.type_expr.name);
try std.testing.expect(fields[1].data == .error_type_expr);
try std.testing.expect(fields[1].data.error_type_expr.name == null);
}
test "parse multi-return with named `!Foo` as trailing element" {
const source = "f :: () -> (s32, s64, !ParseErr) { 0; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
try std.testing.expect(rt.data == .tuple_type_expr);
const fields = rt.data.tuple_type_expr.field_types;
try std.testing.expectEqual(@as(usize, 3), fields.len);
try std.testing.expect(fields[2].data == .error_type_expr);
try std.testing.expectEqualStrings("ParseErr", fields[2].data.error_type_expr.name.?);
}
test "parse error type rejected when not the trailing result element" {
const source = "f :: () -> (!, s32) { 0; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
try std.testing.expectError(error.ParseError, parser.parse());
}
test "parse error type rejected in the middle of a result list" {
const source = "f :: () -> (s32, !, s64) { 0; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
try std.testing.expectError(error.ParseError, parser.parse());
}
test "round-trip print: error-set decl" {
const source = "ParseErr :: error { BadDigit, Overflow, Empty }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
var aw = std.Io.Writer.Allocating.init(arena.allocator());
try print.printNode(root.data.root.decls[0], &aw.writer);
try std.testing.expectEqualStrings(source, aw.writer.toArrayList().items);
}
test "round-trip print: multi-return result list with pointer + named error" {
const source = "open :: () -> (*Handle, !IoErr) { 0; }";
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
var parser = Parser.init(arena.allocator(), source);
const root = try parser.parse();
const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
var aw = std.Io.Writer.Allocating.init(arena.allocator());
try print.printType(rt, &aw.writer);
try std.testing.expectEqualStrings("(*Handle, !IoErr)", aw.writer.toArrayList().items);
}
test "round-trip print: bare inferred and named error types" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
{
var parser = Parser.init(arena.allocator(), "f :: () -> ! { 0; }");
const root = try parser.parse();
const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
var aw = std.Io.Writer.Allocating.init(arena.allocator());
try print.printType(rt, &aw.writer);
try std.testing.expectEqualStrings("!", aw.writer.toArrayList().items);
}
{
var parser = Parser.init(arena.allocator(), "f :: () -> !ParseErr { 0; }");
const root = try parser.parse();
const rt = root.data.root.decls[0].data.fn_decl.return_type.?;
var aw = std.Io.Writer.Allocating.init(arena.allocator());
try print.printType(rt, &aw.writer);
try std.testing.expectEqualStrings("!ParseErr", aw.writer.toArrayList().items);
}
}