mirror of
https://github.com/roc-lang/roc.git
synced 2025-08-04 12:18:19 +00:00
remove dict/hash stuff from the zig builtins
This commit is contained in:
parent
4d55b756bb
commit
6c26d8812f
8 changed files with 3 additions and 1737 deletions
|
@ -1,815 +0,0 @@
|
|||
const std = @import("std");
|
||||
const testing = std.testing;
|
||||
const expectEqual = testing.expectEqual;
|
||||
const mem = std.mem;
|
||||
const assert = std.debug.assert;
|
||||
|
||||
const utils = @import("utils.zig");
|
||||
const RocList = @import("list.zig").RocList;
|
||||
|
||||
const INITIAL_SEED = 0xc70f6907;
|
||||
|
||||
const InPlace = enum(u8) {
|
||||
InPlace,
|
||||
Clone,
|
||||
};
|
||||
|
||||
const Slot = enum(u8) {
|
||||
Empty,
|
||||
Filled,
|
||||
PreviouslyFilled,
|
||||
};
|
||||
|
||||
const MaybeIndexTag = enum { index, not_found };
|
||||
|
||||
const MaybeIndex = union(MaybeIndexTag) { index: usize, not_found: void };
|
||||
|
||||
fn nextSeed(seed: u64) u64 {
|
||||
// TODO is this a valid way to get a new seed? are there better ways?
|
||||
return seed + 1;
|
||||
}
|
||||
|
||||
fn totalCapacityAtLevel(input: usize) usize {
|
||||
if (input == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var n = input;
|
||||
var slots: usize = 8;
|
||||
|
||||
while (n > 1) : (n -= 1) {
|
||||
slots = slots * 2 + slots;
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
fn capacityOfLevel(input: usize) usize {
|
||||
if (input == 0) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
var n = input;
|
||||
var slots: usize = 8;
|
||||
|
||||
while (n > 1) : (n -= 1) {
|
||||
slots = slots * 2;
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
// aligmnent of elements. The number (16 or 8) indicates the maximum
|
||||
// alignment of the key and value. The tag furthermore indicates
|
||||
// which has the biggest aligmnent. If both are the same, we put
|
||||
// the key first
|
||||
const Alignment = extern struct {
|
||||
bits: u8,
|
||||
|
||||
const VALUE_BEFORE_KEY_FLAG: u8 = 0b1000_0000;
|
||||
|
||||
fn toU32(self: Alignment) u32 {
|
||||
if (self.bits >= VALUE_BEFORE_KEY_FLAG) {
|
||||
return self.bits ^ Alignment.VALUE_BEFORE_KEY_FLAG;
|
||||
} else {
|
||||
return self.bits;
|
||||
}
|
||||
}
|
||||
|
||||
fn keyFirst(self: Alignment) bool {
|
||||
if (self.bits & Alignment.VALUE_BEFORE_KEY_FLAG > 0) {
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
pub fn decref(
|
||||
bytes_or_null: ?[*]u8,
|
||||
data_bytes: usize,
|
||||
alignment: Alignment,
|
||||
) void {
|
||||
return utils.decref(bytes_or_null, data_bytes, alignment.toU32());
|
||||
}
|
||||
|
||||
pub fn allocateWithRefcount(
|
||||
data_bytes: usize,
|
||||
alignment: Alignment,
|
||||
) [*]u8 {
|
||||
return utils.allocateWithRefcount(data_bytes, alignment.toU32());
|
||||
}
|
||||
|
||||
pub const RocDict = extern struct {
|
||||
dict_bytes: ?[*]u8,
|
||||
dict_entries_len: usize,
|
||||
number_of_levels: usize,
|
||||
|
||||
pub fn empty() RocDict {
|
||||
return RocDict{
|
||||
.dict_entries_len = 0,
|
||||
.number_of_levels = 0,
|
||||
.dict_bytes = null,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn allocate(
|
||||
number_of_levels: usize,
|
||||
number_of_entries: usize,
|
||||
alignment: Alignment,
|
||||
key_size: usize,
|
||||
value_size: usize,
|
||||
) RocDict {
|
||||
const number_of_slots = totalCapacityAtLevel(number_of_levels);
|
||||
const slot_size = slotSize(key_size, value_size);
|
||||
const data_bytes = number_of_slots * slot_size;
|
||||
|
||||
return RocDict{
|
||||
.dict_bytes = allocateWithRefcount(data_bytes, alignment),
|
||||
.number_of_levels = number_of_levels,
|
||||
.dict_entries_len = number_of_entries,
|
||||
};
|
||||
}
|
||||
|
||||
pub fn reallocate(
|
||||
self: RocDict,
|
||||
alignment: Alignment,
|
||||
key_width: usize,
|
||||
value_width: usize,
|
||||
) RocDict {
|
||||
const new_level = self.number_of_levels + 1;
|
||||
const slot_size = slotSize(key_width, value_width);
|
||||
|
||||
const old_capacity = self.capacity();
|
||||
const new_capacity = totalCapacityAtLevel(new_level);
|
||||
const delta_capacity = new_capacity - old_capacity;
|
||||
|
||||
const data_bytes = new_capacity * slot_size;
|
||||
const first_slot = allocateWithRefcount(data_bytes, alignment);
|
||||
|
||||
// transfer the memory
|
||||
|
||||
if (self.dict_bytes) |source_ptr| {
|
||||
const dest_ptr = first_slot;
|
||||
|
||||
var source_offset: usize = 0;
|
||||
var dest_offset: usize = 0;
|
||||
|
||||
if (alignment.keyFirst()) {
|
||||
// copy keys
|
||||
@memcpy(dest_ptr + dest_offset, source_ptr + source_offset, old_capacity * key_width);
|
||||
|
||||
// copy values
|
||||
source_offset = old_capacity * key_width;
|
||||
dest_offset = new_capacity * key_width;
|
||||
@memcpy(dest_ptr + dest_offset, source_ptr + source_offset, old_capacity * value_width);
|
||||
} else {
|
||||
// copy values
|
||||
@memcpy(dest_ptr + dest_offset, source_ptr + source_offset, old_capacity * value_width);
|
||||
|
||||
// copy keys
|
||||
source_offset = old_capacity * value_width;
|
||||
dest_offset = new_capacity * value_width;
|
||||
@memcpy(dest_ptr + dest_offset, source_ptr + source_offset, old_capacity * key_width);
|
||||
}
|
||||
|
||||
// copy slots
|
||||
source_offset = old_capacity * (key_width + value_width);
|
||||
dest_offset = new_capacity * (key_width + value_width);
|
||||
@memcpy(dest_ptr + dest_offset, source_ptr + source_offset, old_capacity * @sizeOf(Slot));
|
||||
}
|
||||
|
||||
var i: usize = 0;
|
||||
const first_new_slot_value = first_slot + old_capacity * slot_size + delta_capacity * (key_width + value_width);
|
||||
while (i < (new_capacity - old_capacity)) : (i += 1) {
|
||||
(first_new_slot_value)[i] = @enumToInt(Slot.Empty);
|
||||
}
|
||||
|
||||
const result = RocDict{
|
||||
.dict_bytes = first_slot,
|
||||
.number_of_levels = self.number_of_levels + 1,
|
||||
.dict_entries_len = self.dict_entries_len,
|
||||
};
|
||||
|
||||
// NOTE we fuse an increment of all keys/values with a decrement of the input dict
|
||||
decref(self.dict_bytes, self.capacity() * slotSize(key_width, value_width), alignment);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
pub fn asU8ptr(self: RocDict) [*]u8 {
|
||||
return @ptrCast([*]u8, self.dict_bytes);
|
||||
}
|
||||
|
||||
pub fn len(self: RocDict) usize {
|
||||
return self.dict_entries_len;
|
||||
}
|
||||
|
||||
pub fn isEmpty(self: RocDict) bool {
|
||||
return self.len() == 0;
|
||||
}
|
||||
|
||||
pub fn isUnique(self: RocDict) bool {
|
||||
// the empty dict is unique (in the sense that copying it will not leak memory)
|
||||
if (self.isEmpty()) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// otherwise, check if the refcount is one
|
||||
const ptr: [*]usize = @ptrCast([*]usize, @alignCast(@alignOf(usize), self.dict_bytes));
|
||||
return (ptr - 1)[0] == utils.REFCOUNT_ONE;
|
||||
}
|
||||
|
||||
pub fn capacity(self: RocDict) usize {
|
||||
return totalCapacityAtLevel(self.number_of_levels);
|
||||
}
|
||||
|
||||
pub fn makeUnique(self: RocDict, alignment: Alignment, key_width: usize, value_width: usize) RocDict {
|
||||
if (self.isEmpty()) {
|
||||
return self;
|
||||
}
|
||||
|
||||
if (self.isUnique()) {
|
||||
return self;
|
||||
}
|
||||
|
||||
// unfortunately, we have to clone
|
||||
var new_dict = RocDict.allocate(self.number_of_levels, self.dict_entries_len, alignment, key_width, value_width);
|
||||
|
||||
var old_bytes: [*]u8 = @ptrCast([*]u8, self.dict_bytes);
|
||||
var new_bytes: [*]u8 = @ptrCast([*]u8, new_dict.dict_bytes);
|
||||
|
||||
const number_of_bytes = self.capacity() * (@sizeOf(Slot) + key_width + value_width);
|
||||
@memcpy(new_bytes, old_bytes, number_of_bytes);
|
||||
|
||||
// NOTE we fuse an increment of all keys/values with a decrement of the input dict
|
||||
const data_bytes = self.capacity() * slotSize(key_width, value_width);
|
||||
decref(self.dict_bytes, data_bytes, alignment);
|
||||
|
||||
return new_dict;
|
||||
}
|
||||
|
||||
fn getSlot(self: *const RocDict, index: usize, key_width: usize, value_width: usize) Slot {
|
||||
const offset = self.capacity() * (key_width + value_width) + index * @sizeOf(Slot);
|
||||
|
||||
const ptr = self.dict_bytes orelse unreachable;
|
||||
return @intToEnum(Slot, ptr[offset]);
|
||||
}
|
||||
|
||||
fn setSlot(self: *RocDict, index: usize, key_width: usize, value_width: usize, slot: Slot) void {
|
||||
const offset = self.capacity() * (key_width + value_width) + index * @sizeOf(Slot);
|
||||
|
||||
const ptr = self.dict_bytes orelse unreachable;
|
||||
ptr[offset] = @enumToInt(slot);
|
||||
}
|
||||
|
||||
fn setKey(self: *RocDict, index: usize, alignment: Alignment, key_width: usize, value_width: usize, data: Opaque) void {
|
||||
if (key_width == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = blk: {
|
||||
if (alignment.keyFirst()) {
|
||||
break :blk (index * key_width);
|
||||
} else {
|
||||
break :blk (self.capacity() * value_width) + (index * key_width);
|
||||
}
|
||||
};
|
||||
|
||||
const ptr = self.dict_bytes orelse unreachable;
|
||||
|
||||
const source = data orelse unreachable;
|
||||
const dest = ptr + offset;
|
||||
@memcpy(dest, source, key_width);
|
||||
}
|
||||
|
||||
fn getKey(self: *const RocDict, index: usize, alignment: Alignment, key_width: usize, value_width: usize) Opaque {
|
||||
if (key_width == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const offset = blk: {
|
||||
if (alignment.keyFirst()) {
|
||||
break :blk (index * key_width);
|
||||
} else {
|
||||
break :blk (self.capacity() * value_width) + (index * key_width);
|
||||
}
|
||||
};
|
||||
|
||||
const ptr = self.dict_bytes orelse unreachable;
|
||||
return ptr + offset;
|
||||
}
|
||||
|
||||
fn setValue(self: *RocDict, index: usize, alignment: Alignment, key_width: usize, value_width: usize, data: Opaque) void {
|
||||
if (value_width == 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const offset = blk: {
|
||||
if (alignment.keyFirst()) {
|
||||
break :blk (self.capacity() * key_width) + (index * value_width);
|
||||
} else {
|
||||
break :blk (index * value_width);
|
||||
}
|
||||
};
|
||||
|
||||
const ptr = self.dict_bytes orelse unreachable;
|
||||
|
||||
const source = data orelse unreachable;
|
||||
const dest = ptr + offset;
|
||||
@memcpy(dest, source, value_width);
|
||||
}
|
||||
|
||||
fn getValue(self: *const RocDict, index: usize, alignment: Alignment, key_width: usize, value_width: usize) Opaque {
|
||||
if (value_width == 0) {
|
||||
return null;
|
||||
}
|
||||
|
||||
const offset = blk: {
|
||||
if (alignment.keyFirst()) {
|
||||
break :blk (self.capacity() * key_width) + (index * value_width);
|
||||
} else {
|
||||
break :blk (index * value_width);
|
||||
}
|
||||
};
|
||||
|
||||
const ptr = self.dict_bytes orelse unreachable;
|
||||
return ptr + offset;
|
||||
}
|
||||
|
||||
fn findIndex(self: *const RocDict, alignment: Alignment, key: Opaque, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn) MaybeIndex {
|
||||
if (self.isEmpty()) {
|
||||
return MaybeIndex.not_found;
|
||||
}
|
||||
|
||||
var seed: u64 = INITIAL_SEED;
|
||||
|
||||
var current_level: usize = 1;
|
||||
var current_level_size: usize = 8;
|
||||
var next_level_size: usize = 2 * current_level_size;
|
||||
|
||||
while (true) {
|
||||
if (current_level > self.number_of_levels) {
|
||||
return MaybeIndex.not_found;
|
||||
}
|
||||
|
||||
// hash the key, and modulo by the maximum size
|
||||
// (so we get an in-bounds index)
|
||||
const hash = hash_fn(seed, key);
|
||||
const index = capacityOfLevel(current_level - 1) + @intCast(usize, (hash % current_level_size));
|
||||
|
||||
switch (self.getSlot(index, key_width, value_width)) {
|
||||
Slot.Empty, Slot.PreviouslyFilled => {
|
||||
return MaybeIndex.not_found;
|
||||
},
|
||||
Slot.Filled => {
|
||||
// is this the same key, or a new key?
|
||||
const current_key = self.getKey(index, alignment, key_width, value_width);
|
||||
|
||||
if (is_eq(key, current_key)) {
|
||||
return MaybeIndex{ .index = index };
|
||||
} else {
|
||||
current_level += 1;
|
||||
current_level_size *= 2;
|
||||
next_level_size *= 2;
|
||||
|
||||
seed = nextSeed(seed);
|
||||
continue;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
// Dict.empty
|
||||
pub fn dictEmpty(dict: *RocDict) callconv(.C) void {
|
||||
dict.* = RocDict.empty();
|
||||
}
|
||||
|
||||
pub fn slotSize(key_size: usize, value_size: usize) usize {
|
||||
return @sizeOf(Slot) + key_size + value_size;
|
||||
}
|
||||
|
||||
// Dict.len
|
||||
pub fn dictLen(dict: RocDict) callconv(.C) usize {
|
||||
return dict.dict_entries_len;
|
||||
}
|
||||
|
||||
// commonly used type aliases
|
||||
const Opaque = ?[*]u8;
|
||||
const HashFn = fn (u64, ?[*]u8) callconv(.C) u64;
|
||||
const EqFn = fn (?[*]u8, ?[*]u8) callconv(.C) bool;
|
||||
|
||||
const Inc = fn (?[*]u8) callconv(.C) void;
|
||||
const IncN = fn (?[*]u8, usize) callconv(.C) void;
|
||||
const Dec = fn (?[*]u8) callconv(.C) void;
|
||||
|
||||
const Caller3 = fn (?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8, ?[*]u8) callconv(.C) void;
|
||||
|
||||
// Dict.insert : Dict k v, k, v -> Dict k v
|
||||
pub fn dictInsert(
|
||||
input: RocDict,
|
||||
alignment: Alignment,
|
||||
key: Opaque,
|
||||
key_width: usize,
|
||||
value: Opaque,
|
||||
value_width: usize,
|
||||
hash_fn: HashFn,
|
||||
is_eq: EqFn,
|
||||
dec_key: Dec,
|
||||
dec_value: Dec,
|
||||
output: *RocDict,
|
||||
) callconv(.C) void {
|
||||
var seed: u64 = INITIAL_SEED;
|
||||
|
||||
var result = input.makeUnique(alignment, key_width, value_width);
|
||||
|
||||
var current_level: usize = 1;
|
||||
var current_level_size: usize = 8;
|
||||
var next_level_size: usize = 2 * current_level_size;
|
||||
|
||||
while (true) {
|
||||
if (current_level > result.number_of_levels) {
|
||||
result = result.reallocate(alignment, key_width, value_width);
|
||||
}
|
||||
|
||||
const hash = hash_fn(seed, key);
|
||||
const index = capacityOfLevel(current_level - 1) + @intCast(usize, (hash % current_level_size));
|
||||
assert(index < result.capacity());
|
||||
|
||||
switch (result.getSlot(index, key_width, value_width)) {
|
||||
Slot.Empty, Slot.PreviouslyFilled => {
|
||||
result.setSlot(index, key_width, value_width, Slot.Filled);
|
||||
result.setKey(index, alignment, key_width, value_width, key);
|
||||
result.setValue(index, alignment, key_width, value_width, value);
|
||||
|
||||
result.dict_entries_len += 1;
|
||||
break;
|
||||
},
|
||||
Slot.Filled => {
|
||||
// is this the same key, or a new key?
|
||||
const current_key = result.getKey(index, alignment, key_width, value_width);
|
||||
|
||||
if (is_eq(key, current_key)) {
|
||||
// we will override the old value, but first have to decrement its refcount
|
||||
const current_value = result.getValue(index, alignment, key_width, value_width);
|
||||
dec_value(current_value);
|
||||
|
||||
// we must consume the key argument!
|
||||
dec_key(key);
|
||||
|
||||
result.setValue(index, alignment, key_width, value_width, value);
|
||||
break;
|
||||
} else {
|
||||
seed = nextSeed(seed);
|
||||
|
||||
current_level += 1;
|
||||
current_level_size *= 2;
|
||||
next_level_size *= 2;
|
||||
|
||||
continue;
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// write result into pointer
|
||||
output.* = result;
|
||||
}
|
||||
|
||||
// Dict.remove : Dict k v, k -> Dict k v
|
||||
pub fn dictRemove(input: RocDict, alignment: Alignment, key: Opaque, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn, dec_key: Dec, dec_value: Dec, output: *RocDict) callconv(.C) void {
|
||||
switch (input.findIndex(alignment, key, key_width, value_width, hash_fn, is_eq)) {
|
||||
MaybeIndex.not_found => {
|
||||
// the key was not found; we're done
|
||||
output.* = input;
|
||||
return;
|
||||
},
|
||||
MaybeIndex.index => |index| {
|
||||
var dict = input.makeUnique(alignment, key_width, value_width);
|
||||
|
||||
assert(index < dict.capacity());
|
||||
|
||||
dict.setSlot(index, key_width, value_width, Slot.PreviouslyFilled);
|
||||
const old_key = dict.getKey(index, alignment, key_width, value_width);
|
||||
const old_value = dict.getValue(index, alignment, key_width, value_width);
|
||||
|
||||
dec_key(old_key);
|
||||
dec_value(old_value);
|
||||
dict.dict_entries_len -= 1;
|
||||
|
||||
// if the dict is now completely empty, free its allocation
|
||||
if (dict.dict_entries_len == 0) {
|
||||
const data_bytes = dict.capacity() * slotSize(key_width, value_width);
|
||||
decref(dict.dict_bytes, data_bytes, alignment);
|
||||
output.* = RocDict.empty();
|
||||
return;
|
||||
}
|
||||
|
||||
output.* = dict;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Dict.contains : Dict k v, k -> Bool
|
||||
pub fn dictContains(dict: RocDict, alignment: Alignment, key: Opaque, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn) callconv(.C) bool {
|
||||
switch (dict.findIndex(alignment, key, key_width, value_width, hash_fn, is_eq)) {
|
||||
MaybeIndex.not_found => {
|
||||
return false;
|
||||
},
|
||||
MaybeIndex.index => |_| {
|
||||
return true;
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Dict.get : Dict k v, k -> { flag: bool, value: Opaque }
|
||||
pub fn dictGet(dict: RocDict, alignment: Alignment, key: Opaque, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn, inc_value: Inc) callconv(.C) extern struct { value: Opaque, flag: bool } {
|
||||
switch (dict.findIndex(alignment, key, key_width, value_width, hash_fn, is_eq)) {
|
||||
MaybeIndex.not_found => {
|
||||
return .{ .flag = false, .value = null };
|
||||
},
|
||||
MaybeIndex.index => |index| {
|
||||
var value = dict.getValue(index, alignment, key_width, value_width);
|
||||
inc_value(value);
|
||||
return .{ .flag = true, .value = value };
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Dict.elementsRc
|
||||
// increment or decrement all dict elements (but not the dict's allocation itself)
|
||||
pub fn elementsRc(dict: RocDict, alignment: Alignment, key_width: usize, value_width: usize, modify_key: Inc, modify_value: Inc) callconv(.C) void {
|
||||
const size = dict.capacity();
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < size) : (i += 1) {
|
||||
switch (dict.getSlot(i, key_width, value_width)) {
|
||||
Slot.Filled => {
|
||||
modify_key(dict.getKey(i, alignment, key_width, value_width));
|
||||
modify_value(dict.getValue(i, alignment, key_width, value_width));
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dictKeys(
|
||||
dict: RocDict,
|
||||
alignment: Alignment,
|
||||
key_width: usize,
|
||||
value_width: usize,
|
||||
inc_key: Inc,
|
||||
) callconv(.C) RocList {
|
||||
const size = dict.capacity();
|
||||
|
||||
var length: usize = 0;
|
||||
var i: usize = 0;
|
||||
while (i < size) : (i += 1) {
|
||||
switch (dict.getSlot(i, key_width, value_width)) {
|
||||
Slot.Filled => {
|
||||
length += 1;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
if (length == 0) {
|
||||
return RocList.empty();
|
||||
}
|
||||
|
||||
const data_bytes = length * key_width;
|
||||
var ptr = allocateWithRefcount(data_bytes, alignment);
|
||||
|
||||
i = 0;
|
||||
var copied: usize = 0;
|
||||
while (i < size) : (i += 1) {
|
||||
switch (dict.getSlot(i, key_width, value_width)) {
|
||||
Slot.Filled => {
|
||||
const key = dict.getKey(i, alignment, key_width, value_width);
|
||||
inc_key(key);
|
||||
|
||||
const key_cast = @ptrCast([*]const u8, key);
|
||||
@memcpy(ptr + (copied * key_width), key_cast, key_width);
|
||||
copied += 1;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
return RocList{ .bytes = ptr, .length = length, .capacity = length };
|
||||
}
|
||||
|
||||
pub fn dictValues(
|
||||
dict: RocDict,
|
||||
alignment: Alignment,
|
||||
key_width: usize,
|
||||
value_width: usize,
|
||||
inc_value: Inc,
|
||||
) callconv(.C) RocList {
|
||||
const size = dict.capacity();
|
||||
|
||||
var length: usize = 0;
|
||||
var i: usize = 0;
|
||||
while (i < size) : (i += 1) {
|
||||
switch (dict.getSlot(i, key_width, value_width)) {
|
||||
Slot.Filled => {
|
||||
length += 1;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
if (length == 0) {
|
||||
return RocList.empty();
|
||||
}
|
||||
|
||||
const data_bytes = length * value_width;
|
||||
var ptr = allocateWithRefcount(data_bytes, alignment);
|
||||
|
||||
i = 0;
|
||||
var copied: usize = 0;
|
||||
while (i < size) : (i += 1) {
|
||||
switch (dict.getSlot(i, key_width, value_width)) {
|
||||
Slot.Filled => {
|
||||
const value = dict.getValue(i, alignment, key_width, value_width);
|
||||
inc_value(value);
|
||||
|
||||
const value_cast = @ptrCast([*]const u8, value);
|
||||
@memcpy(ptr + (copied * value_width), value_cast, value_width);
|
||||
copied += 1;
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
return RocList{ .bytes = ptr, .length = length, .capacity = length };
|
||||
}
|
||||
|
||||
fn doNothing(_: Opaque) callconv(.C) void {
|
||||
return;
|
||||
}
|
||||
|
||||
pub fn dictUnion(
|
||||
dict1: RocDict,
|
||||
dict2: RocDict,
|
||||
alignment: Alignment,
|
||||
key_width: usize,
|
||||
value_width: usize,
|
||||
hash_fn: HashFn,
|
||||
is_eq: EqFn,
|
||||
inc_key: Inc,
|
||||
inc_value: Inc,
|
||||
output: *RocDict,
|
||||
) callconv(.C) void {
|
||||
output.* = dict1.makeUnique(alignment, key_width, value_width);
|
||||
|
||||
var i: usize = 0;
|
||||
while (i < dict2.capacity()) : (i += 1) {
|
||||
switch (dict2.getSlot(i, key_width, value_width)) {
|
||||
Slot.Filled => {
|
||||
const key = dict2.getKey(i, alignment, key_width, value_width);
|
||||
|
||||
switch (output.findIndex(alignment, key, key_width, value_width, hash_fn, is_eq)) {
|
||||
MaybeIndex.not_found => {
|
||||
const value = dict2.getValue(i, alignment, key_width, value_width);
|
||||
inc_value(value);
|
||||
|
||||
// we need an extra RC token for the key
|
||||
inc_key(key);
|
||||
inc_value(value);
|
||||
|
||||
// we know the newly added key is not a duplicate, so the `dec`s are unreachable
|
||||
const dec_key = doNothing;
|
||||
const dec_value = doNothing;
|
||||
|
||||
dictInsert(output.*, alignment, key, key_width, value, value_width, hash_fn, is_eq, dec_key, dec_value, output);
|
||||
},
|
||||
MaybeIndex.index => |_| {
|
||||
// the key is already in the output dict
|
||||
continue;
|
||||
},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dictIntersection(dict1: RocDict, dict2: RocDict, alignment: Alignment, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn, dec_key: Inc, dec_value: Inc, output: *RocDict) callconv(.C) void {
|
||||
output.* = dict1.makeUnique(alignment, key_width, value_width);
|
||||
|
||||
var i: usize = 0;
|
||||
const size = dict1.capacity();
|
||||
while (i < size) : (i += 1) {
|
||||
switch (output.getSlot(i, key_width, value_width)) {
|
||||
Slot.Filled => {
|
||||
const key = dict1.getKey(i, alignment, key_width, value_width);
|
||||
|
||||
switch (dict2.findIndex(alignment, key, key_width, value_width, hash_fn, is_eq)) {
|
||||
MaybeIndex.not_found => {
|
||||
dictRemove(output.*, alignment, key, key_width, value_width, hash_fn, is_eq, dec_key, dec_value, output);
|
||||
},
|
||||
MaybeIndex.index => |_| {
|
||||
// keep this key/value
|
||||
continue;
|
||||
},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn dictDifference(dict1: RocDict, dict2: RocDict, alignment: Alignment, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn, dec_key: Dec, dec_value: Dec, output: *RocDict) callconv(.C) void {
|
||||
output.* = dict1.makeUnique(alignment, key_width, value_width);
|
||||
|
||||
var i: usize = 0;
|
||||
const size = dict1.capacity();
|
||||
while (i < size) : (i += 1) {
|
||||
switch (output.getSlot(i, key_width, value_width)) {
|
||||
Slot.Filled => {
|
||||
const key = dict1.getKey(i, alignment, key_width, value_width);
|
||||
|
||||
switch (dict2.findIndex(alignment, key, key_width, value_width, hash_fn, is_eq)) {
|
||||
MaybeIndex.not_found => {
|
||||
// keep this key/value
|
||||
continue;
|
||||
},
|
||||
MaybeIndex.index => |_| {
|
||||
dictRemove(output.*, alignment, key, key_width, value_width, hash_fn, is_eq, dec_key, dec_value, output);
|
||||
},
|
||||
}
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn setFromList(list: RocList, alignment: Alignment, key_width: usize, value_width: usize, hash_fn: HashFn, is_eq: EqFn, dec_key: Dec, output: *RocDict) callconv(.C) void {
|
||||
output.* = RocDict.empty();
|
||||
|
||||
var ptr = @ptrCast([*]u8, list.bytes);
|
||||
|
||||
const dec_value = doNothing;
|
||||
const value = null;
|
||||
|
||||
const size = list.length;
|
||||
var i: usize = 0;
|
||||
while (i < size) : (i += 1) {
|
||||
const key = ptr + i * key_width;
|
||||
dictInsert(output.*, alignment, key, key_width, value, value_width, hash_fn, is_eq, dec_key, dec_value, output);
|
||||
}
|
||||
|
||||
// NOTE: decref checks for the empty case
|
||||
const data_bytes = size * key_width;
|
||||
decref(list.bytes, data_bytes, alignment);
|
||||
}
|
||||
|
||||
pub fn dictWalk(
|
||||
dict: RocDict,
|
||||
caller: Caller3,
|
||||
data: Opaque,
|
||||
inc_n_data: IncN,
|
||||
data_is_owned: bool,
|
||||
accum: Opaque,
|
||||
alignment: Alignment,
|
||||
key_width: usize,
|
||||
value_width: usize,
|
||||
accum_width: usize,
|
||||
output: Opaque,
|
||||
) callconv(.C) void {
|
||||
const alignment_u32 = alignment.toU32();
|
||||
// allocate space to write the result of the stepper into
|
||||
// experimentally aliasing the accum and output pointers is not a good idea
|
||||
// TODO handle alloc failing!
|
||||
const bytes_ptr: [*]u8 = utils.alloc(accum_width, alignment_u32) orelse unreachable;
|
||||
var b1 = output orelse unreachable;
|
||||
var b2 = bytes_ptr;
|
||||
|
||||
if (data_is_owned) {
|
||||
inc_n_data(data, dict.len());
|
||||
}
|
||||
|
||||
@memcpy(b2, accum orelse unreachable, accum_width);
|
||||
|
||||
var i: usize = 0;
|
||||
const size = dict.capacity();
|
||||
while (i < size) : (i += 1) {
|
||||
switch (dict.getSlot(i, key_width, value_width)) {
|
||||
Slot.Filled => {
|
||||
const key = dict.getKey(i, alignment, key_width, value_width);
|
||||
const value = dict.getValue(i, alignment, key_width, value_width);
|
||||
|
||||
caller(data, b2, key, value, b1);
|
||||
|
||||
std.mem.swap([*]u8, &b1, &b2);
|
||||
},
|
||||
else => {},
|
||||
}
|
||||
}
|
||||
|
||||
@memcpy(output orelse unreachable, b2, accum_width);
|
||||
utils.dealloc(bytes_ptr, alignment_u32);
|
||||
}
|
|
@ -53,31 +53,6 @@ comptime {
|
|||
exportListFn(list.listIsUnique, "is_unique");
|
||||
}
|
||||
|
||||
// Dict Module
|
||||
const dict = @import("dict.zig");
|
||||
const hash = @import("hash.zig");
|
||||
|
||||
comptime {
|
||||
exportDictFn(dict.dictLen, "len");
|
||||
exportDictFn(dict.dictEmpty, "empty");
|
||||
exportDictFn(dict.dictInsert, "insert");
|
||||
exportDictFn(dict.dictRemove, "remove");
|
||||
exportDictFn(dict.dictContains, "contains");
|
||||
exportDictFn(dict.dictGet, "get");
|
||||
exportDictFn(dict.elementsRc, "elementsRc");
|
||||
exportDictFn(dict.dictKeys, "keys");
|
||||
exportDictFn(dict.dictValues, "values");
|
||||
exportDictFn(dict.dictUnion, "union");
|
||||
exportDictFn(dict.dictIntersection, "intersection");
|
||||
exportDictFn(dict.dictDifference, "difference");
|
||||
exportDictFn(dict.dictWalk, "walk");
|
||||
|
||||
exportDictFn(dict.setFromList, "set_from_list");
|
||||
|
||||
exportDictFn(hash.wyhash, "hash");
|
||||
exportDictFn(hash.wyhash_rocstr, "hash_str");
|
||||
}
|
||||
|
||||
// Num Module
|
||||
const num = @import("num.zig");
|
||||
|
||||
|
|
|
@ -336,24 +336,6 @@ pub const STR_RESERVE: &str = "roc_builtins.str.reserve";
|
|||
pub const STR_APPEND_SCALAR: &str = "roc_builtins.str.append_scalar";
|
||||
pub const STR_GET_SCALAR_UNSAFE: &str = "roc_builtins.str.get_scalar_unsafe";
|
||||
|
||||
pub const DICT_HASH: &str = "roc_builtins.dict.hash";
|
||||
pub const DICT_HASH_STR: &str = "roc_builtins.dict.hash_str";
|
||||
pub const DICT_LEN: &str = "roc_builtins.dict.len";
|
||||
pub const DICT_EMPTY: &str = "roc_builtins.dict.empty";
|
||||
pub const DICT_INSERT: &str = "roc_builtins.dict.insert";
|
||||
pub const DICT_REMOVE: &str = "roc_builtins.dict.remove";
|
||||
pub const DICT_CONTAINS: &str = "roc_builtins.dict.contains";
|
||||
pub const DICT_GET: &str = "roc_builtins.dict.get";
|
||||
pub const DICT_ELEMENTS_RC: &str = "roc_builtins.dict.elementsRc";
|
||||
pub const DICT_KEYS: &str = "roc_builtins.dict.keys";
|
||||
pub const DICT_VALUES: &str = "roc_builtins.dict.values";
|
||||
pub const DICT_UNION: &str = "roc_builtins.dict.union";
|
||||
pub const DICT_DIFFERENCE: &str = "roc_builtins.dict.difference";
|
||||
pub const DICT_INTERSECTION: &str = "roc_builtins.dict.intersection";
|
||||
pub const DICT_WALK: &str = "roc_builtins.dict.walk";
|
||||
|
||||
pub const SET_FROM_LIST: &str = "roc_builtins.dict.set_from_list";
|
||||
|
||||
pub const LIST_MAP: &str = "roc_builtins.list.map";
|
||||
pub const LIST_MAP2: &str = "roc_builtins.list.map2";
|
||||
pub const LIST_MAP3: &str = "roc_builtins.list.map3";
|
||||
|
|
|
@ -2,7 +2,6 @@ use crate::llvm::bitcode::{
|
|||
call_bitcode_fn, call_bitcode_fn_fixing_for_convention, call_list_bitcode_fn,
|
||||
call_str_bitcode_fn, call_void_bitcode_fn,
|
||||
};
|
||||
use crate::llvm::build_hash::generic_hash;
|
||||
use crate::llvm::build_list::{
|
||||
self, allocate_list, empty_polymorphic_list, list_append_unsafe, list_concat, list_drop_at,
|
||||
list_get_unsafe, list_len, list_map, list_map2, list_map3, list_map4, list_prepend,
|
||||
|
@ -6066,13 +6065,7 @@ fn run_low_level<'a, 'ctx, 'env>(
|
|||
BasicValueEnum::IntValue(bool_val)
|
||||
}
|
||||
Hash => {
|
||||
debug_assert_eq!(args.len(), 2);
|
||||
let seed = load_symbol(scope, &args[0]);
|
||||
let (value, layout) = load_symbol_and_layout(scope, &args[1]);
|
||||
|
||||
debug_assert!(seed.is_int_value());
|
||||
|
||||
generic_hash(env, layout_ids, seed.into_int_value(), value, layout).into()
|
||||
unimplemented!()
|
||||
}
|
||||
|
||||
ListMap | ListMap2 | ListMap3 | ListMap4 | ListSortWith => {
|
||||
|
|
|
@ -1,868 +0,0 @@
|
|||
use crate::debug_info_init;
|
||||
use crate::llvm::bitcode::call_bitcode_fn;
|
||||
use crate::llvm::build::tag_pointer_clear_tag_id;
|
||||
use crate::llvm::build::Env;
|
||||
use crate::llvm::build::{get_tag_id, FAST_CALL_CONV, TAG_DATA_INDEX};
|
||||
use crate::llvm::convert::basic_type_from_layout;
|
||||
use bumpalo::collections::Vec;
|
||||
use inkwell::values::{
|
||||
BasicValue, BasicValueEnum, FunctionValue, IntValue, PointerValue, StructValue,
|
||||
};
|
||||
use roc_builtins::bitcode;
|
||||
use roc_module::symbol::Symbol;
|
||||
use roc_mono::layout::{Builtin, Layout, LayoutIds, UnionLayout};
|
||||
|
||||
use super::build::use_roc_value;
|
||||
use super::convert::argument_type_from_union_layout;
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum WhenRecursive<'a> {
|
||||
Unreachable,
|
||||
Loop(UnionLayout<'a>),
|
||||
}
|
||||
|
||||
pub fn generic_hash<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
seed: IntValue<'ctx>,
|
||||
val: BasicValueEnum<'ctx>,
|
||||
layout: &Layout<'a>,
|
||||
) -> IntValue<'ctx> {
|
||||
// NOTE: C and Zig use this value for their initial HashMap seed: 0xc70f6907
|
||||
build_hash_layout(
|
||||
env,
|
||||
layout_ids,
|
||||
seed,
|
||||
val,
|
||||
layout,
|
||||
WhenRecursive::Unreachable,
|
||||
)
|
||||
}
|
||||
|
||||
fn build_hash_layout<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
seed: IntValue<'ctx>,
|
||||
val: BasicValueEnum<'ctx>,
|
||||
layout: &Layout<'a>,
|
||||
when_recursive: WhenRecursive<'a>,
|
||||
) -> IntValue<'ctx> {
|
||||
match layout {
|
||||
Layout::Builtin(builtin) => {
|
||||
hash_builtin(env, layout_ids, seed, val, layout, builtin, when_recursive)
|
||||
}
|
||||
|
||||
Layout::Struct { field_layouts, .. } => build_hash_struct(
|
||||
env,
|
||||
layout_ids,
|
||||
field_layouts,
|
||||
when_recursive,
|
||||
seed,
|
||||
val.into_struct_value(),
|
||||
),
|
||||
|
||||
Layout::LambdaSet(lambda_set) => build_hash_layout(
|
||||
env,
|
||||
layout_ids,
|
||||
seed,
|
||||
val,
|
||||
&lambda_set.runtime_representation(),
|
||||
when_recursive,
|
||||
),
|
||||
|
||||
Layout::Union(union_layout) => build_hash_tag(env, layout_ids, union_layout, seed, val),
|
||||
|
||||
Layout::Boxed(_inner_layout) => {
|
||||
// build_hash_box(env, layout_ids, layout, inner_layout, seed, val)
|
||||
todo!()
|
||||
}
|
||||
|
||||
Layout::RecursivePointer => match when_recursive {
|
||||
WhenRecursive::Unreachable => {
|
||||
unreachable!("recursion pointers should never be hashed directly")
|
||||
}
|
||||
WhenRecursive::Loop(union_layout) => {
|
||||
let layout = Layout::Union(union_layout);
|
||||
|
||||
let bt = basic_type_from_layout(env, &layout);
|
||||
|
||||
// cast the i64 pointer to a pointer to block of memory
|
||||
let field_cast = env
|
||||
.builder
|
||||
.build_bitcast(val, bt, "i64_to_opaque")
|
||||
.into_pointer_value();
|
||||
|
||||
build_hash_tag(env, layout_ids, &union_layout, seed, field_cast.into())
|
||||
}
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
fn append_hash_layout<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
seed: IntValue<'ctx>,
|
||||
val: BasicValueEnum<'ctx>,
|
||||
layout: &Layout<'a>,
|
||||
when_recursive: WhenRecursive<'a>,
|
||||
) -> IntValue<'ctx> {
|
||||
build_hash_layout(env, layout_ids, seed, val, layout, when_recursive)
|
||||
}
|
||||
|
||||
fn hash_builtin<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
seed: IntValue<'ctx>,
|
||||
val: BasicValueEnum<'ctx>,
|
||||
layout: &Layout<'a>,
|
||||
builtin: &Builtin<'a>,
|
||||
when_recursive: WhenRecursive<'a>,
|
||||
) -> IntValue<'ctx> {
|
||||
let ptr_bytes = env.target_info;
|
||||
|
||||
match builtin {
|
||||
Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal => {
|
||||
let hash_bytes = store_and_use_as_u8_ptr(env, val, layout);
|
||||
hash_bitcode_fn(env, seed, hash_bytes, layout.stack_size(ptr_bytes))
|
||||
}
|
||||
Builtin::Str => {
|
||||
// let zig deal with big vs small string
|
||||
call_bitcode_fn(env, &[seed.into(), val], bitcode::DICT_HASH_STR).into_int_value()
|
||||
}
|
||||
|
||||
Builtin::List(element_layout) => build_hash_list(
|
||||
env,
|
||||
layout_ids,
|
||||
layout,
|
||||
element_layout,
|
||||
when_recursive,
|
||||
seed,
|
||||
val.into_struct_value(),
|
||||
),
|
||||
}
|
||||
}
|
||||
|
||||
fn build_hash_struct<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
field_layouts: &'a [Layout<'a>],
|
||||
when_recursive: WhenRecursive<'a>,
|
||||
seed: IntValue<'ctx>,
|
||||
value: StructValue<'ctx>,
|
||||
) -> IntValue<'ctx> {
|
||||
let block = env.builder.get_insert_block().expect("to be in a function");
|
||||
let di_location = env.builder.get_current_debug_location().unwrap();
|
||||
|
||||
let struct_layout = Layout::struct_no_name_order(field_layouts);
|
||||
|
||||
let symbol = Symbol::GENERIC_HASH;
|
||||
let fn_name = layout_ids
|
||||
.get(symbol, &struct_layout)
|
||||
.to_symbol_string(symbol, &env.interns);
|
||||
|
||||
let function = match env.module.get_function(fn_name.as_str()) {
|
||||
Some(function_value) => function_value,
|
||||
None => {
|
||||
let seed_type = env.context.i64_type();
|
||||
|
||||
let arg_type = basic_type_from_layout(env, &struct_layout);
|
||||
|
||||
let function_value = crate::llvm::refcounting::build_header_help(
|
||||
env,
|
||||
&fn_name,
|
||||
seed_type.into(),
|
||||
&[seed_type.into(), arg_type],
|
||||
);
|
||||
|
||||
build_hash_struct_help(
|
||||
env,
|
||||
layout_ids,
|
||||
function_value,
|
||||
when_recursive,
|
||||
field_layouts,
|
||||
);
|
||||
|
||||
function_value
|
||||
}
|
||||
};
|
||||
|
||||
env.builder.position_at_end(block);
|
||||
env.builder
|
||||
.set_current_debug_location(env.context, di_location);
|
||||
let call = env
|
||||
.builder
|
||||
.build_call(function, &[seed.into(), value.into()], "struct_hash");
|
||||
|
||||
call.set_call_convention(FAST_CALL_CONV);
|
||||
|
||||
call.try_as_basic_value().left().unwrap().into_int_value()
|
||||
}
|
||||
|
||||
fn build_hash_struct_help<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
parent: FunctionValue<'ctx>,
|
||||
when_recursive: WhenRecursive<'a>,
|
||||
field_layouts: &[Layout<'a>],
|
||||
) {
|
||||
let ctx = env.context;
|
||||
|
||||
debug_info_init!(env, parent);
|
||||
|
||||
// Add args to scope
|
||||
let mut it = parent.get_param_iter();
|
||||
let seed = it.next().unwrap().into_int_value();
|
||||
let value = it.next().unwrap().into_struct_value();
|
||||
|
||||
seed.set_name(Symbol::ARG_1.as_str(&env.interns));
|
||||
value.set_name(Symbol::ARG_2.as_str(&env.interns));
|
||||
|
||||
let entry = ctx.append_basic_block(parent, "entry");
|
||||
env.builder.position_at_end(entry);
|
||||
|
||||
let result = hash_struct(env, layout_ids, seed, value, when_recursive, field_layouts);
|
||||
|
||||
env.builder.build_return(Some(&result));
|
||||
}
|
||||
|
||||
fn hash_struct<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
mut seed: IntValue<'ctx>,
|
||||
value: StructValue<'ctx>,
|
||||
when_recursive: WhenRecursive<'a>,
|
||||
field_layouts: &[Layout<'a>],
|
||||
) -> IntValue<'ctx> {
|
||||
let ptr_bytes = env.target_info;
|
||||
|
||||
let layout = Layout::struct_no_name_order(field_layouts);
|
||||
|
||||
// Optimization: if the bit representation of equal values is the same
|
||||
// just hash the bits. Caveat here is tags: e.g. `Nothing` in `Just a`
|
||||
// contains garbage bits after the tag (currently)
|
||||
if false {
|
||||
// this is a struct of only basic types, so we can just hash its bits
|
||||
let hash_bytes = store_and_use_as_u8_ptr(env, value.into(), &layout);
|
||||
hash_bitcode_fn(env, seed, hash_bytes, layout.stack_size(ptr_bytes))
|
||||
} else {
|
||||
for (index, field_layout) in field_layouts.iter().enumerate() {
|
||||
let field = env
|
||||
.builder
|
||||
.build_extract_value(value, index as u32, "hash_field")
|
||||
.unwrap();
|
||||
|
||||
let field = use_roc_value(env, *field_layout, field, "store_field_for_hashing");
|
||||
|
||||
if let Layout::RecursivePointer = field_layout {
|
||||
match &when_recursive {
|
||||
WhenRecursive::Unreachable => {
|
||||
unreachable!("The current layout should not be recursive, but is")
|
||||
}
|
||||
WhenRecursive::Loop(union_layout) => {
|
||||
let field_layout = Layout::Union(*union_layout);
|
||||
|
||||
let bt = basic_type_from_layout(env, &field_layout);
|
||||
|
||||
// cast the i64 pointer to a pointer to block of memory
|
||||
let field_cast = env
|
||||
.builder
|
||||
.build_bitcast(field, bt, "i64_to_opaque")
|
||||
.into_pointer_value();
|
||||
|
||||
seed = append_hash_layout(
|
||||
env,
|
||||
layout_ids,
|
||||
seed,
|
||||
field_cast.into(),
|
||||
&field_layout,
|
||||
when_recursive.clone(),
|
||||
)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
seed = append_hash_layout(
|
||||
env,
|
||||
layout_ids,
|
||||
seed,
|
||||
field,
|
||||
field_layout,
|
||||
when_recursive.clone(),
|
||||
);
|
||||
}
|
||||
}
|
||||
seed
|
||||
}
|
||||
}
|
||||
|
||||
fn build_hash_tag<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
union_layout: &UnionLayout<'a>,
|
||||
seed: IntValue<'ctx>,
|
||||
value: BasicValueEnum<'ctx>,
|
||||
) -> IntValue<'ctx> {
|
||||
let block = env.builder.get_insert_block().expect("to be in a function");
|
||||
let di_location = env.builder.get_current_debug_location().unwrap();
|
||||
|
||||
let symbol = Symbol::GENERIC_HASH;
|
||||
let fn_name = layout_ids
|
||||
.get(symbol, &Layout::Union(*union_layout))
|
||||
.to_symbol_string(symbol, &env.interns);
|
||||
|
||||
let function = match env.module.get_function(fn_name.as_str()) {
|
||||
Some(function_value) => function_value,
|
||||
None => {
|
||||
let seed_type = env.context.i64_type();
|
||||
|
||||
let arg_type = argument_type_from_union_layout(env, union_layout);
|
||||
|
||||
let function_value = crate::llvm::refcounting::build_header_help(
|
||||
env,
|
||||
&fn_name,
|
||||
seed_type.into(),
|
||||
&[seed_type.into(), arg_type],
|
||||
);
|
||||
|
||||
build_hash_tag_help(env, layout_ids, function_value, union_layout);
|
||||
|
||||
function_value
|
||||
}
|
||||
};
|
||||
|
||||
env.builder.position_at_end(block);
|
||||
env.builder
|
||||
.set_current_debug_location(env.context, di_location);
|
||||
let call = env
|
||||
.builder
|
||||
.build_call(function, &[seed.into(), value.into()], "struct_hash");
|
||||
|
||||
call.set_call_convention(FAST_CALL_CONV);
|
||||
|
||||
call.try_as_basic_value().left().unwrap().into_int_value()
|
||||
}
|
||||
|
||||
fn build_hash_tag_help<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
parent: FunctionValue<'ctx>,
|
||||
union_layout: &UnionLayout<'a>,
|
||||
) {
|
||||
let ctx = env.context;
|
||||
|
||||
debug_info_init!(env, parent);
|
||||
|
||||
// Add args to scope
|
||||
let mut it = parent.get_param_iter();
|
||||
let seed = it.next().unwrap().into_int_value();
|
||||
let value = it.next().unwrap();
|
||||
|
||||
seed.set_name(Symbol::ARG_1.as_str(&env.interns));
|
||||
value.set_name(Symbol::ARG_2.as_str(&env.interns));
|
||||
|
||||
let entry = ctx.append_basic_block(parent, "entry");
|
||||
env.builder.position_at_end(entry);
|
||||
|
||||
let result = hash_tag(env, layout_ids, parent, seed, value, union_layout);
|
||||
|
||||
env.builder.build_return(Some(&result));
|
||||
}
|
||||
|
||||
fn hash_tag<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
parent: FunctionValue<'ctx>,
|
||||
seed: IntValue<'ctx>,
|
||||
tag: BasicValueEnum<'ctx>,
|
||||
union_layout: &UnionLayout<'a>,
|
||||
) -> IntValue<'ctx> {
|
||||
use UnionLayout::*;
|
||||
|
||||
let entry_block = env.builder.get_insert_block().unwrap();
|
||||
|
||||
let merge_block = env.context.append_basic_block(parent, "merge_block");
|
||||
env.builder.position_at_end(merge_block);
|
||||
|
||||
let tag_id_layout = union_layout.tag_id_layout();
|
||||
let tag_id_basic_type = basic_type_from_layout(env, &tag_id_layout);
|
||||
|
||||
let merge_phi = env.builder.build_phi(seed.get_type(), "merge_hash");
|
||||
|
||||
env.builder.position_at_end(entry_block);
|
||||
match union_layout {
|
||||
NonRecursive(tags) => {
|
||||
let current_tag_id = get_tag_id(env, parent, union_layout, tag);
|
||||
|
||||
let mut cases = Vec::with_capacity_in(tags.len(), env.arena);
|
||||
|
||||
for (tag_id, field_layouts) in tags.iter().enumerate() {
|
||||
let block = env.context.append_basic_block(parent, "tag_id_modify");
|
||||
env.builder.position_at_end(block);
|
||||
|
||||
// hash the tag id
|
||||
let hash_bytes = store_and_use_as_u8_ptr(
|
||||
env,
|
||||
tag_id_basic_type
|
||||
.into_int_type()
|
||||
.const_int(tag_id as u64, false)
|
||||
.into(),
|
||||
&tag_id_layout,
|
||||
);
|
||||
let seed = hash_bitcode_fn(
|
||||
env,
|
||||
seed,
|
||||
hash_bytes,
|
||||
tag_id_layout.stack_size(env.target_info),
|
||||
);
|
||||
|
||||
// hash the tag data
|
||||
let tag = tag.into_pointer_value();
|
||||
let answer =
|
||||
hash_ptr_to_struct(env, layout_ids, union_layout, field_layouts, seed, tag);
|
||||
|
||||
merge_phi.add_incoming(&[(&answer, block)]);
|
||||
env.builder.build_unconditional_branch(merge_block);
|
||||
|
||||
cases.push((
|
||||
current_tag_id.get_type().const_int(tag_id as u64, false),
|
||||
block,
|
||||
));
|
||||
}
|
||||
|
||||
env.builder.position_at_end(entry_block);
|
||||
|
||||
match cases.pop() {
|
||||
Some((_, default)) => {
|
||||
env.builder.build_switch(current_tag_id, default, &cases);
|
||||
}
|
||||
None => {
|
||||
// we're hashing empty tag unions; this code is effectively unreachable
|
||||
env.builder.build_unreachable();
|
||||
}
|
||||
}
|
||||
}
|
||||
Recursive(tags) => {
|
||||
let current_tag_id = get_tag_id(env, parent, union_layout, tag);
|
||||
|
||||
let mut cases = Vec::with_capacity_in(tags.len(), env.arena);
|
||||
|
||||
for (tag_id, field_layouts) in tags.iter().enumerate() {
|
||||
let block = env.context.append_basic_block(parent, "tag_id_modify");
|
||||
env.builder.position_at_end(block);
|
||||
|
||||
// hash the tag id
|
||||
let hash_bytes = store_and_use_as_u8_ptr(
|
||||
env,
|
||||
tag_id_basic_type
|
||||
.into_int_type()
|
||||
.const_int(tag_id as u64, false)
|
||||
.into(),
|
||||
&tag_id_layout,
|
||||
);
|
||||
let seed = hash_bitcode_fn(
|
||||
env,
|
||||
seed,
|
||||
hash_bytes,
|
||||
tag_id_layout.stack_size(env.target_info),
|
||||
);
|
||||
|
||||
// hash the tag data
|
||||
let tag = tag_pointer_clear_tag_id(env, tag.into_pointer_value());
|
||||
let answer =
|
||||
hash_ptr_to_struct(env, layout_ids, union_layout, field_layouts, seed, tag);
|
||||
|
||||
merge_phi.add_incoming(&[(&answer, block)]);
|
||||
env.builder.build_unconditional_branch(merge_block);
|
||||
|
||||
cases.push((
|
||||
current_tag_id.get_type().const_int(tag_id as u64, false),
|
||||
block,
|
||||
));
|
||||
}
|
||||
|
||||
env.builder.position_at_end(entry_block);
|
||||
|
||||
let default = cases.pop().unwrap().1;
|
||||
|
||||
env.builder.build_switch(current_tag_id, default, &cases);
|
||||
}
|
||||
NullableUnwrapped { other_fields, .. } => {
|
||||
let tag = tag.into_pointer_value();
|
||||
|
||||
let is_null = env.builder.build_is_null(tag, "is_null");
|
||||
|
||||
let hash_null_block = env.context.append_basic_block(parent, "hash_null_block");
|
||||
let hash_other_block = env.context.append_basic_block(parent, "hash_other_block");
|
||||
|
||||
env.builder
|
||||
.build_conditional_branch(is_null, hash_null_block, hash_other_block);
|
||||
|
||||
{
|
||||
env.builder.position_at_end(hash_null_block);
|
||||
|
||||
let answer = hash_null(seed);
|
||||
|
||||
merge_phi.add_incoming(&[(&answer, hash_null_block)]);
|
||||
env.builder.build_unconditional_branch(merge_block);
|
||||
}
|
||||
|
||||
{
|
||||
env.builder.position_at_end(hash_other_block);
|
||||
|
||||
let answer =
|
||||
hash_ptr_to_struct(env, layout_ids, union_layout, other_fields, seed, tag);
|
||||
|
||||
merge_phi.add_incoming(&[(&answer, hash_other_block)]);
|
||||
env.builder.build_unconditional_branch(merge_block);
|
||||
}
|
||||
}
|
||||
NullableWrapped {
|
||||
other_tags,
|
||||
nullable_id,
|
||||
} => {
|
||||
let tag = tag.into_pointer_value();
|
||||
|
||||
let is_null = env.builder.build_is_null(tag, "is_null");
|
||||
|
||||
let hash_null_block = env.context.append_basic_block(parent, "hash_null_block");
|
||||
let hash_other_block = env.context.append_basic_block(parent, "hash_other_block");
|
||||
|
||||
env.builder
|
||||
.build_conditional_branch(is_null, hash_null_block, hash_other_block);
|
||||
|
||||
{
|
||||
env.builder.position_at_end(hash_null_block);
|
||||
|
||||
let answer = hash_null(seed);
|
||||
|
||||
merge_phi.add_incoming(&[(&answer, hash_null_block)]);
|
||||
env.builder.build_unconditional_branch(merge_block);
|
||||
}
|
||||
|
||||
{
|
||||
let mut cases = Vec::with_capacity_in(other_tags.len(), env.arena);
|
||||
|
||||
for (mut tag_id, field_layouts) in other_tags.iter().enumerate() {
|
||||
if tag_id >= *nullable_id as usize {
|
||||
tag_id += 1;
|
||||
}
|
||||
|
||||
let block = env.context.append_basic_block(parent, "tag_id_modify");
|
||||
env.builder.position_at_end(block);
|
||||
|
||||
// hash the tag id
|
||||
let hash_bytes = store_and_use_as_u8_ptr(
|
||||
env,
|
||||
tag_id_basic_type
|
||||
.into_int_type()
|
||||
.const_int(tag_id as u64, false)
|
||||
.into(),
|
||||
&tag_id_layout,
|
||||
);
|
||||
let seed1 = hash_bitcode_fn(
|
||||
env,
|
||||
seed,
|
||||
hash_bytes,
|
||||
tag_id_layout.stack_size(env.target_info),
|
||||
);
|
||||
|
||||
// hash tag data
|
||||
let tag = tag_pointer_clear_tag_id(env, tag);
|
||||
let answer = hash_ptr_to_struct(
|
||||
env,
|
||||
layout_ids,
|
||||
union_layout,
|
||||
field_layouts,
|
||||
seed1,
|
||||
tag,
|
||||
);
|
||||
|
||||
merge_phi.add_incoming(&[(&answer, block)]);
|
||||
env.builder.build_unconditional_branch(merge_block);
|
||||
|
||||
cases.push((
|
||||
tag_id_basic_type
|
||||
.into_int_type()
|
||||
.const_int(tag_id as u64, false),
|
||||
block,
|
||||
));
|
||||
}
|
||||
|
||||
env.builder.position_at_end(hash_other_block);
|
||||
|
||||
let tag_id = get_tag_id(env, parent, union_layout, tag.into());
|
||||
|
||||
let default = cases.pop().unwrap().1;
|
||||
|
||||
env.builder.build_switch(tag_id, default, &cases);
|
||||
}
|
||||
}
|
||||
NonNullableUnwrapped(field_layouts) => {
|
||||
let answer = hash_ptr_to_struct(
|
||||
env,
|
||||
layout_ids,
|
||||
union_layout,
|
||||
field_layouts,
|
||||
seed,
|
||||
tag.into_pointer_value(),
|
||||
);
|
||||
|
||||
merge_phi.add_incoming(&[(&answer, entry_block)]);
|
||||
env.builder.build_unconditional_branch(merge_block);
|
||||
}
|
||||
}
|
||||
|
||||
env.builder.position_at_end(merge_block);
|
||||
|
||||
merge_phi.as_basic_value().into_int_value()
|
||||
}
|
||||
|
||||
fn build_hash_list<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
layout: &Layout<'a>,
|
||||
element_layout: &Layout<'a>,
|
||||
when_recursive: WhenRecursive<'a>,
|
||||
seed: IntValue<'ctx>,
|
||||
value: StructValue<'ctx>,
|
||||
) -> IntValue<'ctx> {
|
||||
let block = env.builder.get_insert_block().expect("to be in a function");
|
||||
let di_location = env.builder.get_current_debug_location().unwrap();
|
||||
|
||||
let symbol = Symbol::GENERIC_HASH;
|
||||
let fn_name = layout_ids
|
||||
.get(symbol, layout)
|
||||
.to_symbol_string(symbol, &env.interns);
|
||||
|
||||
let function = match env.module.get_function(fn_name.as_str()) {
|
||||
Some(function_value) => function_value,
|
||||
None => {
|
||||
let seed_type = env.context.i64_type();
|
||||
|
||||
let arg_type = basic_type_from_layout(env, layout);
|
||||
|
||||
let function_value = crate::llvm::refcounting::build_header_help(
|
||||
env,
|
||||
&fn_name,
|
||||
seed_type.into(),
|
||||
&[seed_type.into(), arg_type],
|
||||
);
|
||||
|
||||
build_hash_list_help(
|
||||
env,
|
||||
layout_ids,
|
||||
function_value,
|
||||
when_recursive,
|
||||
element_layout,
|
||||
);
|
||||
|
||||
function_value
|
||||
}
|
||||
};
|
||||
|
||||
env.builder.position_at_end(block);
|
||||
env.builder
|
||||
.set_current_debug_location(env.context, di_location);
|
||||
let call = env
|
||||
.builder
|
||||
.build_call(function, &[seed.into(), value.into()], "struct_hash");
|
||||
|
||||
call.set_call_convention(FAST_CALL_CONV);
|
||||
|
||||
call.try_as_basic_value().left().unwrap().into_int_value()
|
||||
}
|
||||
|
||||
fn build_hash_list_help<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
parent: FunctionValue<'ctx>,
|
||||
when_recursive: WhenRecursive<'a>,
|
||||
element_layout: &Layout<'a>,
|
||||
) {
|
||||
let ctx = env.context;
|
||||
|
||||
debug_info_init!(env, parent);
|
||||
|
||||
// Add args to scope
|
||||
let mut it = parent.get_param_iter();
|
||||
let seed = it.next().unwrap().into_int_value();
|
||||
let value = it.next().unwrap().into_struct_value();
|
||||
|
||||
seed.set_name(Symbol::ARG_1.as_str(&env.interns));
|
||||
value.set_name(Symbol::ARG_2.as_str(&env.interns));
|
||||
|
||||
let entry = ctx.append_basic_block(parent, "entry");
|
||||
env.builder.position_at_end(entry);
|
||||
|
||||
let result = hash_list(
|
||||
env,
|
||||
layout_ids,
|
||||
parent,
|
||||
seed,
|
||||
value,
|
||||
when_recursive,
|
||||
element_layout,
|
||||
);
|
||||
|
||||
env.builder.build_return(Some(&result));
|
||||
}
|
||||
|
||||
fn hash_list<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
parent: FunctionValue<'ctx>,
|
||||
seed: IntValue<'ctx>,
|
||||
value: StructValue<'ctx>,
|
||||
when_recursive: WhenRecursive<'a>,
|
||||
element_layout: &Layout<'a>,
|
||||
) -> IntValue<'ctx> {
|
||||
use crate::llvm::build_list::{incrementing_elem_loop, load_list};
|
||||
use inkwell::types::BasicType;
|
||||
|
||||
// hash of a list is the hash of its elements
|
||||
let done_block = env.context.append_basic_block(parent, "done");
|
||||
let loop_block = env.context.append_basic_block(parent, "loop");
|
||||
|
||||
let element_type = basic_type_from_layout(env, element_layout);
|
||||
let ptr_type = element_type.ptr_type(inkwell::AddressSpace::Generic);
|
||||
|
||||
let (length, ptr) = load_list(env.builder, value, ptr_type);
|
||||
|
||||
let result = env.builder.build_alloca(env.context.i64_type(), "result");
|
||||
env.builder.build_store(result, seed);
|
||||
|
||||
let is_empty = env.builder.build_int_compare(
|
||||
inkwell::IntPredicate::EQ,
|
||||
length,
|
||||
env.ptr_int().const_zero(),
|
||||
"is_empty",
|
||||
);
|
||||
|
||||
env.builder
|
||||
.build_conditional_branch(is_empty, done_block, loop_block);
|
||||
|
||||
env.builder.position_at_end(loop_block);
|
||||
|
||||
let loop_fn = |_index, element| {
|
||||
let seed = env
|
||||
.builder
|
||||
.build_load(result, "load_current")
|
||||
.into_int_value();
|
||||
|
||||
let answer = append_hash_layout(
|
||||
env,
|
||||
layout_ids,
|
||||
seed,
|
||||
element,
|
||||
element_layout,
|
||||
when_recursive.clone(),
|
||||
);
|
||||
|
||||
env.builder.build_store(result, answer);
|
||||
};
|
||||
|
||||
incrementing_elem_loop(
|
||||
env,
|
||||
parent,
|
||||
*element_layout,
|
||||
ptr,
|
||||
length,
|
||||
"current_index",
|
||||
loop_fn,
|
||||
);
|
||||
|
||||
env.builder.build_unconditional_branch(done_block);
|
||||
|
||||
env.builder.position_at_end(done_block);
|
||||
|
||||
env.builder
|
||||
.build_load(result, "load_current")
|
||||
.into_int_value()
|
||||
}
|
||||
|
||||
fn hash_null(seed: IntValue<'_>) -> IntValue<'_> {
|
||||
seed
|
||||
}
|
||||
|
||||
fn hash_ptr_to_struct<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
layout_ids: &mut LayoutIds<'a>,
|
||||
union_layout: &UnionLayout<'a>,
|
||||
field_layouts: &'a [Layout<'a>],
|
||||
seed: IntValue<'ctx>,
|
||||
tag: PointerValue<'ctx>,
|
||||
) -> IntValue<'ctx> {
|
||||
use inkwell::types::BasicType;
|
||||
|
||||
let wrapper_type = argument_type_from_union_layout(env, union_layout);
|
||||
|
||||
// cast the opaque pointer to a pointer of the correct shape
|
||||
let wrapper_ptr = env
|
||||
.builder
|
||||
.build_bitcast(tag, wrapper_type, "hash_ptr_to_struct_opaque_to_correct")
|
||||
.into_pointer_value();
|
||||
|
||||
let struct_ptr = env
|
||||
.builder
|
||||
.build_struct_gep(wrapper_ptr, TAG_DATA_INDEX, "get_tag_data")
|
||||
.unwrap();
|
||||
|
||||
let struct_layout = Layout::struct_no_name_order(field_layouts);
|
||||
let struct_type = basic_type_from_layout(env, &struct_layout);
|
||||
let struct_ptr = env
|
||||
.builder
|
||||
.build_bitcast(
|
||||
struct_ptr,
|
||||
struct_type.ptr_type(inkwell::AddressSpace::Generic),
|
||||
"cast_tag_data",
|
||||
)
|
||||
.into_pointer_value();
|
||||
|
||||
let struct_value = env
|
||||
.builder
|
||||
.build_load(struct_ptr, "load_struct1")
|
||||
.into_struct_value();
|
||||
|
||||
build_hash_struct(
|
||||
env,
|
||||
layout_ids,
|
||||
field_layouts,
|
||||
WhenRecursive::Loop(*union_layout),
|
||||
seed,
|
||||
struct_value,
|
||||
)
|
||||
}
|
||||
|
||||
fn store_and_use_as_u8_ptr<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
value: BasicValueEnum<'ctx>,
|
||||
layout: &Layout<'a>,
|
||||
) -> PointerValue<'ctx> {
|
||||
let basic_type = basic_type_from_layout(env, layout);
|
||||
let alloc = env.builder.build_alloca(basic_type, "store");
|
||||
env.builder.build_store(alloc, value);
|
||||
|
||||
let u8_ptr = env
|
||||
.context
|
||||
.i8_type()
|
||||
.ptr_type(inkwell::AddressSpace::Generic);
|
||||
|
||||
env.builder
|
||||
.build_bitcast(alloc, u8_ptr, "as_u8_ptr")
|
||||
.into_pointer_value()
|
||||
}
|
||||
|
||||
fn hash_bitcode_fn<'a, 'ctx, 'env>(
|
||||
env: &Env<'a, 'ctx, 'env>,
|
||||
seed: IntValue<'ctx>,
|
||||
buffer: PointerValue<'ctx>,
|
||||
width: u32,
|
||||
) -> IntValue<'ctx> {
|
||||
let num_bytes = env.ptr_int().const_int(width as u64, false);
|
||||
|
||||
call_bitcode_fn(
|
||||
env,
|
||||
&[seed.into(), buffer.into(), num_bytes.into()],
|
||||
bitcode::DICT_HASH,
|
||||
)
|
||||
.into_int_value()
|
||||
}
|
|
@ -1,6 +1,5 @@
|
|||
pub mod bitcode;
|
||||
pub mod build;
|
||||
pub mod build_hash;
|
||||
pub mod build_list;
|
||||
pub mod build_str;
|
||||
pub mod compare;
|
||||
|
|
|
@ -18,7 +18,7 @@ pub fn eq_generic<'a>(
|
|||
ctx: &mut Context<'a>,
|
||||
layout: Layout<'a>,
|
||||
) -> Stmt<'a> {
|
||||
let eq_todo = || todo!("Specialized `==` operator for `{:?}`", layout);
|
||||
let _eq_todo = || todo!("Specialized `==` operator for `{:?}`", layout);
|
||||
|
||||
let main_body = match layout {
|
||||
Layout::Builtin(Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal) => {
|
||||
|
|
|
@ -103,7 +103,7 @@ pub fn refcount_generic<'a>(
|
|||
structure: Symbol,
|
||||
) -> Stmt<'a> {
|
||||
debug_assert!(is_rc_implemented_yet(&layout));
|
||||
let rc_todo = || todo!("Please update is_rc_implemented_yet for `{:?}`", layout);
|
||||
let _rc_todo = || todo!("Please update is_rc_implemented_yet for `{:?}`", layout);
|
||||
|
||||
match layout {
|
||||
Layout::Builtin(Builtin::Int(_) | Builtin::Float(_) | Builtin::Bool | Builtin::Decimal) => {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue