Skip to content

Commit b6a089e

Browse files
committed
Fix HAMT crash when function values used as map keys
The hash function returned constant 42 for all function types (fn_val, builtin_fn, protocol_fn, etc.), causing 100% hash collision when >8 function-keyed entries promoted from ArrayMap to HashMap. The HAMT's createTwoNode infinitely recursed (shift wraps at 30 bits) leading to stack overflow (SIGSEGV on Linux, SIGILL on Mac). Two fixes: 1. hash.zig: Identity hash for unhandled types using splitmix64 on the raw NaN-boxed bits (unique per heap object) 2. collections.zig: HAMT collision nodes for full 32-bit hash collisions (flat KV list with linear scan, matching JVM's HashCollisionNode) This fixes spec.clj loading crash and brings cljw test from 11→83 namespaces on Linux (full parity with Mac).
1 parent 65c5a03 commit b6a089e

File tree

2 files changed

+71
-1
lines changed

2 files changed

+71
-1
lines changed

src/runtime/collections.zig

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -759,8 +759,21 @@ pub const HAMTNode = struct {
759759

760760
const EMPTY: HAMTNode = .{};
761761

762+
/// A collision node stores multiple KVs with identical hashes.
763+
/// Identified by: data_map == 0, node_map == 0, kvs.len > 0.
764+
fn isCollision(self: *const HAMTNode) bool {
765+
return self.data_map == 0 and self.node_map == 0 and self.kvs.len > 0;
766+
}
767+
762768
/// Get value for key, or null if not found.
763769
pub fn get(self: *const HAMTNode, hash: u32, shift: u5, key: Value) ?Value {
770+
// Collision node — linear scan
771+
if (self.isCollision()) {
772+
for (self.kvs) |kv| {
773+
if (kv.key.eql(key)) return kv.val;
774+
}
775+
return null;
776+
}
764777
const bit = bitpos(hash, shift);
765778
if (self.data_map & bit != 0) {
766779
const idx = bitmapIndex(self.data_map, bit);
@@ -776,6 +789,27 @@ pub const HAMTNode = struct {
776789

777790
/// Return a new node with the key-value pair added/replaced.
778791
pub fn assoc(self: *const HAMTNode, allocator: std.mem.Allocator, hash: u32, shift: u5, key: Value, val: Value) !*const HAMTNode {
792+
// Collision node — linear scan for match, or append
793+
if (self.isCollision()) {
794+
for (self.kvs, 0..) |kv, i| {
795+
if (kv.key.eql(key)) {
796+
if (kv.val.eql(val)) return self;
797+
const new_kvs = try allocator.alloc(KV, self.kvs.len);
798+
@memcpy(new_kvs, self.kvs);
799+
new_kvs[i] = .{ .key = key, .val = val };
800+
const new_node = try allocator.create(HAMTNode);
801+
new_node.* = .{ .kvs = new_kvs };
802+
return new_node;
803+
}
804+
}
805+
const new_kvs = try allocator.alloc(KV, self.kvs.len + 1);
806+
@memcpy(new_kvs[0..self.kvs.len], self.kvs);
807+
new_kvs[self.kvs.len] = .{ .key = key, .val = val };
808+
const new_node = try allocator.create(HAMTNode);
809+
new_node.* = .{ .kvs = new_kvs };
810+
return new_node;
811+
}
812+
779813
const bit = bitpos(hash, shift);
780814

781815
if (self.data_map & bit != 0) {
@@ -851,6 +885,21 @@ pub const HAMTNode = struct {
851885

852886
/// Return a new node without the given key, or null if node becomes empty.
853887
pub fn dissoc(self: *const HAMTNode, allocator: std.mem.Allocator, hash: u32, shift: u5, key: Value) !?*const HAMTNode {
888+
// Collision node — linear scan
889+
if (self.isCollision()) {
890+
for (self.kvs, 0..) |kv, i| {
891+
if (kv.key.eql(key)) {
892+
if (self.kvs.len == 1) return null;
893+
const new_kvs = try allocator.alloc(KV, self.kvs.len - 1);
894+
copyExcept(KV, new_kvs, self.kvs, i);
895+
const new_node = try allocator.create(HAMTNode);
896+
new_node.* = .{ .kvs = new_kvs };
897+
return new_node;
898+
}
899+
}
900+
return self; // key not found
901+
}
902+
854903
const bit = bitpos(hash, shift);
855904

856905
if (self.data_map & bit != 0) {
@@ -947,6 +996,17 @@ pub const HAMTNode = struct {
947996

948997
/// Create a node with exactly two key-value pairs.
949998
fn createTwoNode(allocator: std.mem.Allocator, hash1: u32, key1: Value, val1: Value, hash2: u32, key2: Value, val2: Value, shift: u5) !*const HAMTNode {
999+
// Full hash collision — create collision node (flat KV list, linear scan)
1000+
if (hash1 == hash2) {
1001+
const node = try allocator.create(HAMTNode);
1002+
const kvs = try allocator.alloc(HAMTNode.KV, 2);
1003+
kvs[0] = .{ .key = key1, .val = val1 };
1004+
kvs[1] = .{ .key = key2, .val = val2 };
1005+
// data_map = 0, node_map = 0, kvs.len > 0 = collision node marker
1006+
node.* = .{ .kvs = kvs };
1007+
return node;
1008+
}
1009+
9501010
const bit1 = bitpos(hash1, shift);
9511011
const bit2 = bitpos(hash2, shift);
9521012

src/runtime/hash.zig

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -172,6 +172,16 @@ pub fn computeHash(v: Value) i64 {
172172
}
173173
break :blk 42;
174174
},
175-
else => 42,
175+
else => blk: {
176+
// Identity hash for function types, atoms, vars, etc.
177+
// Uses the raw NaN-boxed bits (unique per heap object) with splitmix64 mixing.
178+
var x: u64 = @intFromEnum(v);
179+
x ^= x >> 30;
180+
x *%= 0xbf58476d1ce4e5b9;
181+
x ^= x >> 27;
182+
x *%= 0x94d049bb133111eb;
183+
x ^= x >> 31;
184+
break :blk @bitCast(x);
185+
},
176186
};
177187
}

0 commit comments

Comments
 (0)