From e88ee6695311e4db28d748acd74ee8d889ff38c1 Mon Sep 17 00:00:00 2001 From: agra Date: Sun, 31 May 2026 16:40:22 +0300 Subject: [PATCH] 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. --- src/ast.zig | 19 +++++ src/lsp/server.zig | 1 + src/parser.zig | 200 ++++++++++++++++++++++++++++++++++++++++++++- src/print.zig | 70 ++++++++++++++++ src/root.zig | 1 + src/sema.zig | 8 ++ src/token.zig | 2 + 7 files changed, 300 insertions(+), 1 deletion(-) create mode 100644 src/print.zig diff --git a/src/ast.zig b/src/ast.zig index e79d64d..6c488c7 100644 --- a/src/ast.zig +++ b/src/ast.zig @@ -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, }; diff --git a/src/lsp/server.zig b/src/lsp/server.zig index a969118..8683999 100644 --- a/src/lsp/server.zig +++ b/src/lsp/server.zig @@ -1652,6 +1652,7 @@ pub const Server = struct { .kw_true, .kw_false, .kw_enum, + .kw_error, .kw_case, .kw_break, .kw_continue, diff --git a/src/parser.zig b/src/parser.zig index 98853df..b378438 100644 --- a/src/parser.zig +++ b/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); + } +} diff --git a/src/print.zig b/src/print.zig new file mode 100644 index 0000000..e6bddaa --- /dev/null +++ b/src/print.zig @@ -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, + } +} diff --git a/src/root.zig b/src/root.zig index cf255da..b2a999a 100644 --- a/src/root.zig +++ b/src/root.zig @@ -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"); diff --git a/src/sema.zig b/src/sema.zig index 3ec86c3..2faa5e4 100644 --- a/src/sema.zig +++ b/src/sema.zig @@ -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, diff --git a/src/token.zig b/src/token.zig index acafcb9..4443712 100644 --- a/src/token.zig +++ b/src/token.zig @@ -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 },