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

@@ -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);
}
};