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

@@ -411,10 +411,15 @@ pub const Analyzer = struct {
if (tn.data == .parameterized_type_expr) {
return .void_type;
}
// type_expr or identifier — check aliases, enums, structs
// type_expr or identifier — check aliases, enums, structs. A raw
// reference (`` `s2 ``) skips the builtin classifier and resolves
// through user-defined types only (issue 0089).
if (tn.data == .type_expr or tn.data == .identifier) {
const name = if (tn.data == .type_expr) tn.data.type_expr.name else tn.data.identifier.name;
if (Type.fromName(name)) |t| return t;
const is_raw = if (tn.data == .type_expr) tn.data.type_expr.is_raw else tn.data.identifier.is_raw;
if (!is_raw) {
if (Type.fromName(name)) |t| return t;
}
if (self.type_aliases.get(name)) |target| {
if (Type.fromName(target)) |t| return t;
if (self.struct_types.contains(target)) return .{ .struct_type = target };
@@ -430,9 +435,16 @@ pub const Analyzer = struct {
/// Resolve a bare type-name string against the registry (aliases, enums,
/// structs), falling back to primitive spellings. Unlike `Type.fromName`,
/// this knows user-defined types; returns `unresolved` when it can't place
/// the name.
fn resolveTypeNameStr(self: *Analyzer, name: []const u8) Type {
if (Type.fromName(name)) |t| return t;
/// the name. `skip_builtin` is the backtick raw escape (issue 0089) — a raw
/// reference (`` `s2 ``) bypasses the builtin/reserved classifier and
/// resolves only through user-defined types, mirroring the codegen-side
/// `TypeResolver.resolveNamed`. Inner names of compound shapes
/// (pointer/slice element/pointee) are always bare, so their callers pass
/// `false`.
fn resolveTypeNameStr(self: *Analyzer, name: []const u8, skip_builtin: bool) Type {
if (!skip_builtin) {
if (Type.fromName(name)) |t| return t;
}
if (self.type_aliases.get(name)) |target| {
if (Type.fromName(target)) |t| return t;
if (self.struct_types.contains(target)) return .{ .struct_type = target };
@@ -460,8 +472,8 @@ pub const Analyzer = struct {
/// registry; the element name is resolved lazily at index/field time.
fn fieldType(self: *Analyzer, node: *Node) Type {
return switch (node.data) {
.type_expr => |te| self.resolveTypeNameStr(te.name),
.identifier => |id| self.resolveTypeNameStr(id.name),
.type_expr => |te| self.resolveTypeNameStr(te.name, te.is_raw),
.identifier => |id| self.resolveTypeNameStr(id.name, id.is_raw),
.many_pointer_type_expr => |mp| .{ .many_pointer_type = .{ .element_name = self.typeExprName(mp.element_type) } },
.pointer_type_expr => |p| .{ .pointer_type = .{ .pointee_name = self.typeExprName(p.pointee_type) } },
.slice_type_expr => |s| .{ .slice_type = .{ .element_name = self.typeExprName(s.element_type) } },
@@ -476,15 +488,15 @@ pub const Analyzer = struct {
/// pointee first (so `*List(Move)` still iterates `Move`).
fn elementTypeOf(self: *Analyzer, ty: Type) ?Type {
return switch (ty) {
.array_type => |i| self.resolveTypeNameStr(i.element_name),
.slice_type => |i| self.resolveTypeNameStr(i.element_name),
.many_pointer_type => |i| self.resolveTypeNameStr(i.element_name),
.pointer_type => |i| self.elementTypeOf(self.resolveTypeNameStr(i.pointee_name)),
.array_type => |i| self.resolveTypeNameStr(i.element_name, false),
.slice_type => |i| self.resolveTypeNameStr(i.element_name, false),
.many_pointer_type => |i| self.resolveTypeNameStr(i.element_name, false),
.pointer_type => |i| self.elementTypeOf(self.resolveTypeNameStr(i.pointee_name, false)),
.struct_type => |name| blk: {
const info = self.struct_types.get(name) orelse break :blk null;
for (info.field_names, info.field_types) |fname, fty| {
if (std.mem.eql(u8, fname, "items") and fty == .many_pointer_type) {
break :blk self.resolveTypeNameStr(fty.many_pointer_type.element_name);
break :blk self.resolveTypeNameStr(fty.many_pointer_type.element_name, false);
}
}
break :blk null;
@@ -642,7 +654,7 @@ pub const Analyzer = struct {
var obj_ty = self.inferExprType(fa.object);
// `p.field` where `p` is `*T` resolves on the pointee `T`.
if (obj_ty.isPointer()) {
obj_ty = self.resolveTypeNameStr(obj_ty.pointer_type.pointee_name);
obj_ty = self.resolveTypeNameStr(obj_ty.pointer_type.pointee_name, false);
}
// `.len` / `.ptr` on the built-in containers (string, slice, array).
if (std.mem.eql(u8, fa.field, "len")) {
@@ -670,9 +682,9 @@ pub const Analyzer = struct {
.index_expr => |ie| {
const obj_ty = self.inferExprType(ie.object);
if (obj_ty == .string_type) return Type.u(8);
if (obj_ty.isArray()) return self.resolveTypeNameStr(obj_ty.array_type.element_name);
if (obj_ty.isManyPointer()) return self.resolveTypeNameStr(obj_ty.many_pointer_type.element_name);
if (obj_ty.isSlice()) return self.resolveTypeNameStr(obj_ty.slice_type.element_name);
if (obj_ty.isArray()) return self.resolveTypeNameStr(obj_ty.array_type.element_name, false);
if (obj_ty.isManyPointer()) return self.resolveTypeNameStr(obj_ty.many_pointer_type.element_name, false);
if (obj_ty.isSlice()) return self.resolveTypeNameStr(obj_ty.slice_type.element_name, false);
return Type.unresolved;
},
.slice_expr => |se| {
@@ -1054,7 +1066,7 @@ pub const Analyzer = struct {
.field_access => |fa| {
try self.analyzeNode(fa.object);
var owner_ty = self.inferExprType(fa.object);
if (owner_ty.isPointer()) owner_ty = self.resolveTypeNameStr(owner_ty.pointer_type.pointee_name);
if (owner_ty.isPointer()) owner_ty = self.resolveTypeNameStr(owner_ty.pointer_type.pointee_name, false);
self.recordMemberRef(fa.field, owner_ty.toName() orelse "", false);
},
.enum_literal => |el| {
@@ -1083,7 +1095,7 @@ pub const Analyzer = struct {
.match_expr => |me| {
try self.analyzeNode(me.subject);
var subj_ty = self.inferExprType(me.subject);
if (subj_ty.isPointer()) subj_ty = self.resolveTypeNameStr(subj_ty.pointer_type.pointee_name);
if (subj_ty.isPointer()) subj_ty = self.resolveTypeNameStr(subj_ty.pointer_type.pointee_name, false);
const subj_owner = subj_ty.toName() orelse "";
for (me.arms) |arm| {
if (arm.pattern) |pat| {