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

86
src/sema.test.zig Normal file
View File

@@ -0,0 +1,86 @@
// Tests for sema.zig — the editor/LSP type classifier (the SECOND resolver,
// distinct from the codegen-side `ir/type_resolver.zig`). These pin behavior
// the example suite can't reach: the example runner exercises the codegen
// path (`sx run`), never sema's hover/completion/index resolution.
const std = @import("std");
const ast = @import("ast.zig");
const Node = ast.Node;
const Parser = @import("parser.zig").Parser;
const sema = @import("sema.zig");
const types = @import("types.zig");
const Type = types.Type;
// issue 0089 — the backtick raw escape must hold in BOTH classifiers. A raw
// reserved-name type reference (`` `s2 ``) resolves to the user-declared type,
// while a BARE `s2` stays the builtin int. Before the fix sema's
// `resolveTypeNode` ran `Type.fromName` first and ignored `is_raw`, so the
// editor index would show the builtin for backtick code (the issue-0083
// two-resolver divergence applied to raw types).
test "sema: backtick raw type reference resolves to the user type; bare stays builtin" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const src =
\\`s2 :: struct { x: s64; }
\\
;
var parser = Parser.init(alloc, src);
const root = try parser.parse();
var analyzer = sema.Analyzer.init(alloc);
_ = try analyzer.analyze(root);
// The reserved-spelled user type registered under its plain name.
try std.testing.expect(analyzer.struct_types.contains("s2"));
// RAW reference (`` `s2 ``) → the user struct, NOT the 2-bit signed int.
var raw_node = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s2", .is_raw = true } } };
const raw_ty = analyzer.resolveTypeNode(&raw_node);
try std.testing.expect(raw_ty == .struct_type);
try std.testing.expectEqualStrings("s2", raw_ty.struct_type);
// BARE `s2` → the builtin 2-bit signed int.
var bare_node = Node{ .span = .{ .start = 0, .end = 0 }, .data = .{ .type_expr = .{ .name = "s2", .is_raw = false } } };
const bare_ty = analyzer.resolveTypeNode(&bare_node);
try std.testing.expect(bare_ty == .signed);
try std.testing.expectEqual(@as(u8, 2), bare_ty.signed);
}
// The same divergence guard for the string-keyed entry (`resolveTypeNameStr`,
// reached via `fieldType` when registering struct field types): a raw field
// annotation (`` `u8 ``) resolves to the user struct, a bare one (`u8`) to the
// builtin. Driven through the real analyze pipeline (no private access).
test "sema: a raw struct-field annotation resolves to the user type; bare stays builtin" {
var arena = std.heap.ArenaAllocator.init(std.testing.allocator);
defer arena.deinit();
const alloc = arena.allocator();
const src =
\\`u8 :: struct { y: s64; }
\\Holder :: struct { a: `u8; b: u8; }
\\
;
var parser = Parser.init(alloc, src);
const root = try parser.parse();
var analyzer = sema.Analyzer.init(alloc);
_ = try analyzer.analyze(root);
const holder = analyzer.struct_types.get("Holder").?;
var a_ty: ?Type = null;
var b_ty: ?Type = null;
for (holder.field_names, holder.field_types) |fname, fty| {
if (std.mem.eql(u8, fname, "a")) a_ty = fty;
if (std.mem.eql(u8, fname, "b")) b_ty = fty;
}
// field `a : `u8` → the user struct named "u8".
try std.testing.expect(a_ty.? == .struct_type);
try std.testing.expectEqualStrings("u8", a_ty.?.struct_type);
// field `b : u8` → the builtin unsigned 8-bit int.
try std.testing.expect(b_ty.? == .unsigned);
try std.testing.expectEqual(@as(u8, 8), b_ty.?.unsigned);
}