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

@@ -37,6 +37,7 @@ pub const Node = struct {
struct_decl: StructDecl,
struct_literal: StructLiteral,
union_decl: UnionDecl,
error_set_decl: ErrorSetDecl,
lambda: Lambda,
type_expr: TypeExpr,
param: Param,
@@ -56,6 +57,7 @@ pub const Node = struct {
pointer_type_expr: PointerTypeExpr,
many_pointer_type_expr: ManyPointerTypeExpr,
optional_type_expr: OptionalTypeExpr,
error_type_expr: ErrorTypeExpr,
pack_index_type_expr: PackIndexTypeExpr,
comptime_pack_ref: ComptimePackRef,
force_unwrap: ForceUnwrap,
@@ -94,6 +96,7 @@ pub const Node = struct {
.enum_decl => |d| d.name,
.struct_decl => |d| d.name,
.union_decl => |d| d.name,
.error_set_decl => |d| d.name,
.namespace_decl => |d| d.name,
.ufcs_alias => |d| d.name,
.c_import_decl => |d| d.name,
@@ -324,6 +327,13 @@ pub const UnionDecl = struct {
field_types: []const *Node,
};
/// `Foo :: error { TagA, TagB }` — a named error set. Tags are bare
/// identifiers (no payload, no explicit value), unlike enum variants.
pub const ErrorSetDecl = struct {
name: []const u8,
tag_names: []const []const u8,
};
pub const StructTypeParam = struct {
name: []const u8, // e.g. "N" or "T" (without $)
constraint: *Node, // type_expr: "u32" for value param, "Type" for type param
@@ -461,6 +471,15 @@ pub const OptionalTypeExpr = struct {
inner_type: *Node,
};
/// The error channel of a multi-return result list: bare `!` (inferred
/// set) or `!Named` (a declared `error { ... }` set). Appears only as
/// the trailing result element; the parser enforces the position and
/// sema (E1) restricts it to return positions.
pub const ErrorTypeExpr = struct {
/// `null` = inferred set (bare `!`); non-null = named set (`!Named`).
name: ?[]const u8 = null,
};
pub const ForceUnwrap = struct {
operand: *Node,
};

View File

@@ -1652,6 +1652,7 @@ pub const Server = struct {
.kw_true,
.kw_false,
.kw_enum,
.kw_error,
.kw_case,
.kw_break,
.kw_continue,

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);
}
}

70
src/print.zig Normal file
View File

@@ -0,0 +1,70 @@
//! 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.
const std = @import("std");
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 {
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 printType(node, writer),
}
}
/// Print a type-expression node back to source text.
pub fn printType(node: *const Node, writer: Writer) !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,
}
}

View File

@@ -3,6 +3,7 @@ pub const token = @import("token.zig");
pub const lexer = @import("lexer.zig");
pub const ast = @import("ast.zig");
pub const parser = @import("parser.zig");
pub const print = @import("print.zig");
pub const types = @import("types.zig");
pub const target = @import("target.zig");
pub const builtins = @import("builtins.zig");

View File

@@ -1155,6 +1155,11 @@ pub const Analyzer = struct {
.union_decl => |ud| {
try self.addSymbol(ud.name, .enum_type, .{ .union_type = ud.name }, node.span);
},
.error_set_decl => |esd| {
// Register the set name; error-set semantics arrive in the ERR
// stream's E1 sema steps.
try self.addSymbol(esd.name, .type_alias, null, node.span);
},
// Leaf nodes — nothing to recurse into
.int_literal,
.float_literal,
@@ -1179,6 +1184,7 @@ pub const Analyzer = struct {
.pointer_type_expr,
.many_pointer_type_expr,
.optional_type_expr,
.error_type_expr,
.pack_index_type_expr,
.comptime_pack_ref,
.null_literal,
@@ -1619,6 +1625,8 @@ pub fn findNodeAtOffset(node: *Node, offset: u32) ?*Node {
.function_type_expr,
.enum_decl,
.union_decl,
.error_set_decl,
.error_type_expr,
.import_decl,
.c_import_decl,
.array_type_expr,

View File

@@ -13,6 +13,7 @@ pub const Tag = enum {
kw_true,
kw_false,
kw_enum,
kw_error, // error (error-set declaration)
kw_case,
kw_break,
kw_continue,
@@ -222,6 +223,7 @@ pub const keywords = std.StaticStringMap(Tag).initComptime(.{
.{ "true", .kw_true },
.{ "false", .kw_false },
.{ "enum", .kw_enum },
.{ "error", .kw_error },
.{ "case", .kw_case },
.{ "break", .kw_break },
.{ "continue", .kw_continue },