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:
19
src/ast.zig
19
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,
|
||||
};
|
||||
|
||||
@@ -1652,6 +1652,7 @@ pub const Server = struct {
|
||||
.kw_true,
|
||||
.kw_false,
|
||||
.kw_enum,
|
||||
.kw_error,
|
||||
.kw_case,
|
||||
.kw_break,
|
||||
.kw_continue,
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
70
src/print.zig
Normal file
70
src/print.zig
Normal 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,
|
||||
}
|
||||
}
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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 },
|
||||
|
||||
Reference in New Issue
Block a user