Created
November 7, 2025 21:01
-
-
Save eamonburns/901b0699f28d1c6474b9211f1a3b7225 to your computer and use it in GitHub Desktop.
An attempt at creating a stack of variable scopes, but I can't figure out why it's leaking :/
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| //! Leaking hash map | |
| //! | |
| const std = @import("std"); | |
| const assert = std.debug.assert; | |
| const print = std.debug.print; | |
| const Allocator = std.mem.Allocator; | |
| const Stack = @This(); | |
| pub const Value = union(enum) { | |
| nil, | |
| number: f32, | |
| string: []const u8, | |
| }; | |
| scopes: std.ArrayList(Scope), | |
| pub const Scope = struct { | |
| values: std.StringHashMapUnmanaged(Value), | |
| pub const init = Scope{ | |
| .values = .empty, | |
| }; | |
| pub fn deinit(self: *@This(), gpa: Allocator) void { | |
| // Section A: | |
| // - If I uncomment this section alone, nothing happens | |
| // - If I uncomment this section while "Section B" is also edited (comment | |
| // out "return" line, and deinitialize scope early), then I get an "Invalid free" error | |
| // var it = self.values.iterator(); | |
| // while (it.next()) |entry| { | |
| // gpa.destroy(entry.value_ptr); | |
| // } | |
| self.values.deinit(gpa); | |
| } | |
| }; | |
| pub fn init(gpa: Allocator) !Stack { | |
| var stack: Stack = .{ | |
| .scopes = try .initCapacity(gpa, 1), | |
| }; | |
| try stack.pushScope(gpa); | |
| return stack; | |
| } | |
| pub fn deinit(self: *Stack, gpa: Allocator) void { | |
| while (self.popScope(gpa)) |_| continue; | |
| self.scopes.deinit(gpa); | |
| } | |
| pub fn declare(self: *Stack, gpa: Allocator, name: []const u8, value: Value) !void { | |
| var scope = self.scopes.getLastOrNull() orelse return error.NoScopes; | |
| if (self.get(name)) |_| return error.AlreadyDeclared; | |
| // Section B: | |
| // - If I comment out the "return" line, and uncomment the two | |
| // following lines (put the new value, but deinit the scope early), the | |
| // leak goes away, but that means I didn't really put the new value | |
| return scope.values.put(gpa, name, value); | |
| // try scope.values.put(gpa, name, value); | |
| // scope.deinit(gpa); | |
| } | |
| pub fn get(self: *Stack, name: []const u8) ?Value { | |
| assert(self.scopes.items.len > 0); | |
| var i: usize = self.scopes.items.len - 1; | |
| var value: ?Value = null; | |
| while (i > 0 and value == null) : (i -= 1) { | |
| value = self.scopes.items[i].values.get(name); | |
| } | |
| return value; | |
| } | |
| // pub fn set(self: *Stack, gpa: Allocator, name: []const u8, value: Value) !void { | |
| // _ = self; | |
| // _ = gpa; | |
| // _ = name; | |
| // _ = value; | |
| // return error.Unimplemented; | |
| // } | |
| pub fn pushScope(self: *Stack, gpa: Allocator) !void { | |
| return self.scopes.append(gpa, .init); | |
| } | |
| /// Remove and deinitialize the scope at the top of the stack. | |
| /// Returns `null` if the stack is already empty. | |
| pub fn popScope(self: *Stack, gpa: Allocator) ?void { | |
| var scope = self.scopes.getLastOrNull() orelse return null; | |
| const new_length = self.scopes.items.len - 1; | |
| scope.deinit(gpa); | |
| self.scopes.shrinkRetainingCapacity(new_length); | |
| return; | |
| } | |
| test Stack { | |
| const expectEqualDeep = std.testing.expectEqualDeep; | |
| const gpa = std.testing.allocator; | |
| var stack: Stack = try .init(gpa); | |
| defer stack.deinit(gpa); | |
| try expectEqualDeep(null, stack.get("a")); | |
| try expectEqualDeep(null, stack.get("b")); | |
| try expectEqualDeep(null, stack.get("c")); | |
| try stack.declare(gpa, "a", .nil); | |
| // try stack.declare(gpa, "b", .{ .number = 3.14 }); | |
| // try stack.declare(gpa, "c", .{ .string = "hi" }); | |
| // | |
| // try expectEqualDeep(Value.nil, stack.get("a")); | |
| // try expectEqualDeep(Value{ .number = 3.14 }, stack.get("b")); | |
| // try expectEqualDeep(Value{ .string = "hi" }, stack.get("c")); | |
| // | |
| // { | |
| // try stack.pushScope(gpa); | |
| // | |
| // try expectEqualDeep(Value.nil, stack.get("a")); | |
| // try expectEqualDeep(Value{ .number = 3.14 }, stack.get("b")); | |
| // try expectEqualDeep(Value{ .string = "hi" }, stack.get("c")); | |
| // | |
| // try expectEqualDeep(null, stack.get("x")); | |
| // try stack.declare(gpa, "x", .nil); | |
| // try expectEqualDeep(Value.nil, stack.get("x")); | |
| // | |
| // try stack.set(gpa, "a", .{ .string = "thing" }); | |
| // try expectEqualDeep(Value{ .string = "thing" }, stack.get("a")); | |
| // | |
| // stack.popScope(gpa).?; | |
| // } | |
| // | |
| // try expectEqualDeep(null, stack.get("x")); | |
| // | |
| // try expectEqualDeep(Value{ .string = "thing" }, stack.get("a")); | |
| // try expectEqualDeep(Value{ .number = 3.14 }, stack.get("b")); | |
| // try expectEqualDeep(Value{ .string = "hi" }, stack.get("c")); | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment