feat(parser): reserved keyword as member name after .

After a leading `.` (enum literal `.enum`, field access `x.enum` /
`E.struct`, match arm `case .enum:`) a reserved keyword is unambiguously
the member/variant NAME — the dot rules out the keyword reading — so no
backtick escape is needed. A declaration of such a variant still needs
the backtick (enum { `enum: i64 }), since the decl site has no dot.

Adds Parser.dotMemberName() (identifier OR identifier-shaped keyword)
and routes the leading-dot enum-literal and postfix field-access sites
through it. readme updated. The reify example 0614 now uses the cleaner
reify(.enum(...)) spelling (still xfail — reify lands next commit).
This commit is contained in:
agra
2026-06-16 18:22:21 +03:00
parent 1bec54d0c4
commit b25a2f60d6
3 changed files with 44 additions and 12 deletions

View File

@@ -1,6 +1,7 @@
const std = @import("std");
const Token = @import("token.zig").Token;
const Tag = @import("token.zig").Tag;
const getKeyword = @import("token.zig").getKeyword;
const Lexer = @import("lexer.zig").Lexer;
const ast = @import("ast.zig");
const Node = ast.Node;
@@ -2568,16 +2569,16 @@ pub const Parser = struct {
// Dereference: expr.*
self.advance();
expr = try self.createNode(expr.span.start, .{ .deref_expr = .{ .operand = expr } });
} else if (self.current.tag == .identifier) {
// Named field access: expr.field
const field = self.tokenSlice(self.current);
self.advance();
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field } });
} else if (self.current.tag == .int_literal) {
// Numeric field access: tuple.0, tuple.1
const field = self.tokenSlice(self.current);
self.advance();
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field } });
} else if (self.dotMemberName()) |field| {
// Named field access: expr.field. A reserved keyword is a
// valid member name here — the leading dot disambiguates
// (`x.enum`, `E.struct`), so no backtick escape is needed.
expr = try self.createNode(expr.span.start, .{ .field_access = .{ .object = expr, .field = field } });
} else {
return self.fail("expected field name or index after '.'");
}
@@ -2960,12 +2961,11 @@ pub const Parser = struct {
try self.expect(.r_bracket);
return try self.createNode(start, .{ .array_literal = .{ .elements = try elements.toOwnedSlice(self.allocator) } });
}
// Enum literal: .variant_name
if (self.current.tag != .identifier) {
// Enum literal: .variant_name. A reserved keyword is a valid
// variant name here — the leading dot disambiguates (`.enum`,
// `.struct`), so no backtick escape is needed.
const name = self.dotMemberName() orelse
return self.fail("expected variant name, '{', or '[' after '.'");
}
const name = self.tokenSlice(self.current);
self.advance();
// Enum literal: .variant_name — parsePostfix handles optional (...) as a call
return try self.createNode(start, .{ .enum_literal = .{ .name = name } });
},
@@ -4106,6 +4106,22 @@ pub const Parser = struct {
return self.source[token.loc.start..token.loc.end];
}
/// After a `.` in member / enum-literal / variant position, a reserved
/// keyword (`enum`, `struct`, `union`, `error`, …) is unambiguously the
/// member NAME — the leading dot rules out the keyword reading, so no
/// backtick escape is needed (`x.enum`, `.enum(p)`, `case .enum:`).
/// Returns the token text and advances when `current` is an identifier OR
/// an identifier-shaped keyword; null otherwise (a real syntax error there,
/// left for the caller to report).
fn dotMemberName(self: *Parser) ?[]const u8 {
const txt = self.tokenSlice(self.current);
if (self.current.tag == .identifier or getKeyword(txt) != null) {
self.advance();
return txt;
}
return null;
}
fn fail(self: *Parser, msg: []const u8) error{ParseError} {
self.err_msg = msg;
self.err_offset = self.current.loc.start;