feat(lang): universal raw identifier — parser exhaustiveness + raw type continuations + sema/LSP [F0.6]

Closes the remaining three F0.6 findings so the universal backtick raw
identifier holds in BOTH classifiers and at EVERY parser construction site.

1. Struct-body constants thread is_raw + name_span. The struct-body const
   forms (untyped `` `s2 :: 5 `` and typed `` `s2 : T : v ``) built the
   const_decl node without name_span/is_raw, so a backtick const was falsely
   rejected and a bare reserved-name const caretted at 1:1. They now capture
   both. Structural cure: `ast.ConstDecl`'s name_span + is_raw carry NO
   default, so the compiler rejects any construction site that omits them
   (mirrors checkBindingName's required `is_raw` arg). FnDecl keeps its
   defaults — every parser fn_decl routes through parseFnDecl whose
   `name_is_raw` is a required parameter (equivalent guarantee).

2. Raw identifier in TYPE position flows through the normal continuations.
   parseTypeExpr no longer returns a terminal type_expr for a raw atom; the
   raw flag rides the atom through the qualified-path / Closure / parameterized
   continuations, so `` `s2(s64) ``, `` *`s2 ``, `` ?`s2 `` all parse.
   ParameterizedTypeExpr carries is_raw; resolveParameterizedWithBindings
   skips the `Vector` intrinsic when raw.

3. sema/LSP (the second classifier) honors is_raw. Type.fromTypeExpr returns
   null for a raw type_expr; resolveTypeNode skips the builtin classifier when
   raw; resolveTypeNameStr takes a skip_builtin arg threaded from te/id.is_raw
   (compound inner names pass false). A backtick reserved-name annotation now
   resolves to the user type in the editor index, not the builtin.

Tests: examples/0156 (struct-body const), 0157 (parameterized raw type +
wrappers), 1142 (bare struct-body const errors, caret on name); src/sema.test.zig
pins the LSP raw-type resolution (fail-before verified). Gate: 365 unit tests,
429 examples, 0 failed.
This commit is contained in:
agra
2026-06-04 21:14:35 +03:00
parent 023971cae5
commit ef8f021c01
22 changed files with 300 additions and 53 deletions

View File

@@ -632,15 +632,13 @@ pub const Parser = struct {
if (self.current.tag.isTypeKeyword() or self.isIdentLike()) {
// 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
// reserved keyword. The raw flag rides the type ATOM through the
// SAME qualified-path / `Closure` / parameterized continuations as a
// bare name (so `` `s2(s64) ``, `` `s2.Inner ``, `` *`s2 `` all
// parse); it is threaded onto the final `type_expr` /
// `parameterized_type_expr` so resolution skips the builtin
// classifier and looks up a `` `s2 ``-declared type (issue 0089).
if (self.current.is_raw) {
const raw_name = self.tokenSlice(self.current);
self.advance();
return try self.createNode(start, .{ .type_expr = .{ .name = raw_name, .is_raw = true } });
}
const atom_is_raw = self.current.is_raw;
var name = self.tokenSlice(self.current);
self.advance();
@@ -781,6 +779,7 @@ pub const Parser = struct {
return try self.createNode(start, .{ .parameterized_type_expr = .{
.name = name,
.args = try args.toOwnedSlice(self.allocator),
.is_raw = atom_is_raw,
} });
}
@@ -789,7 +788,7 @@ pub const Parser = struct {
for (self.struct_type_params) |tp| {
if (std.mem.eql(u8, tp, name)) { is_struct_generic = true; break; }
}
return try self.createNode(start, .{ .type_expr = .{ .name = name, .is_generic = is_struct_generic } });
return try self.createNode(start, .{ .type_expr = .{ .name = name, .is_generic = is_struct_generic, .is_raw = atom_is_raw } });
}
// Inline struct type in type position: struct { ... }
if (self.current.tag == .kw_struct) {
@@ -1067,6 +1066,8 @@ pub const Parser = struct {
.name = method_name,
.type_annotation = null,
.value = value,
.name_span = method_name_span,
.is_raw = method_is_raw,
} }));
}
continue;
@@ -1080,6 +1081,13 @@ pub const Parser = struct {
return self.fail("expected field name in struct");
}
const field_start = self.current.loc.start;
// Captured for the single-name typed-const path (`name :Type: value`)
// below: a struct-body const binds a name like any other decl, so
// its name_span + raw flag must travel to the `const_decl` node
// (finding 1 — they were being dropped to a 1:1 caret / false
// reserved-name reject).
const field_name_span = ast.Span{ .start = self.current.loc.start, .end = self.current.loc.end };
const field_is_raw = self.current.is_raw;
try group_names.append(self.allocator, self.tokenSlice(self.current));
self.advance();
@@ -1104,6 +1112,8 @@ pub const Parser = struct {
.name = group_names.items[0],
.type_annotation = field_type,
.value = value,
.name_span = field_name_span,
.is_raw = field_is_raw,
} }));
continue;
}