feat(lang): universal backtick raw identifier — valid in value, decl, AND type position [F0.6]

AGRA ruling (attempt 4): `` `name `` is THE LITERAL identifier `name`, usable in
EVERY position — the backtick only means "treat this token as a plain identifier,
never the reserved keyword/type", and is never part of the name's text.

- Raw in TYPE position is now VALID (reverses attempt-2 "raw is not a type"):
  `parseTypeExpr` emits a raw `type_expr`; `TypeResolver.resolveNamed` gains a
  `skip_builtin` flag (threaded from `te.is_raw` via lower.zig + type_bridge) so a
  `` `s2 `` reference resolves to a `` `s2 ``-declared type (struct/enum/union/alias),
  else a normal "unknown type 's2'" error (reportIfUnknownType skips the builtin
  exemption when raw). Bare `s2` in type position stays the builtin int.
- Every declaration-name site is is_raw-exemptible: `is_raw` added to TypeExpr +
  StructDecl/EnumDecl/UnionDecl/ErrorSetDecl/ProtocolDecl/ForeignClassDecl/UfcsAlias/
  NamespaceDecl/ImportDecl/CImportDecl/LibraryDecl; parser threads name_is_raw to
  every decl parse fn; namespace imports carry it through imports.addNamespace.
  Typed-const path (`` `s2 : s64 : 5 ``) now threads name_span+is_raw (fixes the
  1:1-caret bug).
- Check<->exemption made structurally symmetric: checkBindingName/checkDeclName take
  is_raw as a REQUIRED argument and skip inside the check, so no call site can
  validate a name without honoring the exemption (the desync cause of prior rounds).
- Bare reserved-name declarations of every kind still error (0076 preserved);
  `#import c` foreign names stay auto-raw + bare-callable.

specs.md + readme.md updated to the universal model. issue 0089 RESOLVED banner
rewritten. Examples: replace 1139 (raw-not-a-type) with 0154 (raw type reference);
add 0155 (typed const + union tag) and 1141 (bare type-decl negatives).
Gate: zig build + zig build test + run_examples (426 passed, 0 failed).
This commit is contained in:
agra
2026-06-04 20:27:53 +03:00
parent c0e1a5db82
commit 023971cae5
26 changed files with 441 additions and 212 deletions

View File

