674 lines
24 KiB
Zig
674 lines
24 KiB
Zig
const std = @import("std");
|
||
const ast = @import("ast.zig");
|
||
const Node = ast.Node;
|
||
|
||
pub const Type = union(enum) {
|
||
// Variable-width integers (1–64 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;
|
||
}
|
||
};
|