From b25a2f60d6d08b83b492745c38fddc22d0e86a1d Mon Sep 17 00:00:00 2001 From: agra Date: Tue, 16 Jun 2026 18:22:21 +0300 Subject: [PATCH] feat(parser): reserved keyword as member name after `.` MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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). --- examples/0614-comptime-reify-enum.sx | 2 +- readme.md | 18 +++++++++++++- src/parser.zig | 36 ++++++++++++++++++++-------- 3 files changed, 44 insertions(+), 12 deletions(-) diff --git a/examples/0614-comptime-reify-enum.sx b/examples/0614-comptime-reify-enum.sx index 98a4a975..21612d9c 100644 --- a/examples/0614-comptime-reify-enum.sx +++ b/examples/0614-comptime-reify-enum.sx @@ -8,7 +8,7 @@ #import "modules/std.sx"; #import "modules/std/meta.sx"; -E :: reify(.`enum(.{ variants = .[ +E :: reify(.enum(.{ variants = .[ EnumVariant.{ name = "value", payload = i64 }, EnumVariant.{ name = "closed", payload = void }, ] })); diff --git a/readme.md b/readme.md index 91d176e7..05df0144 100644 --- a/readme.md +++ b/readme.md @@ -184,7 +184,23 @@ positions are exempt**: a struct *field*, a union *tag*, and a protocol so they never mis-lower. The bare exemption covers only the identifier-classified reserved names (`i1`..`i64`, `u1`..`u64`, `bool`, `string`, `void`, `usize`, `isize`, `Any`); `f32` and `f64` are lexer keywords, so even in a member slot they -need the backtick (`` struct { `f32: i64 } ``). A leading backtick escapes one into +need the backtick (`` struct { `f32: i64 } ``). + +**After a leading `.`**, however, even a full lexer keyword is accepted bare as the +member/variant name — the dot makes the keyword reading impossible, so no backtick +is needed. This covers the enum-literal (`.enum`), field-access (`x.enum`, +`E.struct`), and match-arm (`case .enum:`) positions. A *declaration* of such a +variant still needs the backtick (`` enum { `enum: i64 } ``), since the decl site +has no disambiguating dot: + +```sx +TI :: enum { `enum: i64; closed; } // decl: backtick needed (no dot) +t := TI.enum(7); // construct: dot disambiguates — bare `enum` +if t == { case .enum: (v) { … } // match: likewise bare after `.` + case .closed: { … } } +``` + +A leading backtick escapes one into a **raw identifier**: `` `name `` is the literal identifier `name` (the backtick drops out of the text), usable in **every** position — value, declaration, and type, and optional in the diff --git a/src/parser.zig b/src/parser.zig index fd779428..e8a36d9a 100644 --- a/src/parser.zig +++ b/src/parser.zig @@ -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;