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:
200
src/parser.zig
200
src/parser.zig
@@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user