Files
sx/src/types.zig
2026-03-02 17:19:41 +02:00

674 lines
24 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;
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,
pub const SliceTypeInfo = struct {
element_name: []const u8,
};
pub const PointerTypeInfo = struct {
pointee_name: []const u8,
};
pub const ManyPointerTypeInfo = struct {
element_name: []const u8,
};
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,
length: u32,
};
pub const VectorTypeInfo = struct {
element_name: []const u8,
length: u32,
};
pub const OptionalTypeInfo = struct {
child_name: []const u8,
};
pub const MetaTypeInfo = struct {
name: []const u8,
};
pub const TupleTypeInfo = struct {
field_names: ?[]const []const u8, // null for positional tuples
field_types: []const Type,
};
/// Content-based equality: compares string fields by content, not pointer identity.
pub fn eql(self: Type, other: Type) bool {
const Tag = std.meta.Tag(Type);
const self_tag: Tag = self;
const other_tag: Tag = other;
if (self_tag != other_tag) return false;
return switch (self) {
.signed => |w| w == other.signed,
.unsigned => |w| w == other.unsigned,
.f32, .f64, .void_type, .boolean, .string_type, .any_type, .usize_type, .isize_type => true,
.enum_type => |n| std.mem.eql(u8, n, other.enum_type),
.struct_type => |n| std.mem.eql(u8, n, other.struct_type),
.union_type => |n| std.mem.eql(u8, n, other.union_type),
.array_type => |info| info.length == other.array_type.length and
std.mem.eql(u8, info.element_name, other.array_type.element_name),
.slice_type => |info| std.mem.eql(u8, info.element_name, other.slice_type.element_name),
.pointer_type => |info| std.mem.eql(u8, info.pointee_name, other.pointer_type.pointee_name),
.many_pointer_type => |info| std.mem.eql(u8, info.element_name, other.many_pointer_type.element_name),
.vector_type => |info| info.length == other.vector_type.length and
std.mem.eql(u8, info.element_name, other.vector_type.element_name),
.function_type => |info| {
const o = other.function_type;
if (info.param_types.len != o.param_types.len) return false;
for (info.param_types, o.param_types) |a, b| {
if (!a.eql(b)) return false;
}
return info.return_type.eql(o.return_type.*);
},
.closure_type => |info| {
const o = other.closure_type;
if (info.param_types.len != o.param_types.len) return false;
for (info.param_types, o.param_types) |a, b| {
if (!a.eql(b)) return false;
}
return info.return_type.eql(o.return_type.*);
},
.optional_type => |info| std.mem.eql(u8, info.child_name, other.optional_type.child_name),
.meta_type => |info| std.mem.eql(u8, info.name, other.meta_type.name),
.tuple_type => |info| {
const o = other.tuple_type;
if (info.field_types.len != o.field_types.len) return false;
for (info.field_types, o.field_types) |a, b| {
if (!a.eql(b)) return false;
}
// If both have names, compare them
if (info.field_names != null and o.field_names != null) {
for (info.field_names.?, o.field_names.?) |a, b| {
if (!std.mem.eql(u8, a, b)) return false;
}
}
return true;
},
};
}
// 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..] } } 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..] } };
}
return null;
},
'*' => if (name.len >= 2) .{ .pointer_type = .{ .pointee_name = name[1..] } } 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;
return fromName(node.data.type_expr.name);
}
pub fn isEnum(self: Type) bool {
return switch (self) {
.enum_type => true,
else => false,
};
}
pub fn isStruct(self: Type) bool {
return switch (self) {
.struct_type => true,
else => false,
};
}
pub fn isUnion(self: Type) bool {
return switch (self) {
.union_type => true,
else => false,
};
}
pub fn isTuple(self: Type) bool {
return switch (self) {
.tuple_type => true,
else => false,
};
}
pub fn isOptional(self: Type) bool {
return switch (self) {
.optional_type => true,
else => false,
};
}
pub fn optionalChild(self: Type) ?[]const u8 {
return switch (self) {
.optional_type => |info| info.child_name,
else => null,
};
}
pub fn isAny(self: Type) bool {
return switch (self) {
.any_type => true,
else => false,
};
}
pub fn isString(self: Type) bool {
return self == .string_type;
}
/// Returns true for both `string` (null-terminated) and `[]u8` (byte slice)
pub fn isStringLike(self: Type) bool {
if (self == .string_type) return true;
if (self.isSlice()) {
return std.mem.eql(u8, self.slice_type.element_name, "u8");
}
return false;
}
pub fn isSlice(self: Type) bool {
return switch (self) {
.slice_type => true,
else => false,
};
}
pub fn sliceElementType(self: Type) ?Type {
return switch (self) {
.slice_type => |info| fromName(info.element_name),
else => null,
};
}
pub fn isPointer(self: Type) bool {
return switch (self) {
.pointer_type => true,
else => false,
};
}
pub fn pointerPointeeType(self: Type) ?Type {
return switch (self) {
.pointer_type => |info| fromName(info.pointee_name),
else => null,
};
}
pub fn isManyPointer(self: Type) bool {
return switch (self) {
.many_pointer_type => true,
else => false,
};
}
pub fn manyPointerElementType(self: Type) ?Type {
return switch (self) {
.many_pointer_type => |info| fromName(info.element_name),
else => null,
};
}
pub fn isFunctionType(self: Type) bool {
return switch (self) {
.function_type => true,
else => false,
};
}
pub fn isClosureType(self: Type) bool {
return switch (self) {
.closure_type => true,
else => false,
};
}
/// Returns true for both bare function pointers and closures
pub fn isCallable(self: Type) bool {
return switch (self) {
.function_type, .closure_type => true,
else => false,
};
}
pub fn isArray(self: Type) bool {
return switch (self) {
.array_type => true,
else => false,
};
}
pub fn isVector(self: Type) bool {
return switch (self) {
.vector_type => true,
else => false,
};
}
pub fn vectorElementType(self: Type) ?Type {
return switch (self) {
.vector_type => |info| fromName(info.element_name),
else => null,
};
}
pub fn isFloat(self: Type) bool {
return switch (self) {
.f32, .f64 => true,
else => false,
};
}
pub fn isInt(self: Type) bool {
return self.isSigned() or self.isUnsigned();
}
pub fn isSigned(self: Type) bool {
return switch (self) {
.signed => true,
else => false,
};
}
pub fn isUnsigned(self: Type) bool {
return switch (self) {
.unsigned => true,
else => false,
};
}
pub fn bitWidth(self: Type) u32 {
return switch (self) {
.signed => |w| w,
.unsigned => |w| w,
.f32 => 32,
.f64 => 64,
.boolean => 1,
.pointer_type, .many_pointer_type, .function_type => 64,
.closure_type => 128, // { ptr, ptr } = 16 bytes
else => 0,
};
}
/// Check if this type can be implicitly converted to `target` without `xx`.
/// Safe (implicit) conversions:
/// - Same type
/// - Both unsigned int, target width >= source width
/// - Both signed int, target width >= source width
/// - Unsigned to signed, target width strictly > source width
/// - Any int to any float
/// - Float to wider float (f32 → f64)
/// Everything else requires `xx`.
pub fn isImplicitlyConvertibleTo(self: Type, target: Type) bool {
if (self.eql(target)) return true;
// string <-> []u8: same layout, bidirectional implicit conversion
if (self == .string_type and target.isSlice() and
std.mem.eql(u8, target.slice_type.element_name, "u8")) return true;
if (self.isSlice() and std.mem.eql(u8, self.slice_type.element_name, "u8") and
target == .string_type) return true;
// *void is universal pointer (both directions)
if (self.isPointer() and target.isPointer()) {
if (std.mem.eql(u8, self.pointer_type.pointee_name, "void")) return true;
if (std.mem.eql(u8, target.pointer_type.pointee_name, "void")) return true;
}
// *T → [*]T: pointer to element is implicitly convertible to many-pointer
// null (*void) → [*]T is also allowed
if (self.isPointer() and target.isManyPointer()) {
if (std.mem.eql(u8, self.pointer_type.pointee_name, "void")) return true;
return std.mem.eql(u8, self.pointer_type.pointee_name, target.many_pointer_type.element_name);
}
// [*]T → *void: any many-pointer converts to void pointer
if (self.isManyPointer() and target.isPointer()) {
return std.mem.eql(u8, target.pointer_type.pointee_name, "void");
}
// Tuple → tuple: same field count and each field implicitly convertible
if (self.isTuple() and target.isTuple()) {
const si = self.tuple_type;
const ti = target.tuple_type;
if (si.field_types.len != ti.field_types.len) return false;
for (si.field_types, ti.field_types) |sf, tf| {
if (!sf.isImplicitlyConvertibleTo(tf)) return false;
}
return true;
}
// T → ?T: any type implicitly wraps into its optional
if (target.isOptional()) {
const child_name = target.optional_type.child_name;
// null → ?T
if (self.isPointer() and std.mem.eql(u8, self.pointer_type.pointee_name, "void")) return true;
// ?T → ?U when T → U
if (self.isOptional()) {
const self_child = fromName(self.optional_type.child_name) orelse return false;
const target_child = fromName(child_name) orelse return false;
return self_child.isImplicitlyConvertibleTo(target_child);
}
// T → ?T: check if self matches the child type
if (fromName(child_name)) |child_type| {
return self.eql(child_type) or self.isImplicitlyConvertibleTo(child_type);
}
// Non-primitive child (struct/enum name): compare by name
return switch (self) {
.struct_type => |n| std.mem.eql(u8, n, child_name),
.enum_type => |n| std.mem.eql(u8, n, child_name),
.union_type => |n| std.mem.eql(u8, n, child_name),
else => false,
};
}
const src_float = self.isFloat();
const dst_float = target.isFloat();
const src_int = self.isInt();
// Float → wider float
if (src_float and dst_float) {
return target.bitWidth() >= self.bitWidth();
}
// Int → float (always safe)
if (src_int and dst_float) return true;
// Both unsigned → target width >= source width
if (self.isUnsigned() and target.isUnsigned()) {
return target.bitWidth() >= self.bitWidth();
}
// Both signed → target width >= source width
if (self.isSigned() and target.isSigned()) {
return target.bitWidth() >= self.bitWidth();
}
// Unsigned → signed: target must be strictly wider
if (self.isUnsigned() and target.isSigned()) {
return target.bitWidth() > self.bitWidth();
}
// Everything else requires xx
return 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",
.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| return fmtAlloc(allocator, "[{d}]{s}", .{ info.length, 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);
},
};
}
/// Widen two types to a common type for binary operations.
/// Used for arithmetic type promotion (e.g., s16 + s32 → s32, int + float → float).
pub fn widen(a: Type, b: Type) Type {
// Same type → return it
if (a.eql(b)) return a;
// Tuple + tuple → return a if same field count
if (a.isTuple() and b.isTuple()) {
if (a.tuple_type.field_types.len == b.tuple_type.field_types.len) return a;
}
// Vector + vector of same dimensions → return a
if (a.isVector() and b.isVector()) return a;
// Vector + scalar → return vector (scalar will be broadcast)
if (a.isVector() and !b.isVector()) return a;
if (b.isVector() and !a.isVector()) return b;
const a_float = a.isFloat();
const b_float = b.isFloat();
const a_int = a.isInt();
const b_int = b.isInt();
// Both float → wider float
if (a_float and b_float) {
return if (a.bitWidth() >= b.bitWidth()) a else b;
}
// int + float → float
if (a_int and b_float) return b;
if (b_int and a_float) return a;
// Both signed → wider signed
if (a.isSigned() and b.isSigned()) {
return Type.s(@intCast(@max(a.bitWidth(), b.bitWidth())));
}
// Both unsigned → wider unsigned
if (a.isUnsigned() and b.isUnsigned()) {
return Type.u(@intCast(@max(a.bitWidth(), b.bitWidth())));
}
// signed + unsigned (mixed)
if (a_int and b_int) {
const aw = a.bitWidth();
const bw = b.bitWidth();
const max_w = @max(aw, bw);
// If same width, need one extra bit for sign; otherwise max is enough
const need: u32 = if (aw == bw) max_w + 1 else max_w;
const capped: u8 = @intCast(@min(need, 128));
return Type.s(capped);
}
// Optional types: widen inner types
if (a.isOptional() and b.isOptional()) return a;
// Pointer types: both are pointers → return first (all are opaque ptr at LLVM level)
if ((a.isPointer() or a.isManyPointer()) and (b.isPointer() or b.isManyPointer())) return a;
return a;
}
};