@@ -380,12 +380,19 @@ pub const EnumDecl = struct {
is_flags: bool = false,
variant_values: []const ?*Node = &.{}, // explicit value per variant (null = auto), empty = all auto
backing_type: ?*Node = null, // optional backing type: enum u8 { ... }
/// True when the declared NAME was a backtick raw identifier
/// (`` `s2 :: enum { … } ``) — exempt from the reserved-type-name decl
/// check (issue 0089). A bare reserved-name decl still errors.
is_raw: bool = false,
};
pub const UnionDecl = struct {
name: []const u8,
field_names: []const []const u8,
field_types: []const *Node,
/// True when the declared NAME was a backtick raw identifier — exempt from
/// the reserved-type-name decl check (issue 0089).
is_raw: bool = false,
};
/// `Foo :: error { TagA, TagB }` — a named error set. Tags are bare
@@ -393,6 +400,9 @@ pub const UnionDecl = struct {
pub const ErrorSetDecl = struct {
name: []const u8,
tag_names: []const []const u8,
/// True when the declared NAME was a backtick raw identifier — exempt from
/// the reserved-type-name decl check (issue 0089).
is_raw: bool = false,
};
pub const StructTypeParam = struct {
@@ -418,6 +428,10 @@ pub const StructDecl = struct {
using_entries: []const UsingEntry = &.{},
methods: []const *Node = &.{}, // fn_decl nodes for struct methods
constants: []const *Node = &.{}, // const_decl nodes for struct-level constants
/// True when the declared NAME was a backtick raw identifier
/// (`` `s2 :: struct { … } ``) — exempt from the reserved-type-name decl
/// check (issue 0089). A bare reserved-name decl still errors.
is_raw: bool = false,
};
pub const StructFieldInit = struct {
@@ -444,6 +458,12 @@ pub const TypeExpr = struct {
name: []const u8,
is_generic: bool = false,
protocol_constraints: []const []const u8 = &.{}, // e.g. ["Eq", "Hashable"] for $T/Eq/Hashable
/// True when written as a backtick raw identifier in type position
/// (`` `s2 ``). Such a reference is the LITERAL name `s2` used as a type —
/// resolution skips the builtin/reserved classifier and looks up a
/// `` `s2 ``-declared type (struct/enum/union/alias), else "unknown type"
/// (issue 0089). A bare `s2` keeps `is_raw = false` and is the int type.
is_raw: bool = false,
};
/// `$<pack_name>[<index>]` in type position. Resolves to the i-th
@@ -530,6 +550,10 @@ pub const ReturnStmt = struct {
pub const ImportDecl = struct {
path: []const u8,
name: ?[]const u8,
/// True when the namespace NAME was a backtick raw identifier
/// (`` `s2 :: #import "…" ``) — exempt from the reserved-type-name decl
/// check (issue 0089). A flat `#import` (name == null) binds nothing.
is_raw: bool = false,
};
pub const ArrayTypeExpr = struct {
@@ -638,6 +662,9 @@ pub const SpreadExpr = struct {
pub const NamespaceDecl = struct {
name: []const u8,
decls: []const *Node,
/// True when the namespace NAME was a backtick raw identifier — exempt
/// from the reserved-type-name decl check (issue 0089).
is_raw: bool = false,
};
pub const ForeignExpr = struct {
@@ -648,6 +675,9 @@ pub const ForeignExpr = struct {
pub const LibraryDecl = struct {
lib_name: []const u8,
name: []const u8, // sx-side constant name
/// True when the constant NAME was a backtick raw identifier — exempt from
/// the reserved-type-name decl check (issue 0089).
is_raw: bool = false,
};
pub const FrameworkDecl = struct {
@@ -691,6 +721,9 @@ pub const TupleElement = struct {
pub const UfcsAlias = struct {
name: []const u8,
target: []const u8,
/// True when the alias NAME was a backtick raw identifier — exempt from
/// the reserved-type-name decl check (issue 0089).
is_raw: bool = false,
};
pub const CImportDecl = struct {
@@ -700,6 +733,9 @@ pub const CImportDecl = struct {
flags: []const []const u8,
name: ?[]const u8 = null,
bitcode_paths: []const []const u8 = &.{}, // populated during import resolution
/// True when the namespace NAME was a backtick raw identifier — exempt
/// from the reserved-type-name decl check (issue 0089).
is_raw: bool = false,
};
pub const ProtocolMethodDecl = struct {
@@ -720,6 +756,9 @@ pub const ProtocolDecl = struct {
methods: []const ProtocolMethodDecl,
is_inline: bool = false, // #inline — embedded fn ptrs instead of vtable pointer
type_params: []const StructTypeParam = &.{}, // for `protocol(Target: Type) { ... }`
/// True when the declared NAME was a backtick raw identifier — exempt from
/// the reserved-type-name decl check (issue 0089).
is_raw: bool = false,
};
pub const ForeignRuntime = enum {
@@ -776,6 +815,9 @@ pub const ForeignClassDecl = struct {
members: []const ForeignClassMember = &.{},
is_foreign: bool = false, // `#foreign #...` prefix — class is provided by the foreign runtime; we only reference it
is_main: bool = false, // `#jni_main` / `#objc_main` — class is the launchable entry (Activity / UIApplicationDelegate / ...)
/// True when the sx-side alias NAME was a backtick raw identifier — exempt
/// from the reserved-type-name decl check (issue 0089).
is_raw: bool = false,
};
pub const JniEnvBlock = struct {

View File

@@ -354,6 +354,7 @@ pub const ResolvedModule = struct {
name: []const u8,
other: ResolvedModule,
span: ast.Span,
is_raw: bool,
) !void {
const ns_node = try allocator.create(Node);
ns_node.* = .{
@@ -361,6 +362,10 @@ pub const ResolvedModule = struct {
.data = .{ .namespace_decl = .{
.name = name,
.decls = other.decls,
// Carry the backtick raw escape from the `name :: #import …`
// form so a reserved-name namespace is exempt from the decl
// check, symmetric to every other decl site (issue 0089).
.is_raw = is_raw,
} },
};
try self.scope.put(name, {});
@@ -487,6 +492,7 @@ pub fn resolveImports(
.data = .{ .namespace_decl = .{
.name = ns_name,
.decls = try ns_decls.toOwnedSlice(allocator),
.is_raw = ci.is_raw,
} },
};
ns_node.source_file = file_path;
@@ -569,7 +575,7 @@ pub fn resolveImports(
};
if (imp.name) |ns_name| {
try mod.addNamespace(allocator, &decl_list, &own_decl_list, &seen_in_list, ns_name, imported_mod, decl.span);
try mod.addNamespace(allocator, &decl_list, &own_decl_list, &seen_in_list, ns_name, imported_mod, decl.span, imp.is_raw);
} else {
try mod.mergeFlat(allocator, &decl_list, &seen_in_list, &seen_nodes, imported_mod);
}

View File

@@ -11892,8 +11892,8 @@ pub const Lowering = struct {
// type_bridge, which now takes the alias map as an explicit argument
// (the `TypeTable.aliases` borrow is gone, A2.3).
switch (node.data) {
.type_expr => |te| return self.typeResolver().resolveName(te.name),
.identifier => |id| return self.typeResolver().resolveName(id.name),
.type_expr => |te| return self.typeResolver().resolveName(te.name, te.is_raw),
.identifier => |id| return self.typeResolver().resolveName(id.name, id.is_raw),
// A non-spread tuple literal in a type position is a tuple-type
// literal (`(s32, s32)`); validate its elements are types and reject
// non-type elements loudly (issue 0067).

View File

@@ -116,13 +116,16 @@ pub const UnknownTypeChecker = struct {
if (node.source_file) |sf| self.diagnostics.current_source_file = sf;
switch (node.data) {
// ── Binding-introducing nodes: check the name(s), then recurse. ──
// Every site passes the node's own `is_raw` straight to the check —
// never an `if (!is_raw)` call-site guard — so the check and its
// exemption are one operation that cannot be threaded apart (0089).
.var_decl => |vd| {
if (!vd.is_raw) self.checkBindingName(vd.name, vd.name_span);
self.checkBindingName(vd.name, vd.name_span, vd.is_raw);
if (vd.value) |v| self.checkBindingNames(v);
},
.destructure_decl => |dd| {
for (dd.names, dd.name_spans, dd.name_is_raw) |n, sp, raw| {
if (!raw) self.checkBindingName(n, sp);
self.checkBindingName(n, sp, raw);
}
self.checkBindingNames(dd.value);
},
@@ -131,7 +134,7 @@ pub const UnknownTypeChecker = struct {
// `s2 :: (…) {…}` (free fn or struct/impl method) is rejected,
// exactly like `s2 := …`. Backtick (`` `s2 :: … ``) and
// `#import c` foreign fns set `is_raw` and are exempt (0089).
if (!fd.is_raw) self.checkBindingName(fd.name, fd.name_span);
self.checkBindingName(fd.name, fd.name_span, fd.is_raw);
self.checkParamNames(fd.params);
self.checkBindingNames(fd.body);
},
@@ -140,29 +143,23 @@ pub const UnknownTypeChecker = struct {
self.checkBindingNames(lm.body);
},
.param => |p| {
if (!p.is_raw) self.checkBindingName(p.name, p.name_span);
self.checkBindingName(p.name, p.name_span, p.is_raw);
if (p.default_expr) |de| self.checkBindingNames(de);
},
.if_expr => |ie| {
if (ie.binding_name) |bn| {
if (!ie.binding_is_raw) self.checkBindingName(bn, ie.binding_span);
}
if (ie.binding_name) |bn| self.checkBindingName(bn, ie.binding_span, ie.binding_is_raw);
self.checkBindingNames(ie.condition);
self.checkBindingNames(ie.then_branch);
if (ie.else_branch) |e| self.checkBindingNames(e);
},
.while_expr => |we| {
if (we.binding_name) |bn| {
if (!we.binding_is_raw) self.checkBindingName(bn, we.binding_span);
}
if (we.binding_name) |bn| self.checkBindingName(bn, we.binding_span, we.binding_is_raw);
self.checkBindingNames(we.condition);
self.checkBindingNames(we.body);
},
.for_expr => |fe| {
if (fe.capture_name.len != 0 and !fe.capture_is_raw) self.checkBindingName(fe.capture_name, fe.capture_span);
if (fe.index_name) |idx| {
if (!fe.index_is_raw) self.checkBindingName(idx, fe.index_span);
}
if (fe.capture_name.len != 0) self.checkBindingName(fe.capture_name, fe.capture_span, fe.capture_is_raw);
if (fe.index_name) |idx| self.checkBindingName(idx, fe.index_span, fe.index_is_raw);
self.checkBindingNames(fe.iterable);
if (fe.range_end) |re| self.checkBindingNames(re);
self.checkBindingNames(fe.body);
@@ -170,31 +167,23 @@ pub const UnknownTypeChecker = struct {
.match_expr => |me| {
self.checkBindingNames(me.subject);
for (me.arms) |arm| {
if (arm.capture) |cap| {
if (!arm.capture_is_raw) self.checkBindingName(cap, arm.capture_span);
}
if (arm.capture) |cap| self.checkBindingName(cap, arm.capture_span, arm.capture_is_raw);
if (arm.pattern) |p| self.checkBindingNames(p);
self.checkBindingNames(arm.body);
}
},
.match_arm => |arm| {
if (arm.capture) |cap| {
if (!arm.capture_is_raw) self.checkBindingName(cap, arm.capture_span);
}
if (arm.capture) |cap| self.checkBindingName(cap, arm.capture_span, arm.capture_is_raw);
if (arm.pattern) |p| self.checkBindingNames(p);
self.checkBindingNames(arm.body);
},
.catch_expr => |ce| {
if (ce.binding) |b| {
if (!ce.binding_is_raw) self.checkBindingName(b, ce.binding_span);
}
if (ce.binding) |b| self.checkBindingName(b, ce.binding_span, ce.binding_is_raw);
self.checkBindingNames(ce.operand);
self.checkBindingNames(ce.body);
},
.onfail_stmt => |os| {
if (os.binding) |b| {
if (!os.binding_is_raw) self.checkBindingName(b, os.binding_span);
}
if (os.binding) |b| self.checkBindingName(b, os.binding_span, os.binding_is_raw);
self.checkBindingNames(os.body);
},
// impl / protocol-default / foreign-class method bodies: each
@@ -203,12 +192,12 @@ pub const UnknownTypeChecker = struct {
// param/local names mis-lower the same as any other.
.impl_block => |ib| for (ib.methods) |m| self.checkBindingNames(m),
.protocol_decl => |pd| {
self.checkDeclName(node, pd.name);
self.checkDeclName(node, pd.name, pd.is_raw);
for (pd.methods) |m| {
if (m.default_body) |body| {
for (m.param_names, m.param_name_spans, 0..) |pn, sp, i| {
if (i < m.param_name_is_raw.len and m.param_name_is_raw[i]) continue;
self.checkBindingName(pn, sp);
const raw = i < m.param_name_is_raw.len and m.param_name_is_raw[i];
self.checkBindingName(pn, sp, raw);
}
self.checkBindingNames(body);
}
@@ -217,12 +206,12 @@ pub const UnknownTypeChecker = struct {
.foreign_class_decl => |fcd| {
// The sx-side alias (left of `::`) is a user-chosen name, so a
// reserved spelling is rejected like any other type decl (0089).
self.checkDeclName(node, fcd.name);
self.checkDeclName(node, fcd.name, fcd.is_raw);
for (fcd.members) |member| switch (member) {
.method => |m| if (m.body) |body| {
for (m.param_names, m.param_name_spans, 0..) |pn, sp, i| {
if (i < m.param_name_is_raw.len and m.param_name_is_raw[i]) continue;
self.checkBindingName(pn, sp);
const raw = i < m.param_name_is_raw.len and m.param_name_is_raw[i];
self.checkBindingName(pn, sp, raw);
}
self.checkBindingNames(body);
},
@@ -235,7 +224,7 @@ pub const UnknownTypeChecker = struct {
// module decls held inline; descend so an imported module's
// reserved-name binding is rejected too (issue 0077).
.namespace_decl => |nd| {
self.checkDeclName(node, nd.name);
self.checkDeclName(node, nd.name, nd.is_raw);
for (nd.decls) |d| self.checkBindingNames(d);
},
.const_decl => |cd| {
@@ -247,12 +236,12 @@ pub const UnknownTypeChecker = struct {
// own name on recursion — don't double-check it here (0089).
switch (cd.value.data) {
.builtin_expr, .struct_decl, .enum_decl, .union_decl, .error_set_decl, .fn_decl => {},
else => if (!cd.is_raw) self.checkBindingName(cd.name, cd.name_span),
else => self.checkBindingName(cd.name, cd.name_span, cd.is_raw),
}
self.checkBindingNames(cd.value);
},
.struct_decl => |sd| {
self.checkDeclName(node, sd.name);
self.checkDeclName(node, sd.name, sd.is_raw);
for (sd.methods) |m| self.checkBindingNames(m);
for (sd.constants) |c| self.checkBindingNames(c);
for (sd.field_defaults) |fdef| if (fdef) |d| self.checkBindingNames(d);
@@ -319,13 +308,13 @@ pub const UnknownTypeChecker = struct {
// spelling as the declared name is rejected (issue 0089). These
// have no nested binding sites, so only the name is checked. A
// flat `#import`/`#import c` (name == null) binds nothing. ──
.enum_decl => |ed| self.checkDeclName(node, ed.name),
.union_decl => |ud| self.checkDeclName(node, ud.name),
.error_set_decl => |esd| self.checkDeclName(node, esd.name),
.ufcs_alias => |ua| self.checkDeclName(node, ua.name),
.library_decl => |ld| self.checkDeclName(node, ld.name),
.import_decl => |imp| if (imp.name) |n| self.checkDeclName(node, n),
.c_import_decl => |cid| if (cid.name) |n| self.checkDeclName(node, n),
.enum_decl => |ed| self.checkDeclName(node, ed.name, ed.is_raw),
.union_decl => |ud| self.checkDeclName(node, ud.name, ud.is_raw),
.error_set_decl => |esd| self.checkDeclName(node, esd.name, esd.is_raw),
.ufcs_alias => |ua| self.checkDeclName(node, ua.name, ua.is_raw),
.library_decl => |ld| self.checkDeclName(node, ld.name, ld.is_raw),
.import_decl => |imp| if (imp.name) |n| self.checkDeclName(node, n, imp.is_raw),
.c_import_decl => |cid| if (cid.name) |n| self.checkDeclName(node, n, cid.is_raw),
// ── Leaves & pure type-expression nodes: no binding sites below. ──
// Type-expression subtrees carry only type names (no value
// bindings). Listing each tag explicitly (rather than an `else`) is
@@ -370,8 +359,9 @@ pub const UnknownTypeChecker = struct {
fn checkParamNames(self: UnknownTypeChecker, params: []const ast.Param) void {
for (params) |p| {
// 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);
// param is exempt from the reserved-type-name rule (issue 0089)
// the exemption is honored inside `checkBindingName` via `p.is_raw`.
self.checkBindingName(p.name, p.name_span, p.is_raw);
if (p.default_expr) |de| self.checkBindingNames(de);
}
}
@@ -708,8 +698,8 @@ pub const UnknownTypeChecker = struct {
switch (node.data) {
// A `$`-prefixed name (`-> $R`) introduces/references a generic type
// param inline — always valid in a type position.
.type_expr => |te| if (!te.is_generic) self.reportIfUnknownType(te.name, node.span, declared, in_scope, type_vals),
.identifier => |id| self.reportIfUnknownType(id.name, node.span, declared, in_scope, type_vals),
.type_expr => |te| if (!te.is_generic) self.reportIfUnknownType(te.name, node.span, declared, in_scope, type_vals, te.is_raw),
.identifier => |id| self.reportIfUnknownType(id.name, node.span, declared, in_scope, type_vals, id.is_raw),
.pointer_type_expr => |pt| self.checkTypeNodeForUnknown(pt.pointee_type, declared, in_scope, type_vals),
.many_pointer_type_expr => |mp| self.checkTypeNodeForUnknown(mp.element_type, declared, in_scope, type_vals),
.slice_type_expr => |st| self.checkTypeNodeForUnknown(st.element_type, declared, in_scope, type_vals),
@@ -753,11 +743,17 @@ pub const UnknownTypeChecker = struct {
declared: *std.StringHashMap(void),
in_scope: []const ast.StructTypeParam,
type_vals: []const []const u8,
is_raw: bool,
) void {
// Only bare identifiers are validated. Inline-spelled compound types
// (`[:0]u8`, `mod.Type`, …) carry non-identifier characters — trust them.
if (!isIdentLike(name)) return;
if (isBuiltinTypeName(name)) return;
// A backtick raw reference (`` `s2 ``) is the LITERAL name used as a
// type — explicitly NOT the builtin/reserved spelling — so it must
// resolve to a `` `s2 ``-declared type, else a normal "unknown type"
// error. Skip the builtin-name exemption that would otherwise wave a
// bare `s2` through (issue 0089).
if (!is_raw and isBuiltinTypeName(name)) return;
for (in_scope) |tp| if (std.mem.eql(u8, tp.name, name)) return;
if (declared.contains(name)) return;
// Registered as a real (non-stub) type — covers imported concrete
@@ -789,7 +785,14 @@ pub const UnknownTypeChecker = struct {
/// (LLVM verifier abort, or a silent mutation-losing copy). Rejecting the
/// name here, before lowering, keeps the `.identifier`-only address-of paths
/// correct without any lowering special-case.
fn checkBindingName(self: UnknownTypeChecker, name: []const u8, span: ?ast.Span) void {
/// `is_raw` is a REQUIRED argument, not a call-site guard: the exemption
/// lives INSIDE the check so no caller can validate a name without also
/// honoring the backtick / `#import c` foreign exemption. This is what keeps
/// the check and the exemption from desyncing — the recurring failure of the
/// earlier attempts, where each site threaded an `if (!is_raw)` guard
/// separately and one was forgotten (issue 0089).
fn checkBindingName(self: UnknownTypeChecker, name: []const u8, span: ?ast.Span, is_raw: bool) void {
if (is_raw) return;
if (isReservedTypeName(name))
self.diagnostics.addFmt(.err, span, "'{s}' is a reserved type name and cannot be used as an identifier", .{name});
}
@@ -798,12 +801,14 @@ pub const UnknownTypeChecker = struct {
/// identifier but carries no dedicated `name_span` field — struct / enum /
/// union / error-set / protocol / foreign-class type decls, ufcs aliases,
/// and namespaced imports (issue 0089). Each such node begins at its name
/// token, so the name's length isolates the caret onto the name. A
/// backtick raw / `#import c` foreign name never reaches here (those forms
/// are exempt at their own decl path).
fn checkDeclName(self: UnknownTypeChecker, node: *const Node, name: []const u8) void {
/// token (`createNode(name_start, …)`), so the name's length isolates the
/// caret onto the name — a single source for the span, no separate stored
/// field to drift from `node.span`. `is_raw` is REQUIRED, exactly as in
/// `checkBindingName`: a backtick raw / `#import c` foreign name is exempt
/// by construction.
fn checkDeclName(self: UnknownTypeChecker, node: *const Node, name: []const u8, is_raw: bool) void {
const span = ast.Span{ .start = node.span.start, .end = node.span.start + @as(u32, @intCast(name.len)) };
self.checkBindingName(name, span);
self.checkBindingName(name, span, is_raw);
}
};

View File

@@ -107,8 +107,8 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap
const n = node orelse return .unresolved;
const si = StatelessInner{ .table = table, .alias_map = alias_map, .consts = consts };
return switch (n.data) {
.type_expr => |te| resolveTypeName(te.name, table, alias_map),
.identifier => |id| resolveTypeName(id.name, table, alias_map),
.type_expr => |te| resolveTypeName(te.name, table, alias_map, te.is_raw),
.identifier => |id| resolveTypeName(id.name, table, alias_map, id.is_raw),
// Structural shapes (`*T`/`[*]T`/`[]T`/`?T`/`[N]T`, functions, plain
// closures, plain tuples) are owned by the single canonical
// `TypeResolver.resolveCompound` — no independent compound algorithm
@@ -174,8 +174,9 @@ pub fn resolveAstType(node: ?*const Node, table: *TypeTable, alias_map: AliasMap
/// Resolve a bare type name. The algorithm lives in `type_resolver.zig`
/// (`TypeResolver.resolveNamed`, the single source); `type_bridge` forwards the
/// caller-threaded `alias_map` (the single-source `ProgramIndex.type_alias_map`).
fn resolveTypeName(name: []const u8, table: *TypeTable, alias_map: AliasMap) TypeId {
return type_resolver.TypeResolver.resolveNamed(name, table, alias_map);
/// `skip_builtin` carries the backtick raw escape (issue 0089).
fn resolveTypeName(name: []const u8, table: *TypeTable, alias_map: AliasMap, skip_builtin: bool) TypeId {
return type_resolver.TypeResolver.resolveNamed(name, table, alias_map, skip_builtin);
}
/// Builtin primitive keyword → TypeId. The keyword table now lives in
@@ -535,7 +536,7 @@ fn resolveInlineErrorSet(esd: *const ast.ErrorSetDecl, table: *TypeTable) TypeId
/// resolves to the same empty inferred set, which is correct while no
/// function raises (E1.3+).
fn resolveErrorType(ete: *const ast.ErrorTypeExpr, table: *TypeTable, alias_map: AliasMap) TypeId {
if (ete.name) |name| return resolveTypeName(name, table, alias_map);
if (ete.name) |name| return resolveTypeName(name, table, alias_map, false);
// `!` is not a legal type/identifier name, so this reserved StringId can
// never collide with a user-declared set.
const name_id = table.internString("!");

View File

@@ -144,21 +144,33 @@ test "TypeResolver.resolveName resolves aliases via ProgramIndex (not the TypeTa
try index.type_alias_map.put("NodeRef", ptr_s64); // alias → pointer
const tr = TypeResolver{ .alloc = alloc, .types = &table, .diagnostics = null, .index = &index };
try std.testing.expectEqual(@as(TypeId, .u32), tr.resolveName("ShaderHandle"));
try std.testing.expectEqual(ptr_s64, tr.resolveName("NodeRef"));
try std.testing.expectEqual(@as(TypeId, .u32), tr.resolveName("ShaderHandle", false));
try std.testing.expectEqual(ptr_s64, tr.resolveName("NodeRef", false));
// Primitive is checked before alias.
try std.testing.expectEqual(@as(TypeId, .s64), tr.resolveName("s64"));
try std.testing.expectEqual(@as(TypeId, .s64), tr.resolveName("s64", false));
}
test "TypeResolver.resolveNamed: width-int, string-prefix, unknown→stub" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
defer table.deinit();
try std.testing.expectEqual(table.intern(.{ .signed = 7 }), TypeResolver.resolveNamed("s7", &table, null));
try std.testing.expectEqual(table.ptrTo(.s64), TypeResolver.resolveNamed("*s64", &table, null));
try std.testing.expectEqual(table.intern(.{ .signed = 7 }), TypeResolver.resolveNamed("s7", &table, null, false));
try std.testing.expectEqual(table.ptrTo(.s64), TypeResolver.resolveNamed("*s64", &table, null, false));
// Unknown name, no alias map → empty-struct stub (preserved behavior;
// never `.unresolved`, which is reserved for failed *generic* resolution).
try std.testing.expect(TypeResolver.resolveNamed("Unknown", &table, null) != .unresolved);
try std.testing.expect(TypeResolver.resolveNamed("Unknown", &table, null, false) != .unresolved);
}
test "TypeResolver.resolveNamed: skip_builtin resolves a raw reserved-name type, not the builtin" {
const alloc = std.testing.allocator;
var table = TypeTable.init(alloc);
defer table.deinit();
// A registered user type named "s2" (a reserved int spelling).
const name_id = table.internString("s2");
const user_s2 = table.intern(.{ .@"struct" = .{ .name = name_id, .fields = &.{} } });
// Bare lookup → the builtin 2-bit signed int; raw lookup → the user type.
try std.testing.expectEqual(table.intern(.{ .signed = 2 }), TypeResolver.resolveNamed("s2", &table, null, false));
try std.testing.expectEqual(user_s2, TypeResolver.resolveNamed("s2", &table, null, true));
}
test "TypeResolver.parseWidthInt: every width 1..64, both signs; rejects out-of-range / non-int" {

View File

@@ -244,11 +244,21 @@ pub const TypeResolver = struct {
/// `type_bridge` via the alias map threaded through `resolveAstType`. The
/// stub fall-through preserves long-standing behavior for as-yet-
/// unregistered names.
pub fn resolveNamed(name: []const u8, table: *TypeTable, alias_map: ?*const std.StringHashMap(TypeId)) TypeId {
///
/// `skip_builtin` is the backtick raw-identifier escape (`` `s2 `` in type
/// position, issue 0089): a raw reference is the LITERAL name used as a
/// type, so it bypasses the builtin/reserved classifier and resolves only
/// through registered-type → alias → stub. A bare `s2` keeps the default
/// (`false`) and resolves to the builtin int type. The string-prefix
/// recursion always passes `false`: the inner names (`*T`/`?T`) are bare,
/// never raw.
pub fn resolveNamed(name: []const u8, table: *TypeTable, alias_map: ?*const std.StringHashMap(TypeId), skip_builtin: bool) TypeId {
// Builtin primitive keyword or arbitrary-width integer (`s1`-`s64`,
// `u1`-`u64`) — the single builtin classifier, also reused by the
// numeric-limit accessor intercept.
if (resolveBuiltinName(name, table)) |id| return id;
if (!skip_builtin) {
if (resolveBuiltinName(name, table)) |id| return id;
}
// Sentinel-terminated slice: [:0]u8 → string.
if (name.len >= 5 and name[0] == '[' and name[1] == ':') {
if (std.mem.indexOfScalar(u8, name, ']')) |close| {
@@ -259,15 +269,15 @@ pub const TypeResolver = struct {
}
// Many-pointer: [*]T.
if (name.len >= 4 and name[0] == '[' and name[1] == '*' and name[2] == ']') {
return table.manyPtrTo(resolveNamed(name[3..], table, alias_map));
return table.manyPtrTo(resolveNamed(name[3..], table, alias_map, false));
}
// Pointer: *T.
if (name.len >= 2 and name[0] == '*') {
return table.ptrTo(resolveNamed(name[1..], table, alias_map));
return table.ptrTo(resolveNamed(name[1..], table, alias_map, false));
}
// Optional: ?T.
if (name.len >= 2 and name[0] == '?') {
return table.optionalOf(resolveNamed(name[1..], table, alias_map));
return table.optionalOf(resolveNamed(name[1..], table, alias_map, false));
}
// Named struct/enum/union — already-registered wins, then alias, then
// a fresh empty-struct stub for an as-yet-unregistered name.
@@ -280,8 +290,9 @@ pub const TypeResolver = struct {
}
/// Resolve a bare type name through the canonical alias source
/// (`ProgramIndex.type_alias_map`).
pub fn resolveName(self: TypeResolver, name: []const u8) TypeId {
return resolveNamed(name, self.types, &self.index.type_alias_map);
/// (`ProgramIndex.type_alias_map`). `skip_builtin` carries the backtick raw
/// escape (issue 0089) — see `resolveNamed`.
pub fn resolveName(self: TypeResolver, name: []const u8, skip_builtin: bool) TypeId {
return resolveNamed(name, self.types, &self.index.type_alias_map, skip_builtin);
}
};

View File

@@ -88,7 +88,7 @@ pub const Parser = struct {
// Check for #import c { ... } (C import block)
if (self.current.tag == .identifier and std.mem.eql(u8, self.tokenSlice(self.current), "c") and self.peekNext() == .l_brace) {
self.advance(); // consume 'c'
return self.parseCImportBlock(start, null);
return self.parseCImportBlock(start, null, false);
}
if (self.current.tag != .string_literal) {
return self.fail("expected string path after '#import'");
@@ -183,7 +183,7 @@ pub const Parser = struct {
// Check for name :: #import c { ... }
if (self.current.tag == .identifier and std.mem.eql(u8, self.tokenSlice(self.current), "c") and self.peekNext() == .l_brace) {
self.advance(); // consume 'c'
return self.parseCImportBlock(start_pos, name);
return self.parseCImportBlock(start_pos, name, name_is_raw);
}
if (self.current.tag != .string_literal) {
return self.fail("expected string path after '#import'");
@@ -192,7 +192,7 @@ pub const Parser = struct {
const path = raw[1 .. raw.len - 1];
self.advance();
try self.expect(.semicolon);
return try self.createNode(start_pos, .{ .import_decl = .{ .path = path, .name = name } });
return try self.createNode(start_pos, .{ .import_decl = .{ .path = path, .name = name, .is_raw = name_is_raw } });
}
// Named library: name :: #library "libname";
@@ -205,7 +205,7 @@ pub const Parser = struct {
const lib_name = raw[1 .. raw.len - 1];
self.advance();
try self.expect(.semicolon);
return try self.createNode(start_pos, .{ .library_decl = .{ .lib_name = lib_name, .name = name } });
return try self.createNode(start_pos, .{ .library_decl = .{ .lib_name = lib_name, .name = name, .is_raw = name_is_raw } });
}
// Compile-time evaluation: name :: #run expr;
@@ -229,22 +229,22 @@ pub const Parser = struct {
// Enum declaration
if (self.current.tag == .kw_enum) {
return self.parseEnumDecl(name, start_pos);
return self.parseEnumDecl(name, start_pos, name_is_raw);
}
// Error-set declaration: name :: error { TagA, TagB }
if (self.current.tag == .kw_error) {
return self.parseErrorSetDecl(name, start_pos);
return self.parseErrorSetDecl(name, start_pos, name_is_raw);
}
// Struct declaration
if (self.current.tag == .kw_struct) {
return self.parseStructDecl(name, start_pos);
return self.parseStructDecl(name, start_pos, name_is_raw);
}
// Protocol declaration
if (self.current.tag == .kw_protocol) {
return self.parseProtocolDecl(name, start_pos);
return self.parseProtocolDecl(name, start_pos, name_is_raw);
}
// Foreign-type binding with optional prefix modifiers:
@@ -255,12 +255,12 @@ pub const Parser = struct {
// `#foreign` flips that to "reference an existing class on the foreign side."
// `#jni_main` flags the class as the launchable entry (Android Activity).
if (self.tryParseForeignClassPrefix()) |prefix| {
return self.parseForeignClassDecl(name, start_pos, prefix.runtime, prefix.is_foreign, prefix.is_main);
return self.parseForeignClassDecl(name, start_pos, prefix.runtime, prefix.is_foreign, prefix.is_main, name_is_raw);
}
// C-style union declaration
if (self.current.tag == .kw_union) {
return self.parseUnionDecl(name, start_pos);
return self.parseUnionDecl(name, start_pos, name_is_raw);
}
// UFCS alias: name :: ufcs target;
@@ -272,7 +272,7 @@ pub const Parser = struct {
const target = self.tokenSlice(self.current);
self.advance();
try self.expect(.semicolon);
return try self.createNode(start_pos, .{ .ufcs_alias = .{ .name = name, .target = target } });
return try self.createNode(start_pos, .{ .ufcs_alias = .{ .name = name, .target = target, .is_raw = name_is_raw } });
}
// Function declaration: (params) -> type { body } or () { body }
@@ -332,7 +332,7 @@ pub const Parser = struct {
return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = null, .value = value, .name_span = name_span, .is_raw = name_is_raw } });
}
fn parseCImportBlock(self: *Parser, start: u32, name: ?[]const u8) anyerror!*Node {
fn parseCImportBlock(self: *Parser, start: u32, name: ?[]const u8, name_is_raw: bool) anyerror!*Node {
try self.expect(.l_brace);
var includes = std.ArrayList([]const u8).empty;
var sources = std.ArrayList([]const u8).empty;
@@ -381,6 +381,7 @@ pub const Parser = struct {
.defines = try defines.toOwnedSlice(self.allocator),
.flags = try flags.toOwnedSlice(self.allocator),
.name = name,
.is_raw = name_is_raw,
} });
}
@@ -394,7 +395,7 @@ pub const Parser = struct {
self.advance();
const value = try self.parseExpr();
try self.expectSemicolonAfter(value);
return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = type_node, .value = value } });
return try self.createNode(start_pos, .{ .const_decl = .{ .name = name, .type_annotation = type_node, .value = value, .name_span = name_span, .is_raw = name_is_raw } });
}
if (self.current.tag == .equal) {
@@ -629,11 +630,16 @@ pub const Parser = struct {
}
if (self.current.tag.isTypeKeyword() or self.isIdentLike()) {
// A backtick raw identifier (`` `s2 ``) is a VALUE-name escape; it is
// never a type. Reject it in type position rather than silently
// type-classifying it (issue 0089).
// A backtick raw identifier (`` `s2 ``) in type position is the
// LITERAL name `s2` used as a type reference — never the builtin /
// reserved keyword. It is always a plain named-type reference (no
// qualified-path, `Closure`, or parameterized continuation), so emit
// a raw `type_expr` and return; resolution skips the builtin
// classifier and looks up a `` `s2 ``-declared type (issue 0089).
if (self.current.is_raw) {
return self.failFmt("`{s}` is a raw identifier, not a type — the backtick escape names a value, never a type", .{self.tokenSlice(self.current)});
const raw_name = self.tokenSlice(self.current);
self.advance();
return try self.createNode(start, .{ .type_expr = .{ .name = raw_name, .is_raw = true } });
}
var name = self.tokenSlice(self.current);
self.advance();
@@ -787,20 +793,20 @@ pub const Parser = struct {
}
// Inline struct type in type position: struct { ... }
if (self.current.tag == .kw_struct) {
return try self.parseStructDecl("__anon", start);
return try self.parseStructDecl("__anon", start, false);
}
// Inline C-style union in type position: union { ... }
if (self.current.tag == .kw_union) {
return try self.parseUnionDecl("__anon", start);
return try self.parseUnionDecl("__anon", start, false);
}
// Inline enum type in type position: enum { ... }
if (self.current.tag == .kw_enum) {
return try self.parseEnumDecl("__anon", start);
return try self.parseEnumDecl("__anon", start, false);
}
return self.fail("expected type name");
}
fn parseEnumDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
fn parseEnumDecl(self: *Parser, name: []const u8, start_pos: u32, name_is_raw: bool) anyerror!*Node {
self.advance(); // skip 'enum'
// Check for 'flags' modifier: enum flags { ... }
@@ -874,10 +880,11 @@ pub const Parser = struct {
.is_flags = is_flags,
.variant_values = if (has_any_value) try variant_values.toOwnedSlice(self.allocator) else &.{},
.backing_type = backing_type,
.is_raw = name_is_raw,
} });
}
fn parseErrorSetDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
fn parseErrorSetDecl(self: *Parser, name: []const u8, start_pos: u32, name_is_raw: bool) anyerror!*Node {
self.advance(); // skip 'error'
try self.expect(.l_brace);
var tag_names = std.ArrayList([]const u8).empty;
@@ -899,10 +906,11 @@ pub const Parser = struct {
return try self.createNode(start_pos, .{ .error_set_decl = .{
.name = name,
.tag_names = try tag_names.toOwnedSlice(self.allocator),
.is_raw = name_is_raw,
} });
}
fn parseUnionDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
fn parseUnionDecl(self: *Parser, name: []const u8, start_pos: u32, name_is_raw: bool) anyerror!*Node {
self.advance(); // skip 'union'
try self.expect(.l_brace);
var field_names = std.ArrayList([]const u8).empty;
@@ -914,7 +922,7 @@ pub const Parser = struct {
const anon_field = try std.fmt.allocPrint(self.allocator, "__anon_{d}", .{anon_idx});
anon_idx += 1;
const anon_struct_name = try std.fmt.allocPrint(self.allocator, "{s}.{s}", .{ name, anon_field });
const struct_node = try self.parseStructDecl(anon_struct_name, self.current.loc.start);
const struct_node = try self.parseStructDecl(anon_struct_name, self.current.loc.start, false);
try field_names.append(self.allocator, anon_field);
try field_types.append(self.allocator, struct_node);
if (self.current.tag == .semicolon) {
@@ -942,10 +950,11 @@ pub const Parser = struct {
.name = name,
.field_names = try field_names.toOwnedSlice(self.allocator),
.field_types = try field_types.toOwnedSlice(self.allocator),
.is_raw = name_is_raw,
} });
}
fn parseStructDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
fn parseStructDecl(self: *Parser, name: []const u8, start_pos: u32, name_is_raw: bool) anyerror!*Node {
self.advance(); // skip 'struct'
// Optional `#compiler` attribute: all methods inside this struct are
@@ -1133,10 +1142,11 @@ pub const Parser = struct {
.using_entries = try using_entries.toOwnedSlice(self.allocator),
.methods = try methods.toOwnedSlice(self.allocator),
.constants = try constants.toOwnedSlice(self.allocator),
.is_raw = name_is_raw,
} });
}
fn parseProtocolDecl(self: *Parser, name: []const u8, start_pos: u32) anyerror!*Node {
fn parseProtocolDecl(self: *Parser, name: []const u8, start_pos: u32, name_is_raw: bool) anyerror!*Node {
self.advance(); // skip 'protocol'
// Optional type params: protocol(Target: Type, U: Type) { ... }
@@ -1249,6 +1259,7 @@ pub const Parser = struct {
.methods = try methods.toOwnedSlice(self.allocator),
.is_inline = is_inline,
.type_params = try type_params.toOwnedSlice(self.allocator),
.is_raw = name_is_raw,
} });
}
@@ -1335,7 +1346,7 @@ pub const Parser = struct {
};
}
fn parseForeignClassDecl(self: *Parser, name: []const u8, start_pos: u32, runtime: ast.ForeignRuntime, is_foreign: bool, is_main: bool) anyerror!*Node {
fn parseForeignClassDecl(self: *Parser, name: []const u8, start_pos: u32, runtime: ast.ForeignRuntime, is_foreign: bool, is_main: bool, name_is_raw: bool) anyerror!*Node {
self.advance(); // skip directive token
try self.expect(.l_paren);
@@ -1576,6 +1587,7 @@ pub const Parser = struct {
.members = try members.toOwnedSlice(self.allocator),
.is_foreign = is_foreign,
.is_main = is_main,
.is_raw = name_is_raw,
} });
}
@@ -2820,15 +2832,15 @@ pub const Parser = struct {
},
.kw_struct => {
// Anonymous struct expression: struct { value: T; count: u32; }
return try self.parseStructDecl("__anon", start);
return try self.parseStructDecl("__anon", start, false);
},
.kw_enum => {
// Anonymous enum expression: enum { variant: T; other: u32; }
return try self.parseEnumDecl("__anon", start);
return try self.parseEnumDecl("__anon", start, false);
},
.kw_union => {
// Anonymous C-style union expression: union { f: f32; i: s32; }
return try self.parseUnionDecl("__anon", start);
return try self.parseUnionDecl("__anon", start, false);
},
.kw_if => {
return self.parseIfExpr();