├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── build.zig.zon └── src └── main.zig /.gitignore: -------------------------------------------------------------------------------- 1 | zig-out/ 2 | .zig-cache/ 3 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Veikka Tuominen 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Comptime HashMap 2 | 3 | A statically initiated HashMap, originally a pull request to the Zig std lib [#5359](https://github.com/ziglang/zig/pull/5359). 4 | 5 | ## Installation 6 | 7 | Build for Zig `0.13.0`. 8 | 9 | ```sh 10 | zig fetch --save git+https://github.com/Vexu/comptime_hash_map 11 | ``` 12 | 13 | In your `build.zig`: 14 | ```zig 15 | const chm = b.dependency("comptime_hash_map", .{}); 16 | exe.root_module.addImport("comptime_hash_map", chm.module("comptime_hash_map")); 17 | ``` 18 | 19 | In your `exe` module: 20 | ```zig 21 | const chm = @import("comptime_hash_map"); 22 | ``` 23 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.Build) void { 4 | const target = b.standardTargetOptions(.{}); 5 | const optimize = b.standardOptimizeOption(.{}); 6 | 7 | const lib = b.addStaticLibrary(.{ 8 | .name = "comptime_hash_map", 9 | .root_source_file = b.path("src/main.zig"), 10 | .target = target, 11 | .optimize = optimize, 12 | }); 13 | b.installArtifact(lib); 14 | 15 | const lib_unit_tests = b.addTest(.{ 16 | .root_source_file = b.path("src/main.zig"), 17 | .target = target, 18 | .optimize = optimize, 19 | }); 20 | const run_lib_unit_tests = b.addRunArtifact(lib_unit_tests); 21 | 22 | const test_step = b.step("test", "Run unit tests"); 23 | test_step.dependOn(&run_lib_unit_tests.step); 24 | 25 | // https://ziggit.dev/t/how-to-package-a-zig-source-module-and-how-to-use-it/3457 26 | _ = b.addModule("comptime_hash_map", .{ 27 | .root_source_file = b.path("src/main.zig"), 28 | .target = target, 29 | .optimize = optimize, 30 | }); 31 | } 32 | -------------------------------------------------------------------------------- /build.zig.zon: -------------------------------------------------------------------------------- 1 | .{ 2 | // This is the default name used by packages depending on this one. For 3 | // example, when a user runs `zig fetch --save `, this field is used 4 | // as the key in the `dependencies` table. Although the user can choose a 5 | // different name, most users will stick with this provided value. 6 | // 7 | // It is redundant to include "zig" in this name because it is already 8 | // within the Zig package namespace. 9 | .name = "comptime_hash_map", 10 | 11 | // This is a [Semantic Version](https://semver.org/). 12 | // In a future version of Zig it will be used for package deduplication. 13 | .version = "0.0.0", 14 | 15 | // This field is optional. 16 | // This is currently advisory only; Zig does not yet do anything 17 | // with this value. 18 | //.minimum_zig_version = "0.11.0", 19 | 20 | // This field is optional. 21 | // Each dependency must either provide a `url` and `hash`, or a `path`. 22 | // `zig build --fetch` can be used to fetch all dependencies of a package, recursively. 23 | // Once all dependencies are fetched, `zig build` no longer requires 24 | // internet connectivity. 25 | .dependencies = .{ 26 | // See `zig fetch --save ` for a command-line interface for adding dependencies. 27 | //.example = .{ 28 | // // When updating this field to a new URL, be sure to delete the corresponding 29 | // // `hash`, otherwise you are communicating that you expect to find the old hash at 30 | // // the new URL. 31 | // .url = "https://example.com/foo.tar.gz", 32 | // 33 | // // This is computed from the file contents of the directory of files that is 34 | // // obtained after fetching `url` and applying the inclusion rules given by 35 | // // `paths`. 36 | // // 37 | // // This field is the source of truth; packages do not come from a `url`; they 38 | // // come from a `hash`. `url` is just one of many possible mirrors for how to 39 | // // obtain a package matching this `hash`. 40 | // // 41 | // // Uses the [multihash](https://multiformats.io/multihash/) format. 42 | // .hash = "...", 43 | // 44 | // // When this is provided, the package is found in a directory relative to the 45 | // // build root. In this case the package's hash is irrelevant and therefore not 46 | // // computed. This field and `url` are mutually exclusive. 47 | // .path = "foo", 48 | 49 | // // When this is set to `true`, a package is declared to be lazily 50 | // // fetched. This makes the dependency only get fetched if it is 51 | // // actually used. 52 | // .lazy = false, 53 | //}, 54 | }, 55 | 56 | // Specifies the set of files and directories that are included in this package. 57 | // Only files and directories listed here are included in the `hash` that 58 | // is computed for this package. Only files listed here will remain on disk 59 | // when using the zig package manager. As a rule of thumb, one should list 60 | // files required for compilation plus any license(s). 61 | // Paths are relative to the build root. Use the empty string (`""`) to refer to 62 | // the build root itself. 63 | // A directory listed here means that all files within, recursively, are included. 64 | .paths = .{ 65 | "build.zig", 66 | "build.zig.zon", 67 | "src", 68 | // For example... 69 | //"LICENSE", 70 | //"README.md", 71 | }, 72 | } 73 | -------------------------------------------------------------------------------- /src/main.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const hash_map = std.hash_map; 3 | const testing = std.testing; 4 | const math = std.math; 5 | 6 | /// A comptime hashmap constructed with automatically selected hash and eql functions. 7 | pub fn AutoComptimeHashMap(comptime K: type, comptime V: type, comptime values: anytype) type { 8 | return ComptimeHashMap(K, V, hash_map.AutoContext(K), values); 9 | } 10 | 11 | /// Builtin hashmap for strings as keys. 12 | pub fn ComptimeStringHashMap(comptime V: type, comptime values: anytype) type { 13 | return ComptimeHashMap([]const u8, V, hash_map.StringContext, values); 14 | } 15 | 16 | /// A hashmap which is constructed at compile time from constant values. 17 | /// Intended to be used as a faster lookup table. 18 | pub fn ComptimeHashMap(comptime K: type, comptime V: type, comptime ctx: type, comptime values: anytype) type { 19 | std.debug.assert(values.len != 0); 20 | @setEvalBranchQuota(1000 * values.len); 21 | 22 | const Entry = struct { 23 | distance_from_start_index: usize = 0, 24 | key: K = undefined, 25 | val: V = undefined, 26 | used: bool = false, 27 | }; 28 | 29 | // ensure that the hash map will be at most 60% full 30 | const size = math.ceilPowerOfTwo(usize, values.len * 5 / 3) catch unreachable; 31 | comptime var slots = [1]Entry{.{}} ** size; 32 | 33 | comptime var max_distance_from_start_index = 0; 34 | 35 | slot_loop: for (values) |kv| { 36 | var key: K = kv.@"0"; 37 | var value: V = kv.@"1"; 38 | 39 | const start_index = @as(usize, ctx.hash(undefined, key)) & (size - 1); 40 | 41 | var roll_over = 0; 42 | var distance_from_start_index = 0; 43 | while (roll_over < size) : ({ 44 | roll_over += 1; 45 | distance_from_start_index += 1; 46 | }) { 47 | const index = (start_index + roll_over) & (size - 1); 48 | const entry = &slots[index]; 49 | 50 | if (entry.used and !ctx.eql(undefined, entry.key, key)) { 51 | if (entry.distance_from_start_index < distance_from_start_index) { 52 | // robin hood to the rescue 53 | const tmp = slots[index]; 54 | max_distance_from_start_index = @max(max_distance_from_start_index, distance_from_start_index); 55 | entry.* = .{ 56 | .used = true, 57 | .distance_from_start_index = distance_from_start_index, 58 | .key = key, 59 | .val = value, 60 | }; 61 | key = tmp.key; 62 | value = tmp.val; 63 | distance_from_start_index = tmp.distance_from_start_index; 64 | } 65 | continue; 66 | } 67 | 68 | max_distance_from_start_index = @max(distance_from_start_index, max_distance_from_start_index); 69 | entry.* = .{ 70 | .used = true, 71 | .distance_from_start_index = distance_from_start_index, 72 | .key = key, 73 | .val = value, 74 | }; 75 | continue :slot_loop; 76 | } 77 | unreachable; // put into a full map 78 | } 79 | 80 | return struct { 81 | const entries = slots; 82 | 83 | pub fn has(key: K) bool { 84 | return get(key) != null; 85 | } 86 | 87 | pub fn get(key: K) ?*const V { 88 | const start_index = @as(usize, ctx.hash(undefined, key)) & (size - 1); 89 | { 90 | var roll_over: usize = 0; 91 | while (roll_over <= max_distance_from_start_index) : (roll_over += 1) { 92 | const index = (start_index + roll_over) & (size - 1); 93 | const entry = &entries[index]; 94 | 95 | if (!entry.used) return null; 96 | if (ctx.eql(undefined, entry.key, key)) return &entry.val; 97 | } 98 | } 99 | return null; 100 | } 101 | }; 102 | } 103 | 104 | test "basic usage" { 105 | const map = ComptimeStringHashMap(usize, .{ 106 | .{ "foo", 1 }, 107 | .{ "bar", 2 }, 108 | .{ "baz", 3 }, 109 | .{ "quux", 4 }, 110 | }); 111 | 112 | try testing.expect(map.has("foo")); 113 | try testing.expect(map.has("bar")); 114 | try testing.expect(!map.has("zig")); 115 | try testing.expect(!map.has("ziguana")); 116 | 117 | try testing.expect(map.get("baz").?.* == 3); 118 | try testing.expect(map.get("quux").?.* == 4); 119 | try testing.expect(map.get("nah") == null); 120 | try testing.expect(map.get("...") == null); 121 | } 122 | 123 | test "auto comptime hash map" { 124 | const map = AutoComptimeHashMap(usize, []const u8, .{ 125 | .{ 1, "foo" }, 126 | .{ 2, "bar" }, 127 | .{ 3, "baz" }, 128 | .{ 45, "quux" }, 129 | }); 130 | 131 | try testing.expect(map.has(1)); 132 | try testing.expect(map.has(2)); 133 | try testing.expect(!map.has(4)); 134 | try testing.expect(!map.has(1_000_000)); 135 | 136 | try testing.expectEqualStrings("foo", map.get(1).?.*); 137 | try testing.expectEqualStrings("bar", map.get(2).?.*); 138 | try testing.expect(map.get(4) == null); 139 | try testing.expect(map.get(4_000_000) == null); 140 | } 141 | --------------------------------------------------------------------------------