Files
sx/src/types.zig
agra d515696e61 fix(lsp): identifier array dimension no longer panics the analyzer [0099]
`Analyzer.resolveTypeNode` read the array `.length` node's `.int_literal`
union field unconditionally. For a named-const dimension (`MAX :: 4;
[MAX]u8`) that node is an `identifier`, so the access tripped Zig's
checked-union panic and `sx lsp` aborted on didOpen. The main compiler
was unaffected (it folds the dim through the IR).

- New `arrayDimLength` helper switches on the dimension node tag:
  int_literal → value; identifier → a recorded module-const int value;
  anything else / out-of-u32-range → unknown. Never assumes a node shape.
- `Type.ArrayTypeInfo.length` is now `?u32`; null is an explicit "editor
  couldn't fold this dimension" marker (rendered `[_]T`), never a
  fabricated concrete length.
- New `const_int_values` registry records integer-literal consts at
  registration time for the identifier path.

Regression: first `src/lsp/*.test.zig` (the minimal LSP harness), wired
into the test graph via `src/root.zig`. Drives `analyzeDocument` over
`[MAX]u8` (folds to 4, no panic), `[64]u8` (happy-path guard), and
`[N]u8` (explicit unknown). Fail-before/pass-after verified.

Sibling audit of the resolveTypeNode/fieldType family: the array dim was
the only unchecked union-field access; all other arms recurse or
tag-check first. Noted a non-crashing display gap in server.zig hover
rendering for step B.
2026-06-05 23:33:22 +03:00

