zig/deps/aro/Value.zig
Veikka Tuominen 5792570197 add Aro sources as a dependency
ref: 5688dbccfb58216468267a0f46b96bed7013715a
2023-10-01 23:51:54 +03:00

602 lines
21 KiB
Zig
Vendored

const std = @import("std");
const assert = std.debug.assert;
const Compilation = @import("Compilation.zig");
const Type = @import("Type.zig");
const Value = @This();
pub const ByteRange = struct {
start: u32,
end: u32,
pub fn len(self: ByteRange) u32 {
return self.end - self.start;
}
pub fn trim(self: ByteRange, amount: u32) ByteRange {
std.debug.assert(self.start <= self.end - amount);
return .{ .start = self.start, .end = self.end - amount };
}
pub fn slice(self: ByteRange, all_bytes: []const u8) []const u8 {
return all_bytes[self.start..self.end];
}
};
tag: Tag = .unavailable,
data: union {
none: void,
int: u64,
float: f64,
bytes: ByteRange,
} = .{ .none = {} },
const Tag = enum {
unavailable,
nullptr_t,
/// int is used to store integer, boolean and pointer values
int,
float,
bytes,
};
pub fn zero(v: Value) Value {
return switch (v.tag) {
.int => int(0),
.float => float(0),
else => unreachable,
};
}
pub fn one(v: Value) Value {
return switch (v.tag) {
.int => int(1),
.float => float(1),
else => unreachable,
};
}
pub fn int(v: anytype) Value {
if (@TypeOf(v) == comptime_int or @typeInfo(@TypeOf(v)).Int.signedness == .unsigned)
return .{ .tag = .int, .data = .{ .int = v } }
else
return .{ .tag = .int, .data = .{ .int = @bitCast(@as(i64, v)) } };
}
pub fn float(v: anytype) Value {
return .{ .tag = .float, .data = .{ .float = v } };
}
pub fn bytes(start: u32, end: u32) Value {
return .{ .tag = .bytes, .data = .{ .bytes = .{ .start = start, .end = end } } };
}
pub fn signExtend(v: Value, old_ty: Type, comp: *Compilation) i64 {
const size = old_ty.sizeof(comp).?;
return switch (size) {
1 => v.getInt(i8),
2 => v.getInt(i16),
4 => v.getInt(i32),
8 => v.getInt(i64),
else => unreachable,
};
}
/// Number of bits needed to hold `v` which is of type `ty`.
/// Asserts that `v` is not negative
pub fn minUnsignedBits(v: Value, ty: Type, comp: *const Compilation) usize {
assert(v.compare(.gte, Value.int(0), ty, comp));
return switch (ty.sizeof(comp).?) {
1 => 8 - @clz(v.getInt(u8)),
2 => 16 - @clz(v.getInt(u16)),
4 => 32 - @clz(v.getInt(u32)),
8 => 64 - @clz(v.getInt(u64)),
else => unreachable,
};
}
test "minUnsignedBits" {
const Test = struct {
fn checkIntBits(comp: *const Compilation, specifier: Type.Specifier, v: u64, expected: usize) !void {
const val = Value.int(v);
try std.testing.expectEqual(expected, val.minUnsignedBits(.{ .specifier = specifier }, comp));
}
};
var comp = Compilation.init(std.testing.allocator);
defer comp.deinit();
comp.target = (try std.zig.CrossTarget.parse(.{ .arch_os_abi = "x86_64-linux-gnu" })).toTarget();
try Test.checkIntBits(&comp, .int, 0, 0);
try Test.checkIntBits(&comp, .int, 1, 1);
try Test.checkIntBits(&comp, .int, 2, 2);
try Test.checkIntBits(&comp, .int, std.math.maxInt(i8), 7);
try Test.checkIntBits(&comp, .int, std.math.maxInt(u8), 8);
try Test.checkIntBits(&comp, .int, std.math.maxInt(i16), 15);
try Test.checkIntBits(&comp, .int, std.math.maxInt(u16), 16);
try Test.checkIntBits(&comp, .int, std.math.maxInt(i32), 31);
try Test.checkIntBits(&comp, .uint, std.math.maxInt(u32), 32);
try Test.checkIntBits(&comp, .long, std.math.maxInt(i64), 63);
try Test.checkIntBits(&comp, .ulong, std.math.maxInt(u64), 64);
try Test.checkIntBits(&comp, .long_long, std.math.maxInt(i64), 63);
try Test.checkIntBits(&comp, .ulong_long, std.math.maxInt(u64), 64);
}
/// Minimum number of bits needed to represent `v` in 2's complement notation
/// Asserts that `v` is negative.
pub fn minSignedBits(v: Value, ty: Type, comp: *const Compilation) usize {
assert(v.compare(.lt, Value.int(0), ty, comp));
return switch (ty.sizeof(comp).?) {
1 => 8 - @clz(~v.getInt(u8)) + 1,
2 => 16 - @clz(~v.getInt(u16)) + 1,
4 => 32 - @clz(~v.getInt(u32)) + 1,
8 => 64 - @clz(~v.getInt(u64)) + 1,
else => unreachable,
};
}
test "minSignedBits" {
const Test = struct {
fn checkIntBits(comp: *const Compilation, specifier: Type.Specifier, v: i64, expected: usize) !void {
const val = Value.int(v);
try std.testing.expectEqual(expected, val.minSignedBits(.{ .specifier = specifier }, comp));
}
};
var comp = Compilation.init(std.testing.allocator);
defer comp.deinit();
comp.target = (try std.zig.CrossTarget.parse(.{ .arch_os_abi = "x86_64-linux-gnu" })).toTarget();
for ([_]Type.Specifier{ .int, .long, .long_long }) |specifier| {
try Test.checkIntBits(&comp, specifier, -1, 1);
try Test.checkIntBits(&comp, specifier, -2, 2);
try Test.checkIntBits(&comp, specifier, -10, 5);
try Test.checkIntBits(&comp, specifier, -101, 8);
try Test.checkIntBits(&comp, specifier, std.math.minInt(i8), 8);
try Test.checkIntBits(&comp, specifier, std.math.minInt(i16), 16);
try Test.checkIntBits(&comp, specifier, std.math.minInt(i32), 32);
}
try Test.checkIntBits(&comp, .long, std.math.minInt(i64), 64);
try Test.checkIntBits(&comp, .long_long, std.math.minInt(i64), 64);
}
pub const FloatToIntChangeKind = enum {
/// value did not change
none,
/// floating point number too small or large for destination integer type
out_of_range,
/// tried to convert a NaN or Infinity
overflow,
/// fractional value was converted to zero
nonzero_to_zero,
/// fractional part truncated
value_changed,
};
fn floatToIntExtra(comptime FloatTy: type, int_ty_signedness: std.builtin.Signedness, int_ty_size: u16, v: *Value) FloatToIntChangeKind {
const float_val = v.getFloat(FloatTy);
const was_zero = float_val == 0;
const had_fraction = std.math.modf(float_val).fpart != 0;
switch (int_ty_signedness) {
inline else => |signedness| switch (int_ty_size) {
inline 1, 2, 4, 8 => |bytecount| {
const IntTy = std.meta.Int(signedness, bytecount * 8);
const intVal = std.math.lossyCast(IntTy, float_val);
v.* = int(intVal);
if (!was_zero and v.isZero()) return .nonzero_to_zero;
if (float_val <= std.math.minInt(IntTy) or float_val >= std.math.maxInt(IntTy)) return .out_of_range;
if (had_fraction) return .value_changed;
return .none;
},
else => unreachable,
},
}
}
/// Converts the stored value from a float to an integer.
/// `.unavailable` value remains unchanged.
pub fn floatToInt(v: *Value, old_ty: Type, new_ty: Type, comp: *Compilation) FloatToIntChangeKind {
assert(old_ty.isFloat());
if (v.tag == .unavailable) return .none;
if (new_ty.is(.bool)) {
const was_zero = v.isZero();
const was_one = v.getFloat(f64) == 1.0;
v.toBool();
if (was_zero or was_one) return .none;
return .value_changed;
} else if (new_ty.isUnsignedInt(comp) and v.data.float < 0) {
v.* = int(0);
return .out_of_range;
} else if (!std.math.isFinite(v.data.float)) {
v.tag = .unavailable;
return .overflow;
}
const old_size = old_ty.sizeof(comp).?;
const new_size: u16 = @intCast(new_ty.sizeof(comp).?);
if (new_ty.isUnsignedInt(comp)) switch (old_size) {
1 => unreachable, // promoted to int
2 => unreachable, // promoted to int
4 => return floatToIntExtra(f32, .unsigned, new_size, v),
8 => return floatToIntExtra(f64, .unsigned, new_size, v),
else => unreachable,
} else switch (old_size) {
1 => unreachable, // promoted to int
2 => unreachable, // promoted to int
4 => return floatToIntExtra(f32, .signed, new_size, v),
8 => return floatToIntExtra(f64, .signed, new_size, v),
else => unreachable,
}
}
/// Converts the stored value from an integer to a float.
/// `.unavailable` value remains unchanged.
pub fn intToFloat(v: *Value, old_ty: Type, new_ty: Type, comp: *Compilation) void {
assert(old_ty.isInt());
if (v.tag == .unavailable) return;
if (!new_ty.isReal() or new_ty.sizeof(comp).? > 8) {
v.tag = .unavailable;
} else if (old_ty.isUnsignedInt(comp)) {
v.* = float(@as(f64, @floatFromInt(v.data.int)));
} else {
v.* = float(@as(f64, @floatFromInt(@as(i64, @bitCast(v.data.int)))));
}
}
/// Truncates or extends bits based on type.
/// old_ty is only used for size.
pub fn intCast(v: *Value, old_ty: Type, new_ty: Type, comp: *Compilation) void {
// assert(old_ty.isInt() and new_ty.isInt());
if (v.tag == .unavailable) return;
if (new_ty.is(.bool)) return v.toBool();
if (!old_ty.isUnsignedInt(comp)) {
const size = new_ty.sizeof(comp).?;
switch (size) {
1 => v.* = int(@as(u8, @truncate(@as(u64, @bitCast(v.signExtend(old_ty, comp)))))),
2 => v.* = int(@as(u16, @truncate(@as(u64, @bitCast(v.signExtend(old_ty, comp)))))),
4 => v.* = int(@as(u32, @truncate(@as(u64, @bitCast(v.signExtend(old_ty, comp)))))),
8 => return,
else => unreachable,
}
}
}
/// Converts the stored value from an integer to a float.
/// `.unavailable` value remains unchanged.
pub fn floatCast(v: *Value, old_ty: Type, new_ty: Type, comp: *Compilation) void {
assert(old_ty.isFloat() and new_ty.isFloat());
if (v.tag == .unavailable) return;
const size = new_ty.sizeof(comp).?;
if (!new_ty.isReal() or size > 8) {
v.tag = .unavailable;
} else if (size == 32) {
v.* = float(@as(f32, @floatCast(v.data.float)));
}
}
/// Truncates data.int to one bit
pub fn toBool(v: *Value) void {
if (v.tag == .unavailable) return;
const res = v.getBool();
v.* = int(@intFromBool(res));
}
pub fn isZero(v: Value) bool {
return switch (v.tag) {
.unavailable => false,
.nullptr_t => false,
.int => v.data.int == 0,
.float => v.data.float == 0,
.bytes => false,
};
}
pub fn getBool(v: Value) bool {
return switch (v.tag) {
.unavailable => unreachable,
.nullptr_t => false,
.int => v.data.int != 0,
.float => v.data.float != 0,
.bytes => true,
};
}
pub fn getInt(v: Value, comptime T: type) T {
if (T == u64) return v.data.int;
return if (@typeInfo(T).Int.signedness == .unsigned)
@truncate(v.data.int)
else
@truncate(@as(i64, @bitCast(v.data.int)));
}
pub fn getFloat(v: Value, comptime T: type) T {
if (T == f64) return v.data.float;
return @floatCast(v.data.float);
}
const bin_overflow = struct {
inline fn addInt(comptime T: type, out: *Value, a: Value, b: Value) bool {
const a_val = a.getInt(T);
const b_val = b.getInt(T);
const sum, const overflowed = @addWithOverflow(a_val, b_val);
out.* = int(sum);
return overflowed != 0;
}
inline fn addFloat(comptime T: type, aa: Value, bb: Value) Value {
const a_val = aa.getFloat(T);
const b_val = bb.getFloat(T);
return float(a_val + b_val);
}
inline fn subInt(comptime T: type, out: *Value, a: Value, b: Value) bool {
const a_val = a.getInt(T);
const b_val = b.getInt(T);
const difference, const overflowed = @subWithOverflow(a_val, b_val);
out.* = int(difference);
return overflowed != 0;
}
inline fn subFloat(comptime T: type, aa: Value, bb: Value) Value {
const a_val = aa.getFloat(T);
const b_val = bb.getFloat(T);
return float(a_val - b_val);
}
inline fn mulInt(comptime T: type, out: *Value, a: Value, b: Value) bool {
const a_val = a.getInt(T);
const b_val = b.getInt(T);
const product, const overflowed = @mulWithOverflow(a_val, b_val);
out.* = int(product);
return overflowed != 0;
}
inline fn mulFloat(comptime T: type, aa: Value, bb: Value) Value {
const a_val = aa.getFloat(T);
const b_val = bb.getFloat(T);
return float(a_val * b_val);
}
const FT = fn (*Value, Value, Value, Type, *Compilation) bool;
fn getOp(comptime intFunc: anytype, comptime floatFunc: anytype) FT {
return struct {
fn op(res: *Value, a: Value, b: Value, ty: Type, comp: *Compilation) bool {
const size = ty.sizeof(comp).?;
if (@TypeOf(floatFunc) != @TypeOf(null) and ty.isFloat()) {
res.* = switch (size) {
4 => floatFunc(f32, a, b),
8 => floatFunc(f64, a, b),
else => unreachable,
};
return false;
}
if (ty.isUnsignedInt(comp)) switch (size) {
1 => return intFunc(u8, res, a, b),
2 => return intFunc(u16, res, a, b),
4 => return intFunc(u32, res, a, b),
8 => return intFunc(u64, res, a, b),
else => unreachable,
} else switch (size) {
1 => return intFunc(u8, res, a, b),
2 => return intFunc(u16, res, a, b),
4 => return intFunc(i32, res, a, b),
8 => return intFunc(i64, res, a, b),
else => unreachable,
}
}
}.op;
}
};
pub const add = bin_overflow.getOp(bin_overflow.addInt, bin_overflow.addFloat);
pub const sub = bin_overflow.getOp(bin_overflow.subInt, bin_overflow.subFloat);
pub const mul = bin_overflow.getOp(bin_overflow.mulInt, bin_overflow.mulFloat);
const bin_ops = struct {
inline fn divInt(comptime T: type, aa: Value, bb: Value) Value {
const a_val = aa.getInt(T);
const b_val = bb.getInt(T);
return int(@divTrunc(a_val, b_val));
}
inline fn divFloat(comptime T: type, aa: Value, bb: Value) Value {
const a_val = aa.getFloat(T);
const b_val = bb.getFloat(T);
return float(a_val / b_val);
}
inline fn remInt(comptime T: type, a: Value, b: Value) Value {
const a_val = a.getInt(T);
const b_val = b.getInt(T);
if (@typeInfo(T).Int.signedness == .signed) {
if (a_val == std.math.minInt(T) and b_val == -1) {
return Value{ .tag = .unavailable, .data = .{ .none = {} } };
} else {
if (b_val > 0) return int(@rem(a_val, b_val));
return int(a_val - @divTrunc(a_val, b_val) * b_val);
}
} else {
return int(a_val % b_val);
}
}
inline fn orInt(comptime T: type, a: Value, b: Value) Value {
const a_val = a.getInt(T);
const b_val = b.getInt(T);
return int(a_val | b_val);
}
inline fn xorInt(comptime T: type, a: Value, b: Value) Value {
const a_val = a.getInt(T);
const b_val = b.getInt(T);
return int(a_val ^ b_val);
}
inline fn andInt(comptime T: type, a: Value, b: Value) Value {
const a_val = a.getInt(T);
const b_val = b.getInt(T);
return int(a_val & b_val);
}
inline fn shl(comptime T: type, a: Value, b: Value) Value {
const ShiftT = std.math.Log2Int(T);
const info = @typeInfo(T).Int;
const UT = std.meta.Int(.unsigned, info.bits);
const b_val = b.getInt(T);
if (b_val > std.math.maxInt(ShiftT)) {
return if (info.signedness == .unsigned)
int(@as(UT, std.math.maxInt(UT)))
else
int(@as(T, std.math.minInt(T)));
}
const amt: ShiftT = @truncate(@as(UT, @bitCast(b_val)));
const a_val = a.getInt(T);
return int(a_val << amt);
}
inline fn shr(comptime T: type, a: Value, b: Value) Value {
const ShiftT = std.math.Log2Int(T);
const UT = std.meta.Int(.unsigned, @typeInfo(T).Int.bits);
const b_val = b.getInt(T);
if (b_val > std.math.maxInt(ShiftT)) return Value.int(0);
const amt: ShiftT = @truncate(@as(UT, @intCast(b_val)));
const a_val = a.getInt(T);
return int(a_val >> amt);
}
const FT = fn (Value, Value, Type, *Compilation) Value;
fn getOp(comptime intFunc: anytype, comptime floatFunc: anytype) FT {
return struct {
fn op(a: Value, b: Value, ty: Type, comp: *Compilation) Value {
const size = ty.sizeof(comp).?;
if (@TypeOf(floatFunc) != @TypeOf(null) and ty.isFloat()) {
switch (size) {
4 => return floatFunc(f32, a, b),
8 => return floatFunc(f64, a, b),
else => unreachable,
}
}
if (ty.isUnsignedInt(comp)) switch (size) {
1 => unreachable, // promoted to int
2 => unreachable, // promoted to int
4 => return intFunc(u32, a, b),
8 => return intFunc(u64, a, b),
else => unreachable,
} else switch (size) {
1 => unreachable, // promoted to int
2 => unreachable, // promoted to int
4 => return intFunc(i32, a, b),
8 => return intFunc(i64, a, b),
else => unreachable,
}
}
}.op;
}
};
/// caller guarantees rhs != 0
pub const div = bin_ops.getOp(bin_ops.divInt, bin_ops.divFloat);
/// caller guarantees rhs != 0
/// caller guarantees lhs != std.math.minInt(T) OR rhs != -1
pub const rem = bin_ops.getOp(bin_ops.remInt, null);
pub const bitOr = bin_ops.getOp(bin_ops.orInt, null);
pub const bitXor = bin_ops.getOp(bin_ops.xorInt, null);
pub const bitAnd = bin_ops.getOp(bin_ops.andInt, null);
pub const shl = bin_ops.getOp(bin_ops.shl, null);
pub const shr = bin_ops.getOp(bin_ops.shr, null);
pub fn bitNot(v: Value, ty: Type, comp: *Compilation) Value {
const size = ty.sizeof(comp).?;
var out: Value = undefined;
if (ty.isUnsignedInt(comp)) switch (size) {
1 => unreachable, // promoted to int
2 => unreachable, // promoted to int
4 => out = int(~v.getInt(u32)),
8 => out = int(~v.getInt(u64)),
else => unreachable,
} else switch (size) {
1 => unreachable, // promoted to int
2 => unreachable, // promoted to int
4 => out = int(~v.getInt(i32)),
8 => out = int(~v.getInt(i64)),
else => unreachable,
}
return out;
}
pub fn compare(a: Value, op: std.math.CompareOperator, b: Value, ty: Type, comp: *const Compilation) bool {
assert(a.tag == b.tag);
if (a.tag == .nullptr_t) {
return switch (op) {
.eq => true,
.neq => false,
else => unreachable,
};
}
const S = struct {
inline fn doICompare(comptime T: type, aa: Value, opp: std.math.CompareOperator, bb: Value) bool {
const a_val = aa.getInt(T);
const b_val = bb.getInt(T);
return std.math.compare(a_val, opp, b_val);
}
inline fn doFCompare(comptime T: type, aa: Value, opp: std.math.CompareOperator, bb: Value) bool {
const a_val = aa.getFloat(T);
const b_val = bb.getFloat(T);
return std.math.compare(a_val, opp, b_val);
}
};
const size = ty.sizeof(comp).?;
switch (a.tag) {
.unavailable => return true,
.int => if (ty.isUnsignedInt(comp)) switch (size) {
1 => return S.doICompare(u8, a, op, b),
2 => return S.doICompare(u16, a, op, b),
4 => return S.doICompare(u32, a, op, b),
8 => return S.doICompare(u64, a, op, b),
else => unreachable,
} else switch (size) {
1 => return S.doICompare(i8, a, op, b),
2 => return S.doICompare(i16, a, op, b),
4 => return S.doICompare(i32, a, op, b),
8 => return S.doICompare(i64, a, op, b),
else => unreachable,
},
.float => switch (size) {
4 => return S.doFCompare(f32, a, op, b),
8 => return S.doFCompare(f64, a, op, b),
else => unreachable,
},
else => @panic("TODO"),
}
return false;
}
pub fn hash(v: Value) u64 {
switch (v.tag) {
.unavailable => unreachable,
.int => return std.hash.Wyhash.hash(0, std.mem.asBytes(&v.data.int)),
else => @panic("TODO"),
}
}
pub fn dump(v: Value, ty: Type, comp: *Compilation, strings: []const u8, w: anytype) !void {
switch (v.tag) {
.unavailable => try w.writeAll("unavailable"),
.int => if (ty.is(.bool) and comp.langopts.standard.atLeast(.c2x)) {
try w.print("{s}", .{if (v.isZero()) "false" else "true"});
} else if (ty.isUnsignedInt(comp)) {
try w.print("{d}", .{v.data.int});
} else {
try w.print("{d}", .{v.signExtend(ty, comp)});
},
.bytes => try w.print("\"{s}\"", .{v.data.bytes.slice(strings)}),
// std.fmt does @as instead of @floatCast
.float => try w.print("{d}", .{@as(f64, @floatCast(v.data.float))}),
else => try w.print("({s})", .{@tagName(v.tag)}),
}
}