feat(lang): backtick raw-identifier escape + #import c foreign-name exemption [F0.6]
Reserved type-name spellings (s1, s2, u8, …) can now be used as value identifiers two ways, resolving issue 0089: 1. Backtick raw identifier: a leading backtick (`s2) lexes to an .identifier token carrying a new Token.is_raw flag, with the backtick excluded from the text. A raw identifier is never type-classified — the parser skips Type.fromName for it — so it is always a value identifier. The flag threads to VarDecl.is_raw / Param.is_raw at binding sites, and the reserved-type-name check (UnknownTypeChecker) skips raw bindings. Because the token tag stays .identifier, the escape works in every position (local, global, param, field, fn name, struct member, later reference) with no per-site parser change. 2. #import c exemption: c_import.zig synthesizes foreign decls with Param.is_raw = true, so generated C param names that collide with reserved type names (s1, s2) import unedited. A bare reserved-name binding in sx still errors (issue 0076 preserved): the is_raw-gated skip only fires for backtick / foreign names, and a raw binding's address-of / autoref lowering stays correct because every occurrence is an .identifier, never a .type_expr. Tests: examples/0151 (backtick, every position), examples/1220 (foreign exemption, compiled+run), lexer unit tests. 1119 (bare-binding rejection) stays green. specs.md + readme.md updated.
This commit is contained in:
@@ -148,6 +148,10 @@ pub const Param = struct {
|
||||
/// Optional default value expression. When the caller omits this
|
||||
/// parameter, lowering substitutes this expression in its place.
|
||||
default_expr: ?*Node = null,
|
||||
/// True when the param name was written as a backtick raw identifier
|
||||
/// (`` `s2 ``) or synthesized by a `#import c` foreign decl. A raw name is
|
||||
/// exempt from the reserved-type-name binding check (issue 0089).
|
||||
is_raw: bool = false,
|
||||
};
|
||||
|
||||
pub const Block = struct {
|
||||
@@ -303,6 +307,10 @@ pub const VarDecl = struct {
|
||||
is_foreign: bool = false,
|
||||
foreign_lib: ?[]const u8 = null,
|
||||
foreign_name: ?[]const u8 = null,
|
||||
/// True when the binding name was written as a backtick raw identifier
|
||||
/// (`` `s2 := … ``). A raw name is exempt from the reserved-type-name
|
||||
/// binding check (issue 0089).
|
||||
is_raw: bool = false,
|
||||
};
|
||||
|
||||
pub const Assignment = struct {
|
||||
|
||||
@@ -127,6 +127,10 @@ pub fn processCImport(
|
||||
.name = pname,
|
||||
.name_span = .{ .start = 0, .end = 0 },
|
||||
.type_expr = ptype_node,
|
||||
// Foreign C param names (`s1`, `s2`, …) are RAW — exempt from
|
||||
// the reserved-type-name binding check; generated bindings
|
||||
// must import without hand-edits (issue 0089).
|
||||
.is_raw = true,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -117,7 +117,7 @@ pub const UnknownTypeChecker = struct {
|
||||
switch (node.data) {
|
||||
// ── Binding-introducing nodes: check the name(s), then recurse. ──
|
||||
.var_decl => |vd| {
|
||||
self.checkBindingName(vd.name, vd.name_span);
|
||||
if (!vd.is_raw) self.checkBindingName(vd.name, vd.name_span);
|
||||
if (vd.value) |v| self.checkBindingNames(v);
|
||||
},
|
||||
.destructure_decl => |dd| {
|
||||
@@ -133,7 +133,7 @@ pub const UnknownTypeChecker = struct {
|
||||
self.checkBindingNames(lm.body);
|
||||
},
|
||||
.param => |p| {
|
||||
self.checkBindingName(p.name, p.name_span);
|
||||
if (!p.is_raw) self.checkBindingName(p.name, p.name_span);
|
||||
if (p.default_expr) |de| self.checkBindingNames(de);
|
||||
},
|
||||
.if_expr => |ie| {
|
||||
@@ -316,7 +316,9 @@ pub const UnknownTypeChecker = struct {
|
||||
/// (a lambda default), so recurse into it.
|
||||
fn checkParamNames(self: UnknownTypeChecker, params: []const ast.Param) void {
|
||||
for (params) |p| {
|
||||
self.checkBindingName(p.name, p.name_span);
|
||||
// A backtick raw param (`` (`s2: T) ``) or a `#import c` foreign
|
||||
// param is exempt from the reserved-type-name rule (issue 0089).
|
||||
if (!p.is_raw) self.checkBindingName(p.name, p.name_span);
|
||||
if (p.default_expr) |de| self.checkBindingNames(de);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -50,6 +50,24 @@ pub const Lexer = struct {
|
||||
return self.lexString(start);
|
||||
}
|
||||
|
||||
// Raw-identifier escape: `ident — a leading backtick forces the
|
||||
// following identifier to be RAW (never type-classified, never
|
||||
// reserved-checked). The emitted token's span excludes the backtick, so
|
||||
// its text is the bare name, and a backticked keyword spelling
|
||||
// (`` `s2 ``, `` `string ``) is still an `.identifier`, never a keyword.
|
||||
if (c == '`') {
|
||||
const id_start = start + 1;
|
||||
if (id_start < self.source.len and isIdentStart(self.source[id_start])) {
|
||||
self.index = id_start;
|
||||
var tok = self.lexIdentifier(id_start);
|
||||
tok.tag = .identifier;
|
||||
tok.is_raw = true;
|
||||
return tok;
|
||||
}
|
||||
self.index += 1;
|
||||
return self.makeToken(.invalid, start, self.index);
|
||||
}
|
||||
|
||||
|
||||
// Directives: #import, #insert, #run, #builtin, #foreign, #library, #string
|
||||
if (c == '#') {
|
||||
@@ -485,6 +503,38 @@ test "lex type-like identifiers" {
|
||||
}
|
||||
}
|
||||
|
||||
test "lex backtick raw identifier" {
|
||||
const source: [:0]const u8 = "`s2 `string `for";
|
||||
var lex = Lexer.init(source);
|
||||
// Each is an `.identifier` carrying `is_raw`, even a keyword spelling
|
||||
// (`for`), with text that excludes the leading backtick.
|
||||
const t1 = lex.next();
|
||||
try std.testing.expectEqual(Tag.identifier, t1.tag);
|
||||
try std.testing.expect(t1.is_raw);
|
||||
try std.testing.expectEqualStrings("s2", t1.slice(source));
|
||||
const t2 = lex.next();
|
||||
try std.testing.expectEqual(Tag.identifier, t2.tag);
|
||||
try std.testing.expect(t2.is_raw);
|
||||
try std.testing.expectEqualStrings("string", t2.slice(source));
|
||||
const t3 = lex.next();
|
||||
try std.testing.expectEqual(Tag.identifier, t3.tag);
|
||||
try std.testing.expect(t3.is_raw);
|
||||
try std.testing.expectEqualStrings("for", t3.slice(source));
|
||||
try std.testing.expectEqual(Tag.eof, lex.next().tag);
|
||||
}
|
||||
|
||||
test "lex bare identifier is not raw" {
|
||||
var lex = Lexer.init("s2");
|
||||
const tok = lex.next();
|
||||
try std.testing.expectEqual(Tag.identifier, tok.tag);
|
||||
try std.testing.expect(!tok.is_raw);
|
||||
}
|
||||
|
||||
test "lex lone backtick is invalid" {
|
||||
var lex = Lexer.init("` 5");
|
||||
try std.testing.expectEqual(Tag.invalid, lex.next().tag);
|
||||
}
|
||||
|
||||
test "lex hash_run" {
|
||||
var lex = Lexer.init("#run");
|
||||
try std.testing.expectEqual(Tag.hash_run, lex.next().tag);
|
||||
|
||||
@@ -146,6 +146,7 @@ pub const Parser = struct {
|
||||
}
|
||||
const name = self.tokenSlice(self.current);
|
||||
const name_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end };
|
||||
const name_is_raw = self.current.is_raw;
|
||||
self.advance();
|
||||
|
||||
// IDENT :: ...
|
||||
@@ -158,7 +159,7 @@ pub const Parser = struct {
|
||||
// IDENT : type = value; (typed variable)
|
||||
if (self.current.tag == .colon) {
|
||||
self.advance();
|
||||
return self.parseTypedBinding(name, name_span, start);
|
||||
return self.parseTypedBinding(name, name_span, start, name_is_raw);
|
||||
}
|
||||
|
||||
// IDENT := value; (variable)
|
||||
@@ -166,7 +167,7 @@ pub const Parser = struct {
|
||||
self.advance();
|
||||
const value = try self.parseExpr();
|
||||
try self.expectSemicolonAfter(value);
|
||||
return try self.createNode(start, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = null, .value = value } });
|
||||
return try self.createNode(start, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = null, .value = value, .is_raw = name_is_raw } });
|
||||
}
|
||||
|
||||
return self.fail("expected '::', ':=', or ':' after identifier");
|
||||
@@ -383,7 +384,7 @@ pub const Parser = struct {
|
||||
} });
|
||||
}
|
||||
|
||||
fn parseTypedBinding(self: *Parser, name: []const u8, name_span: ast.Span, start_pos: u32) anyerror!*Node {
|
||||
fn parseTypedBinding(self: *Parser, name: []const u8, name_span: ast.Span, start_pos: u32, name_is_raw: bool) anyerror!*Node {
|
||||
// After `name :`
|
||||
// Parse type
|
||||
const type_node = try self.parseTypeExpr();
|
||||
@@ -401,13 +402,13 @@ pub const Parser = struct {
|
||||
self.advance();
|
||||
const value = try self.parseExpr();
|
||||
try self.expectSemicolonAfter(value);
|
||||
return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = type_node, .value = value } });
|
||||
return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = type_node, .value = value, .is_raw = name_is_raw } });
|
||||
}
|
||||
|
||||
if (self.current.tag == .semicolon) {
|
||||
// name : type; (default-initialized variable)
|
||||
self.advance();
|
||||
return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = type_node, .value = null } });
|
||||
return try self.createNode(start_pos, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = type_node, .value = null, .is_raw = name_is_raw } });
|
||||
}
|
||||
|
||||
if (self.current.tag == .hash_foreign) {
|
||||
@@ -433,6 +434,7 @@ pub const Parser = struct {
|
||||
.is_foreign = true,
|
||||
.foreign_lib = lib_ref,
|
||||
.foreign_name = c_name,
|
||||
.is_raw = name_is_raw,
|
||||
} });
|
||||
}
|
||||
|
||||
@@ -1778,11 +1780,12 @@ pub const Parser = struct {
|
||||
}
|
||||
const param_name = self.tokenSlice(self.current);
|
||||
const param_name_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end };
|
||||
const param_is_raw = self.current.is_raw;
|
||||
self.advance();
|
||||
// Optional type annotation: if no ':', infer type from context
|
||||
if (self.current.tag != .colon) {
|
||||
const inferred_node = try self.createNode(param_name_span.start, .{ .inferred_type = {} });
|
||||
try params.append(self.allocator, .{ .name = param_name, .name_span = param_name_span, .type_expr = inferred_node, .is_variadic = is_variadic, .is_comptime = is_ct_param });
|
||||
try params.append(self.allocator, .{ .name = param_name, .name_span = param_name_span, .type_expr = inferred_node, .is_variadic = is_variadic, .is_comptime = is_ct_param, .is_raw = param_is_raw });
|
||||
continue;
|
||||
}
|
||||
self.advance(); // consume ':'
|
||||
@@ -1822,7 +1825,7 @@ pub const Parser = struct {
|
||||
.type_expr, .parameterized_type_expr => true,
|
||||
else => false,
|
||||
};
|
||||
try params.append(self.allocator, .{ .name = param_name, .name_span = param_name_span, .type_expr = param_type, .is_variadic = is_variadic, .is_comptime = is_comptime_param, .is_pack = is_pack, .default_expr = default_expr });
|
||||
try params.append(self.allocator, .{ .name = param_name, .name_span = param_name_span, .type_expr = param_type, .is_variadic = is_variadic, .is_comptime = is_comptime_param, .is_pack = is_pack, .default_expr = default_expr, .is_raw = param_is_raw });
|
||||
}
|
||||
for (params.items, 0..) |param, i| {
|
||||
if (param.is_variadic and i != params.items.len - 1) {
|
||||
@@ -2023,6 +2026,7 @@ pub const Parser = struct {
|
||||
const start = self.current.loc.start;
|
||||
const name = self.tokenSlice(self.current);
|
||||
const name_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end };
|
||||
const name_is_raw = self.current.is_raw;
|
||||
self.advance();
|
||||
|
||||
if (self.current.tag == .colon_colon) {
|
||||
@@ -2033,11 +2037,11 @@ pub const Parser = struct {
|
||||
self.advance();
|
||||
const value = try self.parseExpr();
|
||||
try self.expectSemicolonAfter(value);
|
||||
return try self.createNode(start, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = null, .value = value } });
|
||||
return try self.createNode(start, .{ .var_decl = .{ .name = name, .name_span = name_span, .type_annotation = null, .value = value, .is_raw = name_is_raw } });
|
||||
}
|
||||
if (self.current.tag == .colon) {
|
||||
self.advance();
|
||||
return self.parseTypedBinding(name, name_span, start);
|
||||
return self.parseTypedBinding(name, name_span, start, name_is_raw);
|
||||
}
|
||||
|
||||
// Multi-target assignment: ident, expr, ... = expr, expr, ...;
|
||||
@@ -2686,8 +2690,11 @@ pub const Parser = struct {
|
||||
},
|
||||
.identifier => {
|
||||
const name = self.tokenSlice(self.current);
|
||||
// Check if this identifier is a type name (e.g. s32, u8, s128)
|
||||
if (Type.fromName(name) != null) {
|
||||
// A backtick raw identifier (`` `s2 ``) is NEVER type-classified —
|
||||
// it is always a value identifier, bypassing the reserved-type-name
|
||||
// rule (issue 0089). Only a bare spelling is checked for a type name
|
||||
// (e.g. s32, u8, s128).
|
||||
if (!self.current.is_raw and Type.fromName(name) != null) {
|
||||
self.advance();
|
||||
return try self.createNode(start, .{ .type_expr = .{ .name = name } });
|
||||
}
|
||||
|
||||
@@ -210,6 +210,12 @@ pub const Tag = enum {
|
||||
pub const Token = struct {
|
||||
tag: Tag,
|
||||
loc: Loc,
|
||||
/// True when an `.identifier` was introduced by a leading backtick
|
||||
/// (`` `s2 ``): a RAW identifier whose text excludes the backtick and which
|
||||
/// the parser must NEVER type-classify (it bypasses the reserved-type-name
|
||||
/// rule). `loc` already spans only the un-backticked name, so `slice` returns
|
||||
/// the bare text.
|
||||
is_raw: bool = false,
|
||||
|
||||
pub const Loc = struct {
|
||||
start: u32,
|
||||
|
||||
Reference in New Issue
Block a user