355 lines
14 KiB
Zig
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const std = @import("std");
const ast = @import("ast.zig");
const Node = ast.Node;
/// Editor-indexing and parse-time name metadata — used by `src/sema.zig` (the
/// language-server symbol/type index) for navigation, completion, and hover, and
/// by `src/parser.zig` for parse-time primitive-name classification. This is NOT
/// a compiler type model: it carries no type-resolution surface (no widening,
/// convertibility, or layout). The canonical model the compiler resolves, lowers,
/// and lays out against is `TypeId` / `TypeTable` in `src/ir/types.zig`. Keep this
/// display- and classification-only; never add resolution semantics here.
pub const Type = union(enum) {
// Variable-width integers (164 bits)
signed: u8,
unsigned: u8,
// Fixed-width floats
f32,
f64,
// Other
void_type,
boolean,
string_type,
enum_type: []const u8,
struct_type: []const u8,
union_type: []const u8,
array_type: ArrayTypeInfo,
slice_type: SliceTypeInfo,
pointer_type: PointerTypeInfo,
many_pointer_type: ManyPointerTypeInfo,
vector_type: VectorTypeInfo,
function_type: FunctionTypeInfo,
closure_type: ClosureTypeInfo,
any_type,
usize_type,
isize_type,
optional_type: OptionalTypeInfo,
meta_type: MetaTypeInfo,
tuple_type: TupleTypeInfo,
/// Type resolution failed (sema couldn't infer/resolve). A dedicated
/// sentinel — never a legitimate type — so callers can't mistake it for a
/// real result the way a fabricated `s(64)` would be. Mirrors
/// `ir.TypeId.unresolved`.
unresolved,
/// `is_raw` records whether the inner type-name came from a backtick raw
/// reference (`` `s2 ``) or an already-resolved user type. It is the
/// `skip_builtin` the resolver MUST pass when re-resolving the stored inner
/// name (issue 0089) — without it `resolveTypeNameStr` would reclassify a
/// user type named `s2` as the builtin int, diverging from codegen. The
/// field is REQUIRED (no default) so a future construction site cannot
/// silently drop the bit, the way the LSP index did for compound shapes.
pub const SliceTypeInfo = struct {
element_name: []const u8,
is_raw: bool,
};
pub const PointerTypeInfo = struct {
pointee_name: []const u8,
is_raw: bool,
};
pub const ManyPointerTypeInfo = struct {
element_name: []const u8,
is_raw: bool,
};
pub const FunctionTypeInfo = struct {
param_types: []const Type,
return_type: *const Type,
};
pub const ClosureTypeInfo = struct {
param_types: []const Type,
return_type: *const Type,
};
pub const ArrayTypeInfo = struct {
element_name: []const u8,
/// null = the dimension could not be folded to a compile-time integer
/// by the editor index (an identifier const it couldn't resolve, or a
/// non-const expression). Explicit "unknown" rather than a fabricated
/// concrete length — this is hover/metadata, not codegen (issue 0099).
length: ?u32,
is_raw: bool,
};
pub const VectorTypeInfo = struct {
element_name: []const u8,
length: u32,
};
pub const OptionalTypeInfo = struct {
child_name: []const u8,
is_raw: bool,
};
pub const MetaTypeInfo = struct {
name: []const u8,
};
pub const TupleTypeInfo = struct {
field_names: ?[]const []const u8, // null for positional tuples
field_types: []const Type,
};
// Convenience constructors
pub fn s(width: u8) Type {
return .{ .signed = width };
}
pub fn u(width: u8) Type {
return .{ .unsigned = width };
}
pub fn fromName(name: []const u8) ?Type {
if (name.len == 0) return null;
return switch (name[0]) {
's' => {
if (std.mem.eql(u8, name, "string")) return .string_type;
if (name.len >= 2) {
const width = std.fmt.parseInt(u8, name[1..], 10) catch return null;
if (width >= 1 and width <= 64) return Type.s(width);
}
return null;
},
'u' => {
if (std.mem.eql(u8, name, "usize")) return .usize_type;
if (name.len >= 2) {
const width = std.fmt.parseInt(u8, name[1..], 10) catch return null;
if (width >= 1 and width <= 64) return Type.u(width);
}
return null;
},
'i' => {
if (std.mem.eql(u8, name, "isize")) return .isize_type;
return null;
},
'b' => if (std.mem.eql(u8, name, "bool")) .boolean else null,
'f' => {
if (std.mem.eql(u8, name, "f32")) return .f32;
if (std.mem.eql(u8, name, "f64")) return .f64;
return null;
},
'?' => if (name.len >= 2) .{ .optional_type = .{ .child_name = name[1..], .is_raw = false } } else null,
'A' => if (std.mem.eql(u8, name, "Any")) .any_type else null,
'v' => if (std.mem.eql(u8, name, "void")) .void_type else null,
'[' => {
// Sentinel-terminated slice: [:0]u8 → string_type
if (name.len >= 5 and name[1] == ':') {
if (std.mem.indexOfScalar(u8, name, ']')) |close| {
const sentinel = name[2..close];
const elem = name[close + 1 ..];
if (std.mem.eql(u8, sentinel, "0") and std.mem.eql(u8, elem, "u8")) {
return .string_type;
}
}
}
// Many-pointer: [*]T
if (name.len >= 4 and name[1] == '*' and name[2] == ']') {
return .{ .many_pointer_type = .{ .element_name = name[3..], .is_raw = false } };
}
return null;
},
'*' => if (name.len >= 2) .{ .pointer_type = .{ .pointee_name = name[1..], .is_raw = false } } else null,
'V' => {
// Vector(N,T)
if (name.len >= 10 and std.mem.startsWith(u8, name, "Vector(") and name[name.len - 1] == ')') {
const inner = name[7 .. name.len - 1];
if (std.mem.indexOfScalar(u8, inner, ',')) |comma| {
const length = std.fmt.parseInt(u32, inner[0..comma], 10) catch return null;
const elem_name = inner[comma + 1 ..];
if (elem_name.len > 0) {
return .{ .vector_type = .{ .element_name = elem_name, .length = length } };
}
}
}
return null;
},
else => null,
};
}
/// Returns the canonical type name for this type, or null for complex types.
/// Used for looking up impl methods on non-struct types (e.g., s32.eq).
pub fn toName(self: Type) ?[]const u8 {
return switch (self) {
.signed => |w| switch (w) {
8 => "s8",
16 => "s16",
32 => "s32",
64 => "s64",
else => null,
},
.unsigned => |w| switch (w) {
8 => "u8",
16 => "u16",
32 => "u32",
64 => "u64",
else => null,
},
.f32 => "f32",
.f64 => "f64",
.boolean => "bool",
.string_type => "string",
.void_type => "void",
.usize_type => "usize",
.isize_type => "isize",
.struct_type => |n| n,
.enum_type => |n| n,
.union_type => |n| n,
else => null,
};
}
pub fn fromTypeExpr(node: *Node) ?Type {
if (node.data != .type_expr) return null;
// A backtick raw type reference (`` `s2 ``) is the LITERAL name used as
// a type — it must skip this builtin/reserved classifier and resolve
// through user-defined types only (issue 0089), mirroring the codegen-
// side `resolveNamed`'s `skip_builtin`. Returning null lets the sema
// callers fall through to their struct/enum/alias registry lookup.
if (node.data.type_expr.is_raw) return null;
return fromName(node.data.type_expr.name);
}
pub fn isStruct(self: Type) bool {
return switch (self) {
.struct_type => true,
else => false,
};
}
pub fn isOptional(self: Type) bool {
return switch (self) {
.optional_type => true,
else => false,
};
}
pub fn isSlice(self: Type) bool {
return switch (self) {
.slice_type => true,
else => false,
};
}
pub fn isPointer(self: Type) bool {
return switch (self) {
.pointer_type => true,
else => false,
};
}
pub fn isManyPointer(self: Type) bool {
return switch (self) {
.many_pointer_type => true,
else => false,
};
}
pub fn isArray(self: Type) bool {
return switch (self) {
.array_type => true,
else => false,
};
}
fn fmtAlloc(allocator: std.mem.Allocator, comptime fmt: []const u8, args: anytype) ![]const u8 {
var buf: [128]u8 = undefined;
const result = std.fmt.bufPrint(&buf, fmt, args) catch
return try std.fmt.allocPrint(allocator, fmt, args);
return try allocator.dupe(u8, result);
}
/// Format type name for mangling and display (e.g. "s32", "u8", "f64")
pub fn displayName(self: Type, allocator: std.mem.Allocator) ![]const u8 {
return switch (self) {
.signed => |w| {
var buf: [4]u8 = undefined;
const result = std.fmt.bufPrint(&buf, "s{d}", .{w}) catch unreachable;
return try allocator.dupe(u8, result);
},
.unsigned => |w| {
var buf: [4]u8 = undefined;
const result = std.fmt.bufPrint(&buf, "u{d}", .{w}) catch unreachable;
return try allocator.dupe(u8, result);
},
.f32 => "f32",
.f64 => "f64",
.boolean => "bool",
.string_type => "string",
.void_type => "void",
.any_type => "Any",
.usize_type => "usize",
.isize_type => "isize",
.unresolved => "<unresolved>",
.enum_type => |name| name,
.struct_type => |name| name,
.union_type => |name| name,
.slice_type => |info| return fmtAlloc(allocator, "[]{s}", .{info.element_name}),
.pointer_type => |info| return fmtAlloc(allocator, "*{s}", .{info.pointee_name}),
.many_pointer_type => |info| return fmtAlloc(allocator, "[*]{s}", .{info.element_name}),
.array_type => |info| {
if (info.length) |n| return fmtAlloc(allocator, "[{d}]{s}", .{ n, info.element_name });
return fmtAlloc(allocator, "[_]{s}", .{info.element_name});
},
.vector_type => |info| return fmtAlloc(allocator, "Vector({d},{s})", .{ info.length, info.element_name }),
.function_type => |info| {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, '(');
for (info.param_types, 0..) |pt, i| {
if (i > 0) try buf.appendSlice(allocator, ", ");
try buf.appendSlice(allocator, try pt.displayName(allocator));
}
try buf.append(allocator, ')');
if (!std.meta.eql(info.return_type.*, Type.void_type)) {
try buf.appendSlice(allocator, " -> ");
try buf.appendSlice(allocator, try info.return_type.displayName(allocator));
}
return try buf.toOwnedSlice(allocator);
},
.closure_type => |info| {
var buf = std.ArrayList(u8).empty;
try buf.appendSlice(allocator, "Closure(");
for (info.param_types, 0..) |pt, i| {
if (i > 0) try buf.appendSlice(allocator, ", ");
try buf.appendSlice(allocator, try pt.displayName(allocator));
}
try buf.append(allocator, ')');
if (!std.meta.eql(info.return_type.*, Type.void_type)) {
try buf.appendSlice(allocator, " -> ");
try buf.appendSlice(allocator, try info.return_type.displayName(allocator));
}
return try buf.toOwnedSlice(allocator);
},
.optional_type => |info| return fmtAlloc(allocator, "?{s}", .{info.child_name}),
.meta_type => |info| info.name,
.tuple_type => |info| {
var buf = std.ArrayList(u8).empty;
try buf.append(allocator, '(');
for (info.field_types, 0..) |ft, i| {
if (i > 0) try buf.appendSlice(allocator, ", ");
if (info.field_names) |names| {
try buf.appendSlice(allocator, names[i]);
try buf.appendSlice(allocator, ": ");
}
try buf.appendSlice(allocator, try ft.displayName(allocator));
}
try buf.append(allocator, ')');
return try buf.toOwnedSlice(allocator);
},
};
}
};