diff --git a/DIRECTORY.md b/DIRECTORY.md index e3d39ed..48537ef 100644 --- a/DIRECTORY.md +++ b/DIRECTORY.md @@ -44,6 +44,18 @@ * [Quicksort](https://github.com/TheAlgorithms/Zig/blob/HEAD/sort/quickSort.zig) * [Radixsort](https://github.com/TheAlgorithms/Zig/blob/HEAD/sort/radixSort.zig) +## Tiger Style +Expert-level algorithms following [TigerBeetle's Tiger Style](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md) principles + * [Time Simulation](https://github.com/TheAlgorithms/Zig/blob/HEAD/tiger_style/time_simulation.zig) - Deterministic time framework + * [Merge Sort Tiger](https://github.com/TheAlgorithms/Zig/blob/HEAD/tiger_style/merge_sort_tiger.zig) - Zero-recursion merge sort + * [Knapsack Tiger](https://github.com/TheAlgorithms/Zig/blob/HEAD/tiger_style/knapsack_tiger.zig) - Heavy-assertion DP + * [Ring Buffer](https://github.com/TheAlgorithms/Zig/blob/HEAD/tiger_style/ring_buffer.zig) - Bounded FIFO queue + * [Raft Consensus](https://github.com/TheAlgorithms/Zig/blob/HEAD/tiger_style/raft_consensus.zig) - Raft consensus + * [Two-Phase Commit](https://github.com/TheAlgorithms/Zig/blob/HEAD/tiger_style/two_phase_commit.zig) - 2PC protocol + * [VSR Consensus](https://github.com/TheAlgorithms/Zig/blob/HEAD/tiger_style/vsr_consensus.zig) - Viewstamped Replication + * [Robin Hood Hash](https://github.com/TheAlgorithms/Zig/blob/HEAD/tiger_style/robin_hood_hash.zig) - Cache-efficient hash table + * [Skip List](https://github.com/TheAlgorithms/Zig/blob/HEAD/tiger_style/skip_list.zig) - Probabilistic ordered map + ## Web * Http * [Client](https://github.com/TheAlgorithms/Zig/blob/HEAD/web/http/client.zig) diff --git a/build.zig b/build.zig index 392af3e..d5a7551 100644 --- a/build.zig +++ b/build.zig @@ -224,6 +224,71 @@ pub fn build(b: *std.Build) void { .name = "newton_raphson_root.zig", .category = "numerical_methods", }); + + // Tiger Style + if (std.mem.eql(u8, op, "tiger_style/time_simulation")) + buildAlgorithm(b, .{ + .optimize = optimize, + .target = target, + .name = "time_simulation.zig", + .category = "tiger_style", + }); + if (std.mem.eql(u8, op, "tiger_style/merge_sort_tiger")) + buildAlgorithm(b, .{ + .optimize = optimize, + .target = target, + .name = "merge_sort_tiger.zig", + .category = "tiger_style", + }); + if (std.mem.eql(u8, op, "tiger_style/knapsack_tiger")) + buildAlgorithm(b, .{ + .optimize = optimize, + .target = target, + .name = "knapsack_tiger.zig", + .category = "tiger_style", + }); + if (std.mem.eql(u8, op, "tiger_style/ring_buffer")) + buildAlgorithm(b, .{ + .optimize = optimize, + .target = target, + .name = "ring_buffer.zig", + .category = "tiger_style", + }); + if (std.mem.eql(u8, op, "tiger_style/raft_consensus")) + buildAlgorithm(b, .{ + .optimize = optimize, + .target = target, + .name = "raft_consensus.zig", + .category = "tiger_style", + }); + if (std.mem.eql(u8, op, "tiger_style/two_phase_commit")) + buildAlgorithm(b, .{ + .optimize = optimize, + .target = target, + .name = "two_phase_commit.zig", + .category = "tiger_style", + }); + if (std.mem.eql(u8, op, "tiger_style/vsr_consensus")) + buildAlgorithm(b, .{ + .optimize = optimize, + .target = target, + .name = "vsr_consensus.zig", + .category = "tiger_style", + }); + if (std.mem.eql(u8, op, "tiger_style/robin_hood_hash")) + buildAlgorithm(b, .{ + .optimize = optimize, + .target = target, + .name = "robin_hood_hash.zig", + .category = "tiger_style", + }); + if (std.mem.eql(u8, op, "tiger_style/skip_list")) + buildAlgorithm(b, .{ + .optimize = optimize, + .target = target, + .name = "skip_list.zig", + .category = "tiger_style", + }); } fn buildAlgorithm(b: *std.Build, info: BInfo) void { diff --git a/runall.zig b/runall.zig index ffe4ee3..d107baa 100644 --- a/runall.zig +++ b/runall.zig @@ -51,6 +51,17 @@ pub fn main() !void { // Numerical Methods try runTest(allocator, "numerical_methods/newton_raphson"); + + // Tiger Style + try runTest(allocator, "tiger_style/time_simulation"); + try runTest(allocator, "tiger_style/merge_sort_tiger"); + try runTest(allocator, "tiger_style/knapsack_tiger"); + try runTest(allocator, "tiger_style/ring_buffer"); + try runTest(allocator, "tiger_style/raft_consensus"); + try runTest(allocator, "tiger_style/two_phase_commit"); + try runTest(allocator, "tiger_style/vsr_consensus"); + try runTest(allocator, "tiger_style/robin_hood_hash"); + try runTest(allocator, "tiger_style/skip_list"); } fn runTest(allocator: std.mem.Allocator, comptime algorithm: []const u8) !void { diff --git a/tiger_style/README.md b/tiger_style/README.md new file mode 100644 index 0000000..691f114 --- /dev/null +++ b/tiger_style/README.md @@ -0,0 +1,158 @@ +# Tiger Style Algorithms + +> "Simplicity and elegance are unpopular because they require hard work and discipline to achieve" — Edsger Dijkstra + +This folder showcases **expert-level algorithmic techniques** following [TigerBeetle's Tiger Style](https://github.com/tigerbeetle/tigerbeetle/blob/main/docs/TIGER_STYLE.md) coding principles. + +## Tiger Style Principles + +### 1. **Safety First** + +- Heavy use of assertions (minimum 2 per function) +- Assert all preconditions, postconditions, and invariants +- Fail-fast on violations +- Assertions downgrade catastrophic bugs into liveness bugs + +### 2. **Explicit Everything** + +- Use explicitly-sized types: `u32`, `u64`, `i32` (never `usize`) +- Simple, explicit control flow only +- No recursion - all algorithms are iterative +- Bounded loops with explicit upper limits + +### 3. **Zero Technical Debt** + +- Production-quality code from the start +- No shortcuts or workarounds +- Solve problems correctly the first time +- Every abstraction must earn its place + +### 4. **Deterministic Testing** + +- Time simulation for reproducible tests +- Fuzz-friendly with heavy assertions +- Test edge cases exhaustively + +## Implementations + +### `time_simulation.zig` + +**Deterministic time simulation framework** inspired by TigerBeetle's testing approach. + +- Virtual clock with nanosecond precision +- Deterministic event scheduling +- Reproducible test scenarios +- Perfect for testing distributed algorithms + +### `merge_sort_tiger.zig` + +**Zero-recursion merge sort** with Tiger Style discipline. + +- Iterative bottom-up implementation +- Explicit stack bounds +- Heavy assertions on every invariant +- No hidden allocations + +### `knapsack_tiger.zig` + +**0/1 Knapsack with militant assertion discipline.** + +- Every array access validated +- Explicit capacity bounds +- DP table invariants checked +- Overflow protection + +### `ring_buffer.zig` + +**Bounded ring buffer** demonstrating fail-fast principles. + +- Fixed capacity with compile-time guarantees +- All operations bounded O(1) +- Assertions on every state transition +- Production-grade reliability + +### `raft_consensus.zig` + +**Raft consensus algorithm** for distributed systems. + +- Explicit state machine (follower/candidate/leader) +- Bounded log with fail-fast +- Leader election with majority votes +- Inspired by TigerBeetle's consensus approach + +### `two_phase_commit.zig` + +**Two-Phase Commit protocol** for distributed transactions. + +- Coordinator with bounded participants +- Prepare and commit phases +- Timeout detection +- Atomic commit/abort decisions + +### `vsr_consensus.zig` + +**VSR (Viewstamped Replication)** - TigerBeetle's actual consensus. + +- More sophisticated than Raft +- View change protocol +- Explicit view and op numbers +- Inspired by "Viewstamped Replication Revisited" + +### `robin_hood_hash.zig` + +**Cache-efficient hash table** with Robin Hood hashing. + +- Fixed capacity (no dynamic resizing) +- Linear probing with fairness +- Bounded probe distances +- Explicit load factor limits + +### `skip_list.zig` + +**Skip list** - probabilistic ordered map. + +- Foundation for LSM trees +- Deterministic randomness (seeded RNG) +- Bounded maximum level +- No recursion (iterative traversal) + +## Why Tiger Style? + +Tiger Style is about **engineering excellence**: + +1. **Code you can trust** - Heavy assertions catch bugs during fuzzing +2. **No surprises** - Explicit bounds prevent tail latency spikes +3. **Maintainable** - Simple control flow is easy to reason about +4. **Fast** - No recursion overhead, explicit types, cache-friendly + +## Running Tests + +```bash +# Test individual files +zig test tiger_style/time_simulation.zig # 7 tests +zig test tiger_style/merge_sort_tiger.zig # 14 tests +zig test tiger_style/knapsack_tiger.zig # 12 tests +zig test tiger_style/ring_buffer.zig # 15 tests +zig test tiger_style/raft_consensus.zig # 12 tests +zig test tiger_style/two_phase_commit.zig # 9 tests +zig test tiger_style/vsr_consensus.zig # 11 tests ⭐ +zig test tiger_style/robin_hood_hash.zig # 12 tests ⭐ +zig test tiger_style/skip_list.zig # 10 tests ⭐ + +# Total: 102 tests across 9 implementations! +``` + +## Contributing + +When adding to tiger_style/: + +1. **No recursion** - use iteration with explicit bounds +2. **Assert everything** - minimum 2 assertions per function +3. **Explicit types** - u32/u64/i32, never usize +4. **Bounded loops** - every loop must have a provable upper bound +5. **Fail-fast** - detect violations immediately +6. **Simple control flow** - keep it explicit and obvious + +--- + +*"The rules act like the seat-belt in your car: initially they are perhaps a little uncomfortable, but after a while their use becomes second-nature and not using them becomes unimaginable."* — Gerard J. Holzmann diff --git a/tiger_style/knapsack_tiger.zig b/tiger_style/knapsack_tiger.zig new file mode 100644 index 0000000..141785f --- /dev/null +++ b/tiger_style/knapsack_tiger.zig @@ -0,0 +1,443 @@ +//! Tiger Style 0/1 Knapsack - Militant Assertion Discipline +//! +//! Demonstrates Tiger Style dynamic programming: +//! - Every array access validated with assertions +//! - Explicit u32 capacity bounds (never usize) +//! - DP table invariants checked at every step +//! - Overflow protection on all arithmetic +//! - Bounded loops with provable upper bounds +//! - Simple, explicit control flow + +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; + +/// Maximum number of items (must be bounded) +pub const MAX_ITEMS: u32 = 10000; + +/// Maximum knapsack capacity (must be bounded) +pub const MAX_CAPACITY: u32 = 100000; + +/// Item with weight and value +pub const Item = struct { + weight: u32, + value: u32, + + /// Validate item invariants + pub fn validate(self: Item) void { + // Items must have positive weight (zero weight items are degenerate) + assert(self.weight > 0); + assert(self.weight <= MAX_CAPACITY); + // Value can be zero but should fit in u32 + assert(self.value <= std.math.maxInt(u32)); + } +}; + +/// Solve 0/1 knapsack problem using dynamic programming +/// Returns maximum value achievable within capacity +/// +/// Time: O(n * capacity) +/// Space: O(n * capacity) for DP table +pub fn knapsack(items: []const Item, capacity: u32, allocator: std.mem.Allocator) !u32 { + // Preconditions - validate all inputs + assert(items.len <= MAX_ITEMS); + assert(capacity <= MAX_CAPACITY); + + const n: u32 = @intCast(items.len); + + // Validate all items + for (items) |item| { + item.validate(); + } + + // Handle trivial cases + if (n == 0 or capacity == 0) { + return 0; + } + + // Allocate DP table: dp[i][w] = max value with first i items and capacity w + // Using explicit u32 dimensions + const rows = n + 1; + const cols = capacity + 1; + + // Bounds check before allocation + assert(rows <= MAX_ITEMS + 1); + assert(cols <= MAX_CAPACITY + 1); + + // Allocate as flat array to avoid double pointer indirection + const table_size: usize = @as(usize, rows) * @as(usize, cols); + const dp = try allocator.alloc(u32, table_size); + defer allocator.free(dp); + + // Helper to access dp[i][w] + const getDP = struct { + fn get(table: []u32, row: u32, col: u32, num_cols: u32, num_rows: u32) u32 { + assert(row < num_rows); + assert(col < num_cols); + const index: usize = @as(usize, row) * @as(usize, num_cols) + @as(usize, col); + assert(index < table.len); + return table[index]; + } + + fn set(table: []u32, row: u32, col: u32, num_cols: u32, num_rows: u32, value: u32) void { + assert(row < num_rows); + assert(col < num_cols); + const index: usize = @as(usize, row) * @as(usize, num_cols) + @as(usize, col); + assert(index < table.len); + table[index] = value; + } + }; + + // Initialize base case: dp[0][w] = 0 for all w + // (zero items = zero value) + var w: u32 = 0; + while (w <= capacity) : (w += 1) { + assert(w < cols); + getDP.set(dp, 0, w, cols, rows, 0); + + // Invariant: base case initialized + assert(getDP.get(dp, 0, w, cols, rows) == 0); + } + + // Fill DP table + var i: u32 = 1; + while (i <= n) : (i += 1) { + assert(i > 0); + assert(i <= n); + assert(i < rows); + + const item_index = i - 1; + assert(item_index < items.len); + + const item = items[item_index]; + item.validate(); // Revalidate during computation + + w = 0; + while (w <= capacity) : (w += 1) { + assert(w < cols); + + // Get value without including current item + const without_item = getDP.get(dp, i - 1, w, cols, rows); + + // Can we include this item? + if (w >= item.weight) { + // Yes - check if including it is better + assert(w >= item.weight); + const remaining_capacity = w - item.weight; + assert(remaining_capacity <= w); + + const with_item_prev = getDP.get(dp, i - 1, remaining_capacity, cols, rows); + + // Check for overflow before adding + const max_value = std.math.maxInt(u32); + if (with_item_prev <= max_value - item.value) { + const with_item = with_item_prev + item.value; + const best = @max(without_item, with_item); + + getDP.set(dp, i, w, cols, rows, best); + + // Invariant: DP value is non-decreasing with capacity + if (w > 0) { + const prev_w_value = getDP.get(dp, i, w - 1, cols, rows); + assert(getDP.get(dp, i, w, cols, rows) >= prev_w_value); + } + + // Invariant: DP value never decreases with more items + assert(getDP.get(dp, i, w, cols, rows) >= getDP.get(dp, i - 1, w, cols, rows)); + } else { + // Overflow would occur, use without_item + getDP.set(dp, i, w, cols, rows, without_item); + } + } else { + // Cannot include item (too heavy) + assert(w < item.weight); + getDP.set(dp, i, w, cols, rows, without_item); + + // Invariant: same as without item + assert(getDP.get(dp, i, w, cols, rows) == getDP.get(dp, i - 1, w, cols, rows)); + } + } + } + + // Get final answer + const result = getDP.get(dp, n, capacity, cols, rows); + + // Postconditions + assert(result <= std.math.maxInt(u32)); + // Result should not exceed sum of all values (sanity check) + var total_value: u64 = 0; + for (items) |item| { + total_value += item.value; + } + assert(result <= total_value); + + return result; +} + +/// Solve knapsack and also return which items to include +/// Returns tuple of (max_value, selected_items) +pub fn knapsackWithItems( + items: []const Item, + capacity: u32, + allocator: std.mem.Allocator, +) !struct { value: u32, selected: []bool } { + // Preconditions + assert(items.len <= MAX_ITEMS); + assert(capacity <= MAX_CAPACITY); + + const n: u32 = @intCast(items.len); + + if (n == 0 or capacity == 0) { + const selected = try allocator.alloc(bool, items.len); + @memset(selected, false); + return .{ .value = 0, .selected = selected }; + } + + // Allocate DP table + const rows = n + 1; + const cols = capacity + 1; + const table_size: usize = @as(usize, rows) * @as(usize, cols); + const dp = try allocator.alloc(u32, table_size); + defer allocator.free(dp); + + // Helper functions + const getDP = struct { + fn get(table: []u32, row: u32, col: u32, num_cols: u32, num_rows: u32) u32 { + assert(row < num_rows); + assert(col < num_cols); + const index: usize = @as(usize, row) * @as(usize, num_cols) + @as(usize, col); + assert(index < table.len); + return table[index]; + } + + fn set(table: []u32, row: u32, col: u32, num_cols: u32, num_rows: u32, value: u32) void { + assert(row < num_rows); + assert(col < num_cols); + const index: usize = @as(usize, row) * @as(usize, num_cols) + @as(usize, col); + assert(index < table.len); + table[index] = value; + } + }; + + // Fill DP table (same as knapsack function) + var w: u32 = 0; + while (w <= capacity) : (w += 1) { + getDP.set(dp, 0, w, cols, rows, 0); + } + + var i: u32 = 1; + while (i <= n) : (i += 1) { + const item = items[i - 1]; + item.validate(); + + w = 0; + while (w <= capacity) : (w += 1) { + const without = getDP.get(dp, i - 1, w, cols, rows); + + if (w >= item.weight) { + const with_prev = getDP.get(dp, i - 1, w - item.weight, cols, rows); + const max_value = std.math.maxInt(u32); + + if (with_prev <= max_value - item.value) { + const with = with_prev + item.value; + getDP.set(dp, i, w, cols, rows, @max(without, with)); + } else { + getDP.set(dp, i, w, cols, rows, without); + } + } else { + getDP.set(dp, i, w, cols, rows, without); + } + } + } + + // Backtrack to find selected items + const selected = try allocator.alloc(bool, items.len); + @memset(selected, false); + + var current_capacity = capacity; + var current_item: u32 = n; + + // Bounded backtracking loop + while (current_item > 0) { + assert(current_item <= n); + assert(current_capacity <= capacity); + + const item_index = current_item - 1; + assert(item_index < items.len); + + const item = items[item_index]; + const current_value = getDP.get(dp, current_item, current_capacity, cols, rows); + const prev_value = getDP.get(dp, current_item - 1, current_capacity, cols, rows); + + // Was this item included? + if (current_value != prev_value) { + // Yes, item was included + assert(current_capacity >= item.weight); + selected[item_index] = true; + current_capacity -= item.weight; + } + + current_item -= 1; + } + + const result_value = getDP.get(dp, n, capacity, cols, rows); + + return .{ .value = result_value, .selected = selected }; +} + +// ============================================================================ +// Tests - Exhaustive edge case coverage +// ============================================================================ + +test "knapsack: empty items" { + const items: []const Item = &.{}; + const result = try knapsack(items, 100, testing.allocator); + try testing.expectEqual(@as(u32, 0), result); +} + +test "knapsack: zero capacity" { + const items = [_]Item{ + .{ .weight = 10, .value = 60 }, + .{ .weight = 20, .value = 100 }, + }; + + const result = try knapsack(&items, 0, testing.allocator); + try testing.expectEqual(@as(u32, 0), result); +} + +test "knapsack: single item fits" { + const items = [_]Item{ + .{ .weight = 10, .value = 60 }, + }; + + const result = try knapsack(&items, 50, testing.allocator); + try testing.expectEqual(@as(u32, 60), result); +} + +test "knapsack: single item doesn't fit" { + const items = [_]Item{ + .{ .weight = 100, .value = 500 }, + }; + + const result = try knapsack(&items, 50, testing.allocator); + try testing.expectEqual(@as(u32, 0), result); +} + +test "knapsack: classic example" { + const items = [_]Item{ + .{ .weight = 10, .value = 60 }, + .{ .weight = 20, .value = 100 }, + .{ .weight = 30, .value = 120 }, + }; + + const result = try knapsack(&items, 50, testing.allocator); + try testing.expectEqual(@as(u32, 220), result); +} + +test "knapsack: all items fit exactly" { + const items = [_]Item{ + .{ .weight = 10, .value = 10 }, + .{ .weight = 20, .value = 20 }, + .{ .weight = 30, .value = 30 }, + }; + + const result = try knapsack(&items, 60, testing.allocator); + try testing.expectEqual(@as(u32, 60), result); +} + +test "knapsack: fractional knapsack would be better" { + // Tests that we get 0/1 solution, not fractional + const items = [_]Item{ + .{ .weight = 10, .value = 20 }, // ratio: 2.0 + .{ .weight = 15, .value = 25 }, // ratio: 1.67 + }; + + // Fractional would take all of first + 5 units of second = 20 + 8.33 = 28.33 + // 0/1 takes the second item (best value that fits) + const result = try knapsack(&items, 15, testing.allocator); + try testing.expectEqual(@as(u32, 25), result); +} + +test "knapsack: with item selection" { + const items = [_]Item{ + .{ .weight = 5, .value = 40 }, + .{ .weight = 3, .value = 20 }, + .{ .weight = 6, .value = 10 }, + .{ .weight = 3, .value = 30 }, + }; + + const result = try knapsackWithItems(&items, 10, testing.allocator); + defer testing.allocator.free(result.selected); + + try testing.expectEqual(@as(u32, 70), result.value); + + // Should select items 0, 1, 3 (weights: 5+3+3=11... wait that's > 10) + // Actually should select items 0 and 3 (weights: 5+3=8, values: 40+30=70) + // Or items 0, 1 (weights: 5+3=8, values: 40+20=60) - no + // Let's verify the selection is optimal + var selected_weight: u32 = 0; + var selected_value: u32 = 0; + + for (items, result.selected) |item, selected| { + if (selected) { + selected_weight += item.weight; + selected_value += item.value; + } + } + + try testing.expect(selected_weight <= 10); + try testing.expectEqual(result.value, selected_value); +} + +test "knapsack: large capacity" { + const items = [_]Item{ + .{ .weight = 1000, .value = 5000 }, + .{ .weight = 2000, .value = 8000 }, + .{ .weight = 1500, .value = 6000 }, + }; + + const result = try knapsack(&items, 5000, testing.allocator); + try testing.expectEqual(@as(u32, 19000), result); +} + +test "knapsack: many small items" { + var items: [100]Item = undefined; + for (&items, 0..) |*item, i| { + item.* = Item{ + .weight = @intCast(i + 1), + .value = @intCast((i + 1) * 10), + }; + } + + const result = try knapsack(&items, 500, testing.allocator); + + // Result should be positive and bounded + try testing.expect(result > 0); + try testing.expect(result <= 50500); // Sum of all values +} + +test "knapsack: duplicate weights different values" { + const items = [_]Item{ + .{ .weight = 10, .value = 100 }, + .{ .weight = 10, .value = 50 }, + .{ .weight = 10, .value = 150 }, + }; + + const result = try knapsack(&items, 20, testing.allocator); + try testing.expectEqual(@as(u32, 250), result); // Best two +} + +test "knapsack: stress test - no stack overflow" { + var items: [1000]Item = undefined; + for (&items, 0..) |*item, i| { + item.* = Item{ + .weight = @intCast((i % 100) + 1), + .value = @intCast((i % 100) * 10), + }; + } + + const result = try knapsack(&items, 5000, testing.allocator); + + // Just verify it completes without overflow + try testing.expect(result >= 0); +} diff --git a/tiger_style/merge_sort_tiger.zig b/tiger_style/merge_sort_tiger.zig new file mode 100644 index 0000000..761e6ef --- /dev/null +++ b/tiger_style/merge_sort_tiger.zig @@ -0,0 +1,372 @@ +//! Tiger Style Merge Sort - Zero Recursion Implementation +//! +//! Demonstrates Tiger Style principles: +//! - Iterative bottom-up merge sort (no recursion) +//! - Explicit u32 indices (never usize) +//! - Heavy assertions on all array accesses +//! - Bounded loops with provable upper bounds +//! - Fail-fast on invalid inputs +//! - Simple, explicit control flow + +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; + +/// Maximum array size we support (must be bounded) +pub const MAX_ARRAY_SIZE: u32 = 1_000_000; + +/// Tiger Style merge sort - sorts array A using work buffer B +/// Both arrays must have identical length <= MAX_ARRAY_SIZE +/// Time: O(n log n), Space: O(n) for work buffer +pub fn sort(comptime T: type, A: []T, B: []T) void { + // Preconditions - assert all inputs + assert(A.len == B.len); + assert(A.len <= MAX_ARRAY_SIZE); + + const n: u32 = @intCast(A.len); + + // Handle trivial cases + if (n <= 1) { + // Postcondition: trivial arrays are already sorted + return; + } + + // Copy A to B for initial pass + copyArray(T, A, 0, n, B); + + // Bottom-up iterative merge sort + // No recursion! Loop bound: log2(n) iterations + var width: u32 = 1; + var iteration: u32 = 0; + const max_iterations: u32 = 32; // log2(MAX_ARRAY_SIZE) = ~20, use 32 for safety + + while (width < n) : (iteration += 1) { + // Assert bounded loop + assert(iteration < max_iterations); + assert(width > 0); + assert(width <= n); + + // Merge subarrays of size 'width' + var i: u32 = 0; + const merge_count_max = n / width + 1; // Upper bound on merges this iteration + var merge_count: u32 = 0; + + while (i < n) : (merge_count += 1) { + // Assert bounded inner loop + assert(merge_count <= merge_count_max); + assert(i < n); + + const left = i; + const middle = @min(i + width, n); + const right = @min(i + 2 * width, n); + + // Invariants + assert(left < middle); + assert(middle <= right); + assert(right <= n); + + // Merge on alternating passes + if (iteration % 2 == 0) { + merge(T, B, left, middle, right, A); + } else { + merge(T, A, left, middle, right, B); + } + + i = right; + } + + width = width * 2; + + // Postcondition: width increased + assert(width > 0); // Check for overflow + } + + // If even number of iterations, result is in B, copy back to A + if (iteration % 2 == 0) { + copyArray(T, B, 0, n, A); + } + + // Postcondition: array is sorted (verified in tests) +} + +/// Merge two sorted subarrays from A into B +/// Merges A[begin..middle) with A[middle..end) into B[begin..end) +fn merge( + comptime T: type, + A: []const T, + begin: u32, + middle: u32, + end: u32, + B: []T, +) void { + // Preconditions + assert(begin <= middle); + assert(middle <= end); + assert(end <= A.len); + assert(end <= B.len); + assert(A.len <= MAX_ARRAY_SIZE); + assert(B.len <= MAX_ARRAY_SIZE); + + var i: u32 = begin; // Index for left subarray + var j: u32 = middle; // Index for right subarray + var k: u32 = begin; // Index for output + + // Merge with explicit bounds + const iterations_max = end - begin; + var iterations: u32 = 0; + + while (k < end) : ({ + k += 1; + iterations += 1; + }) { + // Assert bounded loop + assert(iterations <= iterations_max); + assert(k < end); + assert(k < B.len); + + // Choose from left or right subarray + if (i < middle and (j >= end or A[i] <= A[j])) { + // Take from left + assert(i < A.len); + B[k] = A[i]; + i += 1; + } else { + // Take from right + assert(j < A.len); + assert(j < end); + B[k] = A[j]; + j += 1; + } + + // Invariants + assert(i <= middle); + assert(j <= end); + } + + // Postconditions + assert(k == end); + assert(i == middle or j == end); // One subarray exhausted +} + +/// Copy elements from A to B in range [begin, end) +fn copyArray( + comptime T: type, + A: []const T, + begin: u32, + end: u32, + B: []T, +) void { + // Preconditions + assert(begin <= end); + assert(end <= A.len); + assert(end <= B.len); + assert(A.len <= MAX_ARRAY_SIZE); + assert(B.len <= MAX_ARRAY_SIZE); + + var k: u32 = begin; + const iterations_max = end - begin; + var iterations: u32 = 0; + + while (k < end) : ({ + k += 1; + iterations += 1; + }) { + // Assert bounded loop + assert(iterations <= iterations_max); + assert(k < A.len); + assert(k < B.len); + + B[k] = A[k]; + } + + // Postcondition + assert(k == end); +} + +/// Verify array is sorted in ascending order +fn isSorted(comptime T: type, array: []const T) bool { + assert(array.len <= MAX_ARRAY_SIZE); + + if (array.len <= 1) return true; + + const n: u32 = @intCast(array.len); + var i: u32 = 1; + + while (i < n) : (i += 1) { + assert(i < array.len); + if (array[i - 1] > array[i]) { + return false; + } + } + + return true; +} + +// ============================================================================ +// Tests - Exhaustive edge case coverage +// ============================================================================ + +test "sort: empty array" { + const array: []i32 = &.{}; + const work: []i32 = &.{}; + + sort(i32, array, work); + + try testing.expect(isSorted(i32, array)); +} + +test "sort: single element" { + var array: [1]i32 = .{42}; + var work: [1]i32 = .{0}; + + sort(i32, &array, &work); + + try testing.expect(isSorted(i32, &array)); + try testing.expectEqual(@as(i32, 42), array[0]); +} + +test "sort: two elements sorted" { + var array: [2]i32 = .{ 1, 2 }; + var work: [2]i32 = .{0} ** 2; + + sort(i32, &array, &work); + + try testing.expect(isSorted(i32, &array)); + try testing.expectEqual(@as(i32, 1), array[0]); + try testing.expectEqual(@as(i32, 2), array[1]); +} + +test "sort: two elements reversed" { + var array: [2]i32 = .{ 2, 1 }; + var work: [2]i32 = .{0} ** 2; + + sort(i32, &array, &work); + + try testing.expect(isSorted(i32, &array)); + try testing.expectEqual(@as(i32, 1), array[0]); + try testing.expectEqual(@as(i32, 2), array[1]); +} + +test "sort: already sorted" { + var array: [10]i32 = .{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; + var work: [10]i32 = .{0} ** 10; + + sort(i32, &array, &work); + + try testing.expect(isSorted(i32, &array)); + for (array, 0..) |value, i| { + try testing.expectEqual(@as(i32, @intCast(i + 1)), value); + } +} + +test "sort: reverse order" { + var array: [10]i32 = .{ 10, 9, 8, 7, 6, 5, 4, 3, 2, 1 }; + var work: [10]i32 = .{0} ** 10; + + sort(i32, &array, &work); + + try testing.expect(isSorted(i32, &array)); + for (array, 0..) |value, i| { + try testing.expectEqual(@as(i32, @intCast(i + 1)), value); + } +} + +test "sort: duplicates" { + var array: [8]i32 = .{ 5, 2, 8, 2, 9, 1, 5, 5 }; + var work: [8]i32 = .{0} ** 8; + + sort(i32, &array, &work); + + try testing.expect(isSorted(i32, &array)); + // Verify specific values + try testing.expectEqual(@as(i32, 1), array[0]); + try testing.expectEqual(@as(i32, 2), array[1]); + try testing.expectEqual(@as(i32, 2), array[2]); +} + +test "sort: all same elements" { + var array: [10]i32 = .{7} ** 10; + var work: [10]i32 = .{0} ** 10; + + sort(i32, &array, &work); + + try testing.expect(isSorted(i32, &array)); + for (array) |value| { + try testing.expectEqual(@as(i32, 7), value); + } +} + +test "sort: negative numbers" { + var array: [6]i32 = .{ -5, -1, -10, 0, -3, -7 }; + var work: [6]i32 = .{0} ** 6; + + sort(i32, &array, &work); + + try testing.expect(isSorted(i32, &array)); + try testing.expectEqual(@as(i32, -10), array[0]); + try testing.expectEqual(@as(i32, 0), array[5]); +} + +test "sort: large array power of 2" { + var array: [256]i32 = undefined; + var work: [256]i32 = undefined; + + // Initialize with reverse order + for (&array, 0..) |*elem, i| { + elem.* = @intCast(255 - i); + } + + sort(i32, &array, &work); + + try testing.expect(isSorted(i32, &array)); + for (array, 0..) |value, i| { + try testing.expectEqual(@as(i32, @intCast(i)), value); + } +} + +test "sort: large array non-power of 2" { + var array: [1000]i32 = undefined; + var work: [1000]i32 = undefined; + + // Initialize with pseudorandom pattern + for (&array, 0..) |*elem, i| { + elem.* = @intCast((i * 7919) % 1000); + } + + sort(i32, &array, &work); + + try testing.expect(isSorted(i32, &array)); +} + +test "sort: stress test - verify no recursion stack overflow" { + // This would overflow stack with recursive implementation + var array: [10000]i32 = undefined; + var work: [10000]i32 = undefined; + + // Worst case: reverse sorted + for (&array, 0..) |*elem, i| { + elem.* = @intCast(9999 - i); + } + + sort(i32, &array, &work); + + try testing.expect(isSorted(i32, &array)); +} + +test "sort: different types - u32" { + var array: [5]u32 = .{ 5, 2, 8, 1, 9 }; + var work: [5]u32 = .{0} ** 5; + + sort(u32, &array, &work); + + try testing.expect(isSorted(u32, &array)); +} + +test "sort: different types - u64" { + var array: [5]u64 = .{ 5, 2, 8, 1, 9 }; + var work: [5]u64 = .{0} ** 5; + + sort(u64, &array, &work); + + try testing.expect(isSorted(u64, &array)); +} diff --git a/tiger_style/raft_consensus.zig b/tiger_style/raft_consensus.zig new file mode 100644 index 0000000..9f54218 --- /dev/null +++ b/tiger_style/raft_consensus.zig @@ -0,0 +1,514 @@ +//! Tiger Style Raft Consensus Algorithm +//! +//! Implements the Raft distributed consensus algorithm with Tiger Style discipline: +//! - Explicit state machine (no recursion) +//! - Bounded message queues with fail-fast +//! - Heavy assertions on all state transitions +//! - Deterministic testing with time simulation +//! - All operations have explicit upper bounds +//! +//! Reference: "In Search of an Understandable Consensus Algorithm" (Raft paper) + +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; + +/// Maximum number of nodes in cluster (must be bounded) +pub const MAX_NODES: u32 = 16; + +/// Maximum entries in log (must be bounded for Tiger Style) +pub const MAX_LOG_ENTRIES: u32 = 10000; + +/// Maximum pending messages per node (bounded queue) +pub const MAX_PENDING_MESSAGES: u32 = 256; + +/// Node ID type - explicit u32 +pub const NodeId = u32; + +/// Term number in Raft - monotonically increasing +pub const Term = u64; + +/// Log index - explicit u32 +pub const LogIndex = u32; + +/// Raft node states +pub const NodeState = enum(u8) { + follower, + candidate, + leader, + + pub fn validate(self: NodeState) void { + // Ensure state is valid + assert(@intFromEnum(self) <= 2); + } +}; + +/// Log entry in the replicated state machine +pub const LogEntry = struct { + term: Term, + index: LogIndex, + command: u64, // Simplified: actual systems would have arbitrary commands + + pub fn validate(self: LogEntry) void { + assert(self.index > 0); // Log indices start at 1 + assert(self.term > 0); // Terms start at 1 + } +}; + +/// Message types in Raft protocol +pub const MessageType = enum(u8) { + request_vote, + request_vote_reply, + append_entries, + append_entries_reply, +}; + +/// Raft protocol message +pub const Message = struct { + msg_type: MessageType, + term: Term, + from: NodeId, + to: NodeId, + + // RequestVote fields + candidate_id: NodeId, + last_log_index: LogIndex, + last_log_term: Term, + + // RequestVote reply + vote_granted: bool, + + // AppendEntries fields + prev_log_index: LogIndex, + prev_log_term: Term, + leader_commit: LogIndex, + + // AppendEntries reply + success: bool, + match_index: LogIndex, +}; + +/// Raft node implementing consensus +pub const RaftNode = struct { + /// Node ID + id: NodeId, + + /// Current state + state: NodeState, + + /// Current term + current_term: Term, + + /// Who we voted for in current term (0 = none) + voted_for: NodeId, + + /// Replicated log + log: [MAX_LOG_ENTRIES]LogEntry, + log_length: u32, + + /// Commit index + commit_index: LogIndex, + + /// Last applied index + last_applied: LogIndex, + + /// Leader state (only valid when state == leader) + next_index: [MAX_NODES]LogIndex, + match_index: [MAX_NODES]LogIndex, + + /// Cluster configuration + cluster_size: u32, + + /// Election timeout (in milliseconds) + election_timeout: u64, + last_heartbeat: u64, + + /// Vote tracking + votes_received: u32, + + /// Initialize a new Raft node + pub fn init(id: NodeId, cluster_size: u32) RaftNode { + // Preconditions + assert(id > 0); + assert(id <= MAX_NODES); + assert(cluster_size > 0); + assert(cluster_size <= MAX_NODES); + assert(cluster_size % 2 == 1); // Raft requires odd cluster size + + var node = RaftNode{ + .id = id, + .state = .follower, + .current_term = 1, + .voted_for = 0, + .log = undefined, + .log_length = 0, + .commit_index = 0, + .last_applied = 0, + .next_index = undefined, + .match_index = undefined, + .cluster_size = cluster_size, + .election_timeout = 150 + (id * 50), // Randomized per node + .last_heartbeat = 0, + .votes_received = 0, + }; + + // Initialize leader state + var i: u32 = 0; + while (i < MAX_NODES) : (i += 1) { + node.next_index[i] = 1; + node.match_index[i] = 0; + } + + // Postconditions + assert(node.state == .follower); + assert(node.current_term > 0); + assert(node.log_length == 0); + node.validate(); + + return node; + } + + /// Validate node invariants + pub fn validate(self: *const RaftNode) void { + // State invariants + self.state.validate(); + assert(self.id > 0); + assert(self.id <= MAX_NODES); + assert(self.current_term > 0); + assert(self.log_length <= MAX_LOG_ENTRIES); + assert(self.commit_index <= self.log_length); + assert(self.last_applied <= self.commit_index); + assert(self.cluster_size > 0); + assert(self.cluster_size <= MAX_NODES); + + // If we voted, must be for valid node + if (self.voted_for != 0) { + assert(self.voted_for <= MAX_NODES); + } + + // Vote count bounded by cluster size + assert(self.votes_received <= self.cluster_size); + } + + /// Start election (transition to candidate) + pub fn startElection(self: *RaftNode, current_time: u64) void { + // Preconditions + self.validate(); + assert(self.state == .follower or self.state == .candidate); + + // Transition to candidate + self.state = .candidate; + self.current_term += 1; + self.voted_for = self.id; // Vote for self + self.votes_received = 1; // Count our own vote + self.last_heartbeat = current_time; + + // Postconditions + assert(self.state == .candidate); + assert(self.votes_received == 1); + self.validate(); + } + + /// Receive vote in election + pub fn receiveVote(self: *RaftNode, from: NodeId, term: Term) void { + // Preconditions + self.validate(); + assert(self.state == .candidate); + assert(from > 0); + assert(from <= MAX_NODES); + assert(term == self.current_term); + + self.votes_received += 1; + + // Check if we won the election (majority) + const majority = self.cluster_size / 2 + 1; + if (self.votes_received >= majority) { + self.becomeLeader(); + } + + // Postconditions + assert(self.votes_received <= self.cluster_size); + self.validate(); + } + + /// Become leader + fn becomeLeader(self: *RaftNode) void { + // Preconditions + assert(self.state == .candidate); + + self.state = .leader; + + // Initialize leader state + var i: u32 = 0; + while (i < MAX_NODES) : (i += 1) { + self.next_index[i] = self.log_length + 1; + self.match_index[i] = 0; + } + + // Postconditions + assert(self.state == .leader); + self.validate(); + } + + /// Step down to follower (discovered higher term) + pub fn stepDown(self: *RaftNode, new_term: Term) void { + // Preconditions + self.validate(); + assert(new_term > self.current_term); + + self.state = .follower; + self.current_term = new_term; + self.voted_for = 0; + self.votes_received = 0; + + // Postconditions + assert(self.state == .follower); + assert(self.current_term == new_term); + self.validate(); + } + + /// Append entry to log (leader only) + pub fn appendEntry(self: *RaftNode, command: u64) !LogIndex { + // Preconditions + self.validate(); + assert(self.state == .leader); + + // Fail-fast: log full + if (self.log_length >= MAX_LOG_ENTRIES) { + return error.LogFull; + } + + const index = self.log_length; + assert(index < MAX_LOG_ENTRIES); + + self.log[index] = LogEntry{ + .term = self.current_term, + .index = @intCast(index + 1), // 1-indexed + .command = command, + }; + self.log[index].validate(); + + self.log_length += 1; + + // Postconditions + assert(self.log_length <= MAX_LOG_ENTRIES); + self.validate(); + + return @intCast(index + 1); + } + + /// Commit entries up to index + pub fn commitUpTo(self: *RaftNode, index: LogIndex) void { + // Preconditions + self.validate(); + assert(index <= self.log_length); + + if (index > self.commit_index) { + self.commit_index = index; + } + + // Postconditions + assert(self.commit_index <= self.log_length); + self.validate(); + } + + /// Apply committed entries + pub fn applyCommitted(self: *RaftNode) u32 { + // Preconditions + self.validate(); + assert(self.last_applied <= self.commit_index); + + var applied: u32 = 0; + + while (self.last_applied < self.commit_index) { + self.last_applied += 1; + applied += 1; + + // Bounded loop + assert(applied <= MAX_LOG_ENTRIES); + } + + // Postconditions + assert(self.last_applied == self.commit_index); + self.validate(); + + return applied; + } + + /// Check if election timeout expired + pub fn isElectionTimeoutExpired(self: *const RaftNode, current_time: u64) bool { + self.validate(); + const elapsed = current_time - self.last_heartbeat; + return elapsed > self.election_timeout; + } + + /// Reset election timer + pub fn resetElectionTimer(self: *RaftNode, current_time: u64) void { + self.validate(); + self.last_heartbeat = current_time; + } +}; + +// ============================================================================ +// Tests - Consensus algorithm verification +// ============================================================================ + +test "RaftNode: initialization" { + const node = RaftNode.init(1, 3); + + try testing.expectEqual(@as(NodeId, 1), node.id); + try testing.expectEqual(NodeState.follower, node.state); + try testing.expectEqual(@as(Term, 1), node.current_term); + try testing.expectEqual(@as(u32, 0), node.log_length); + try testing.expectEqual(@as(LogIndex, 0), node.commit_index); +} + +test "RaftNode: start election" { + var node = RaftNode.init(1, 3); + + node.startElection(100); + + try testing.expectEqual(NodeState.candidate, node.state); + try testing.expectEqual(@as(Term, 2), node.current_term); + try testing.expectEqual(@as(u32, 1), node.votes_received); + try testing.expectEqual(@as(NodeId, 1), node.voted_for); +} + +test "RaftNode: win election with majority" { + var node = RaftNode.init(1, 3); + + node.startElection(100); + try testing.expectEqual(NodeState.candidate, node.state); + + // Receive vote from another node (2/3 = majority) + node.receiveVote(2, 2); + + try testing.expectEqual(NodeState.leader, node.state); +} + +test "RaftNode: step down on higher term" { + var node = RaftNode.init(1, 3); + node.startElection(100); + + try testing.expectEqual(NodeState.candidate, node.state); + try testing.expectEqual(@as(Term, 2), node.current_term); + + // Discover higher term + node.stepDown(5); + + try testing.expectEqual(NodeState.follower, node.state); + try testing.expectEqual(@as(Term, 5), node.current_term); + try testing.expectEqual(@as(NodeId, 0), node.voted_for); +} + +test "RaftNode: append entries as leader" { + var node = RaftNode.init(1, 3); + + // Become leader + node.startElection(100); + node.receiveVote(2, 2); + try testing.expectEqual(NodeState.leader, node.state); + + // Append entries + const idx1 = try node.appendEntry(100); + const idx2 = try node.appendEntry(200); + const idx3 = try node.appendEntry(300); + + try testing.expectEqual(@as(LogIndex, 1), idx1); + try testing.expectEqual(@as(LogIndex, 2), idx2); + try testing.expectEqual(@as(LogIndex, 3), idx3); + try testing.expectEqual(@as(u32, 3), node.log_length); +} + +test "RaftNode: commit and apply entries" { + var node = RaftNode.init(1, 3); + + // Become leader and append entries + node.startElection(100); + node.receiveVote(2, 2); + _ = try node.appendEntry(100); + _ = try node.appendEntry(200); + _ = try node.appendEntry(300); + + // Commit first two entries + node.commitUpTo(2); + try testing.expectEqual(@as(LogIndex, 2), node.commit_index); + + // Apply committed entries + const applied = node.applyCommitted(); + try testing.expectEqual(@as(u32, 2), applied); + try testing.expectEqual(@as(LogIndex, 2), node.last_applied); +} + +test "RaftNode: bounded log" { + var node = RaftNode.init(1, 3); + + node.startElection(100); + node.receiveVote(2, 2); + + // Fill log to max + var i: u32 = 0; + while (i < MAX_LOG_ENTRIES) : (i += 1) { + _ = try node.appendEntry(@intCast(i)); + } + + try testing.expectEqual(MAX_LOG_ENTRIES, node.log_length); + + // Next append should fail (bounded) + const result = node.appendEntry(9999); + try testing.expectError(error.LogFull, result); +} + +test "RaftNode: election timeout" { + const node = RaftNode.init(1, 3); + + // Initially not expired + try testing.expect(!node.isElectionTimeoutExpired(100)); + + // After timeout period + try testing.expect(node.isElectionTimeoutExpired(300)); +} + +test "RaftNode: reset election timer" { + var node = RaftNode.init(1, 3); + + node.resetElectionTimer(100); + try testing.expect(!node.isElectionTimeoutExpired(200)); + + // Timer expired after timeout (election_timeout = 150 + 1*50 = 200) + try testing.expect(node.isElectionTimeoutExpired(301)); +} + +test "RaftNode: log validation" { + var node = RaftNode.init(1, 3); + node.startElection(100); + node.receiveVote(2, 2); + + _ = try node.appendEntry(42); + + const entry = node.log[0]; + entry.validate(); // Should not fail + + try testing.expectEqual(@as(u64, 42), entry.command); + try testing.expectEqual(@as(LogIndex, 1), entry.index); +} + +test "RaftNode: cluster size must be odd" { + // This demonstrates the assertion - odd cluster sizes required + const node = RaftNode.init(1, 5); + try testing.expectEqual(@as(u32, 5), node.cluster_size); +} + +test "RaftNode: majority calculation" { + var node = RaftNode.init(1, 5); + node.startElection(100); + + // Need 3 votes for majority in cluster of 5 + try testing.expectEqual(NodeState.candidate, node.state); + + node.receiveVote(2, 2); + try testing.expectEqual(NodeState.candidate, node.state); + + node.receiveVote(3, 2); + try testing.expectEqual(NodeState.leader, node.state); +} diff --git a/tiger_style/ring_buffer.zig b/tiger_style/ring_buffer.zig new file mode 100644 index 0000000..8b8ce69 --- /dev/null +++ b/tiger_style/ring_buffer.zig @@ -0,0 +1,471 @@ +//! Tiger Style Ring Buffer - Bounded FIFO Queue +//! +//! Demonstrates Tiger Style for production data structures: +//! - Fixed capacity with compile-time guarantees +//! - All operations O(1) with explicit bounds +//! - Assertions on every state transition +//! - Explicit u32 indices (never usize) +//! - Fail-fast on full/empty violations +//! - Zero allocations after initialization + +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; + +/// Ring buffer with fixed capacity +/// Generic over element type T and capacity +pub fn RingBuffer(comptime T: type, comptime capacity: u32) type { + // Precondition: capacity must be reasonable + comptime { + assert(capacity > 0); + assert(capacity <= 1_000_000); // Sanity limit + } + + return struct { + const Self = @This(); + + /// Fixed-size storage array + buffer: [capacity]T, + + /// Index of first element (read position) + head: u32, + + /// Index where next element will be written + tail: u32, + + /// Number of elements currently in buffer + count: u32, + + /// Initialize empty ring buffer + pub fn init() Self { + var self = Self{ + .buffer = undefined, + .head = 0, + .tail = 0, + .count = 0, + }; + + // Postconditions + assert(self.head == 0); + assert(self.tail == 0); + assert(self.count == 0); + assert(self.isEmpty()); + assert(!self.isFull()); + + return self; + } + + /// Push element to back of buffer + /// Returns error if buffer is full (fail-fast) + pub fn push(self: *Self, item: T) error{BufferFull}!void { + // Preconditions + assert(self.count <= capacity); + assert(self.head < capacity); + assert(self.tail < capacity); + + // Fail-fast: buffer full + if (self.count >= capacity) { + return error.BufferFull; + } + + assert(!self.isFull()); + + // Write element + self.buffer[self.tail] = item; + + // Advance tail with wraparound + self.tail += 1; + if (self.tail >= capacity) { + self.tail = 0; + } + + self.count += 1; + + // Postconditions + assert(self.count <= capacity); + assert(self.count > 0); + assert(self.tail < capacity); + } + + /// Pop element from front of buffer + /// Returns error if buffer is empty (fail-fast) + pub fn pop(self: *Self) error{BufferEmpty}!T { + // Preconditions + assert(self.count <= capacity); + assert(self.head < capacity); + assert(self.tail < capacity); + + // Fail-fast: buffer empty + if (self.count == 0) { + return error.BufferEmpty; + } + + assert(!self.isEmpty()); + + // Read element + const item = self.buffer[self.head]; + + // Advance head with wraparound + self.head += 1; + if (self.head >= capacity) { + self.head = 0; + } + + self.count -= 1; + + // Postconditions + assert(self.count < capacity); + assert(self.head < capacity); + + return item; + } + + /// Peek at front element without removing + /// Returns error if buffer is empty + pub fn peek(self: *const Self) error{BufferEmpty}!T { + // Preconditions + assert(self.count <= capacity); + assert(self.head < capacity); + + if (self.count == 0) { + return error.BufferEmpty; + } + + return self.buffer[self.head]; + } + + /// Peek at back element (most recently pushed) + /// Returns error if buffer is empty + pub fn peekBack(self: *const Self) error{BufferEmpty}!T { + // Preconditions + assert(self.count <= capacity); + assert(self.tail < capacity); + + if (self.count == 0) { + return error.BufferEmpty; + } + + // Calculate index of last element + const back_index = if (self.tail > 0) self.tail - 1 else capacity - 1; + assert(back_index < capacity); + + return self.buffer[back_index]; + } + + /// Check if buffer is empty + pub fn isEmpty(self: *const Self) bool { + assert(self.count <= capacity); + return self.count == 0; + } + + /// Check if buffer is full + pub fn isFull(self: *const Self) bool { + assert(self.count <= capacity); + return self.count == capacity; + } + + /// Get number of elements in buffer + pub fn len(self: *const Self) u32 { + assert(self.count <= capacity); + return self.count; + } + + /// Get remaining capacity + pub fn available(self: *const Self) u32 { + assert(self.count <= capacity); + return capacity - self.count; + } + + /// Clear all elements + pub fn clear(self: *Self) void { + // Preconditions + assert(self.count <= capacity); + + self.head = 0; + self.tail = 0; + self.count = 0; + + // Postconditions + assert(self.isEmpty()); + assert(!self.isFull() or capacity == 0); + } + + /// Get element at index (0 = front, count-1 = back) + /// Returns error if index out of bounds + pub fn get(self: *const Self, index: u32) error{IndexOutOfBounds}!T { + // Preconditions + assert(self.count <= capacity); + assert(self.head < capacity); + + if (index >= self.count) { + return error.IndexOutOfBounds; + } + + // Calculate actual buffer index with wraparound + var buffer_index = self.head + index; + if (buffer_index >= capacity) { + buffer_index -= capacity; + } + + assert(buffer_index < capacity); + return self.buffer[buffer_index]; + } + + /// Iterator for iterating over elements in FIFO order + pub const Iterator = struct { + buffer: *const Self, + position: u32, + + pub fn next(iter: *Iterator) ?T { + if (iter.position >= iter.buffer.count) { + return null; + } + + const item = iter.buffer.get(iter.position) catch unreachable; + iter.position += 1; + + return item; + } + }; + + /// Get iterator for buffer + pub fn iterator(self: *const Self) Iterator { + return Iterator{ + .buffer = self, + .position = 0, + }; + } + }; +} + +// ============================================================================ +// Tests - Exhaustive edge case coverage +// ============================================================================ + +test "RingBuffer: initialization" { + const Buffer = RingBuffer(i32, 8); + const buf = Buffer.init(); + + try testing.expect(buf.isEmpty()); + try testing.expect(!buf.isFull()); + try testing.expectEqual(@as(u32, 0), buf.len()); + try testing.expectEqual(@as(u32, 8), buf.available()); +} + +test "RingBuffer: push and pop single element" { + const Buffer = RingBuffer(i32, 8); + var buf = Buffer.init(); + + try buf.push(42); + + try testing.expect(!buf.isEmpty()); + try testing.expectEqual(@as(u32, 1), buf.len()); + + const value = try buf.pop(); + + try testing.expectEqual(@as(i32, 42), value); + try testing.expect(buf.isEmpty()); +} + +test "RingBuffer: push to full" { + const Buffer = RingBuffer(i32, 4); + var buf = Buffer.init(); + + try buf.push(1); + try buf.push(2); + try buf.push(3); + try buf.push(4); + + try testing.expect(buf.isFull()); + try testing.expectEqual(@as(u32, 4), buf.len()); + + // Try to push one more - should fail + const result = buf.push(5); + try testing.expectError(error.BufferFull, result); +} + +test "RingBuffer: pop from empty" { + const Buffer = RingBuffer(i32, 4); + var buf = Buffer.init(); + + const result = buf.pop(); + try testing.expectError(error.BufferEmpty, result); +} + +test "RingBuffer: FIFO ordering" { + const Buffer = RingBuffer(i32, 8); + var buf = Buffer.init(); + + try buf.push(10); + try buf.push(20); + try buf.push(30); + + try testing.expectEqual(@as(i32, 10), try buf.pop()); + try testing.expectEqual(@as(i32, 20), try buf.pop()); + try testing.expectEqual(@as(i32, 30), try buf.pop()); +} + +test "RingBuffer: wraparound" { + const Buffer = RingBuffer(i32, 4); + var buf = Buffer.init(); + + // Fill buffer + try buf.push(1); + try buf.push(2); + try buf.push(3); + try buf.push(4); + + // Remove two + _ = try buf.pop(); + _ = try buf.pop(); + + // Add two more (will wrap around) + try buf.push(5); + try buf.push(6); + + // Verify order + try testing.expectEqual(@as(i32, 3), try buf.pop()); + try testing.expectEqual(@as(i32, 4), try buf.pop()); + try testing.expectEqual(@as(i32, 5), try buf.pop()); + try testing.expectEqual(@as(i32, 6), try buf.pop()); + + try testing.expect(buf.isEmpty()); +} + +test "RingBuffer: peek" { + const Buffer = RingBuffer(i32, 4); + var buf = Buffer.init(); + + try buf.push(100); + try buf.push(200); + + // Peek should not remove element + try testing.expectEqual(@as(i32, 100), try buf.peek()); + try testing.expectEqual(@as(u32, 2), buf.len()); + + // Verify element still there + try testing.expectEqual(@as(i32, 100), try buf.pop()); +} + +test "RingBuffer: peekBack" { + const Buffer = RingBuffer(i32, 4); + var buf = Buffer.init(); + + try buf.push(100); + try buf.push(200); + try buf.push(300); + + try testing.expectEqual(@as(i32, 300), try buf.peekBack()); + try testing.expectEqual(@as(u32, 3), buf.len()); +} + +test "RingBuffer: clear" { + const Buffer = RingBuffer(i32, 4); + var buf = Buffer.init(); + + try buf.push(1); + try buf.push(2); + try buf.push(3); + + buf.clear(); + + try testing.expect(buf.isEmpty()); + try testing.expectEqual(@as(u32, 0), buf.len()); + + // Can push again after clear + try buf.push(10); + try testing.expectEqual(@as(i32, 10), try buf.pop()); +} + +test "RingBuffer: get by index" { + const Buffer = RingBuffer(i32, 8); + var buf = Buffer.init(); + + try buf.push(10); + try buf.push(20); + try buf.push(30); + + try testing.expectEqual(@as(i32, 10), try buf.get(0)); + try testing.expectEqual(@as(i32, 20), try buf.get(1)); + try testing.expectEqual(@as(i32, 30), try buf.get(2)); + + // Out of bounds + const result = buf.get(3); + try testing.expectError(error.IndexOutOfBounds, result); +} + +test "RingBuffer: iterator" { + const Buffer = RingBuffer(i32, 8); + var buf = Buffer.init(); + + try buf.push(1); + try buf.push(2); + try buf.push(3); + try buf.push(4); + + var iter = buf.iterator(); + + try testing.expectEqual(@as(i32, 1), iter.next().?); + try testing.expectEqual(@as(i32, 2), iter.next().?); + try testing.expectEqual(@as(i32, 3), iter.next().?); + try testing.expectEqual(@as(i32, 4), iter.next().?); + try testing.expectEqual(@as(?i32, null), iter.next()); +} + +test "RingBuffer: stress test - many operations" { + const Buffer = RingBuffer(i32, 64); + var buf = Buffer.init(); + + // Perform many push/pop operations + var i: i32 = 0; + while (i < 1000) : (i += 1) { + // Only push if not full + if (!buf.isFull()) { + try buf.push(i); + } + + // Pop every other iteration + if (@rem(i, 2) == 0 and !buf.isEmpty()) { + _ = try buf.pop(); + } + } + + // Buffer should have some elements + try testing.expect(!buf.isEmpty()); +} + +test "RingBuffer: capacity 1" { + const Buffer = RingBuffer(i32, 1); + var buf = Buffer.init(); + + try buf.push(42); + try testing.expect(buf.isFull()); + + try testing.expectEqual(@as(i32, 42), try buf.pop()); + try testing.expect(buf.isEmpty()); +} + +test "RingBuffer: different types - u64" { + const Buffer = RingBuffer(u64, 4); + var buf = Buffer.init(); + + try buf.push(1000000000000); + try testing.expectEqual(@as(u64, 1000000000000), try buf.pop()); +} + +test "RingBuffer: struct type" { + const Point = struct { x: i32, y: i32 }; + const Buffer = RingBuffer(Point, 4); + var buf = Buffer.init(); + + try buf.push(.{ .x = 10, .y = 20 }); + try buf.push(.{ .x = 30, .y = 40 }); + + const p1 = try buf.pop(); + try testing.expectEqual(@as(i32, 10), p1.x); + try testing.expectEqual(@as(i32, 20), p1.y); + + const p2 = try buf.pop(); + try testing.expectEqual(@as(i32, 30), p2.x); + try testing.expectEqual(@as(i32, 40), p2.y); +} diff --git a/tiger_style/robin_hood_hash.zig b/tiger_style/robin_hood_hash.zig new file mode 100644 index 0000000..5d4cbcc --- /dev/null +++ b/tiger_style/robin_hood_hash.zig @@ -0,0 +1,509 @@ +//! Tiger Style Robin Hood Hash Table +//! +//! Cache-efficient hash table with Robin Hood hashing for fairness. +//! Follows TigerBeetle's data structure principles: +//! - Fixed capacity (no dynamic resizing) +//! - Explicit load factor bounds +//! - Linear probing with Robin Hood swapping +//! - Heavy assertions on all invariants +//! - Bounded probe distances +//! - Cache-friendly memory layout + +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; + +/// Maximum load factor before rejecting inserts (bounded) +pub const MAX_LOAD_FACTOR_PERCENT: u32 = 90; + +/// Maximum probe distance (bounded search) +pub const MAX_PROBE_DISTANCE: u32 = 64; + +/// Robin Hood Hash Table +pub fn RobinHoodHashMap(comptime K: type, comptime V: type, comptime capacity: u32) type { + // Preconditions + comptime { + assert(capacity > 0); + assert(capacity <= 1_000_000); // Sanity limit + // Capacity should be power of 2 for fast modulo + assert(capacity & (capacity - 1) == 0); + } + + return struct { + const Self = @This(); + + /// Entry in hash table + const Entry = struct { + key: K, + value: V, + occupied: bool, + psl: u32, // Probe sequence length + + fn init() Entry { + return Entry{ + .key = undefined, + .value = undefined, + .occupied = false, + .psl = 0, + }; + } + }; + + /// Fixed-size storage + entries: [capacity]Entry, + + /// Number of occupied entries + count: u32, + + /// Initialize empty hash map + pub fn init() Self { + var map = Self{ + .entries = undefined, + .count = 0, + }; + + // Initialize all entries + var i: u32 = 0; + while (i < capacity) : (i += 1) { + map.entries[i] = Entry.init(); + } + + // Postconditions + assert(map.count == 0); + assert(map.isEmpty()); + map.validate(); + + return map; + } + + /// Validate hash map invariants + fn validate(self: *const Self) void { + assert(self.count <= capacity); + + // Verify PSL invariants (in debug builds) + if (std.debug.runtime_safety) { + var i: u32 = 0; + var occupied: u32 = 0; + while (i < capacity) : (i += 1) { + if (self.entries[i].occupied) { + occupied += 1; + // PSL bounded + assert(self.entries[i].psl < MAX_PROBE_DISTANCE); + // PSL matches actual distance + const hash_index = self.hashKey(self.entries[i].key); + const actual_psl = self.distance(hash_index, i); + assert(self.entries[i].psl == actual_psl); + } + } + assert(occupied == self.count); + } + } + + /// Hash function + fn hashKey(self: *const Self, key: K) u32 { + _ = self; + // Simple hash for integers - use proper hash for complex types + comptime { + assert(@sizeOf(K) <= 8); // Support up to 64-bit keys + } + const h = switch (@sizeOf(K)) { + 1 => @as(u64, @as(u8, @bitCast(key))), + 2 => @as(u64, @as(u16, @bitCast(key))), + 4 => @as(u64, @as(u32, @bitCast(key))), + 8 => @as(u64, @bitCast(key)), + else => @compileError("Unsupported key size"), + }; + // Multiply by golden ratio and take upper bits + const golden = 0x9e3779b97f4a7c15; + const hash = (h *% golden) >> 32; + return @intCast(hash & (capacity - 1)); + } + + /// Calculate distance between indices (with wraparound) + fn distance(self: *const Self, from: u32, to: u32) u32 { + _ = self; + assert(from < capacity); + assert(to < capacity); + + if (to >= from) { + return to - from; + } else { + return (capacity - from) + to; + } + } + + /// Insert key-value pair + pub fn put(self: *Self, key: K, value: V) !void { + // Preconditions + self.validate(); + + // Fail-fast: check load factor + const load_percent = (self.count * 100) / capacity; + if (load_percent >= MAX_LOAD_FACTOR_PERCENT) { + return error.HashMapFull; + } + + var entry = Entry{ + .key = key, + .value = value, + .occupied = true, + .psl = 0, + }; + + var index = self.hashKey(key); + var probes: u32 = 0; + + while (probes < MAX_PROBE_DISTANCE) : (probes += 1) { + assert(index < capacity); + + // Empty slot - insert here + if (!self.entries[index].occupied) { + self.entries[index] = entry; + self.count += 1; + + // Postconditions + assert(self.count <= capacity); + self.validate(); + return; + } + + // Key already exists - update value + if (self.entries[index].key == key) { + self.entries[index].value = value; + self.validate(); + return; + } + + // Robin Hood: swap if current entry is richer (lower PSL) + if (entry.psl > self.entries[index].psl) { + // Swap entries + const temp = self.entries[index]; + self.entries[index] = entry; + entry = temp; + } + + // Move to next slot + entry.psl += 1; + index = (index + 1) & (capacity - 1); + } + + // Fail-fast: probe distance exceeded + return error.ProbeDistanceExceeded; + } + + /// Get value for key + pub fn get(self: *const Self, key: K) ?V { + self.validate(); + + var index = self.hashKey(key); + var psl: u32 = 0; + + while (psl < MAX_PROBE_DISTANCE) : (psl += 1) { + assert(index < capacity); + + // Empty slot - key not found + if (!self.entries[index].occupied) { + return null; + } + + // Found key + if (self.entries[index].key == key) { + return self.entries[index].value; + } + + // Robin Hood: if we've probed farther than entry's PSL, key doesn't exist + if (psl > self.entries[index].psl) { + return null; + } + + index = (index + 1) & (capacity - 1); + } + + return null; + } + + /// Remove key from map + pub fn remove(self: *Self, key: K) bool { + self.validate(); + + var index = self.hashKey(key); + var psl: u32 = 0; + + while (psl < MAX_PROBE_DISTANCE) : (psl += 1) { + assert(index < capacity); + + if (!self.entries[index].occupied) { + return false; + } + + if (self.entries[index].key == key) { + // Found - now backshift to maintain Robin Hood invariant + self.backshift(index); + self.count -= 1; + self.validate(); + return true; + } + + if (psl > self.entries[index].psl) { + return false; + } + + index = (index + 1) & (capacity - 1); + } + + return false; + } + + /// Backshift entries after removal + fn backshift(self: *Self, start: u32) void { + var index = start; + var iterations: u32 = 0; + + while (iterations < capacity) : (iterations += 1) { + const next_index = (index + 1) & (capacity - 1); + + // Stop if next is empty or has PSL of 0 + if (!self.entries[next_index].occupied or self.entries[next_index].psl == 0) { + self.entries[index].occupied = false; + self.entries[index].psl = 0; + return; + } + + // Shift entry back and decrease PSL + self.entries[index] = self.entries[next_index]; + self.entries[index].psl -= 1; + + index = next_index; + } + + // Should never reach here + unreachable; + } + + /// Check if map is empty + pub fn isEmpty(self: *const Self) bool { + assert(self.count <= capacity); + return self.count == 0; + } + + /// Get number of entries + pub fn len(self: *const Self) u32 { + assert(self.count <= capacity); + return self.count; + } + + /// Clear all entries + pub fn clear(self: *Self) void { + self.validate(); + + var i: u32 = 0; + while (i < capacity) : (i += 1) { + self.entries[i] = Entry.init(); + } + + self.count = 0; + + // Postconditions + assert(self.isEmpty()); + self.validate(); + } + + /// Calculate current load factor percentage + pub fn loadFactor(self: *const Self) u32 { + assert(self.count <= capacity); + return (self.count * 100) / capacity; + } + + /// Get maximum PSL in table (for statistics) + pub fn maxPSL(self: *const Self) u32 { + var max: u32 = 0; + var i: u32 = 0; + while (i < capacity) : (i += 1) { + if (self.entries[i].occupied and self.entries[i].psl > max) { + max = self.entries[i].psl; + } + } + return max; + } + }; +} + +// ============================================================================ +// Tests - Hash table verification +// ============================================================================ + +test "RobinHoodHashMap: initialization" { + const Map = RobinHoodHashMap(u32, u32, 16); + const map = Map.init(); + + try testing.expect(map.isEmpty()); + try testing.expectEqual(@as(u32, 0), map.len()); +} + +test "RobinHoodHashMap: insert and get" { + const Map = RobinHoodHashMap(u32, u32, 16); + var map = Map.init(); + + try map.put(1, 100); + try map.put(2, 200); + try map.put(3, 300); + + try testing.expectEqual(@as(u32, 3), map.len()); + try testing.expectEqual(@as(u32, 100), map.get(1).?); + try testing.expectEqual(@as(u32, 200), map.get(2).?); + try testing.expectEqual(@as(u32, 300), map.get(3).?); +} + +test "RobinHoodHashMap: update existing key" { + const Map = RobinHoodHashMap(u32, u32, 16); + var map = Map.init(); + + try map.put(1, 100); + try testing.expectEqual(@as(u32, 100), map.get(1).?); + + try map.put(1, 999); + try testing.expectEqual(@as(u32, 999), map.get(1).?); + try testing.expectEqual(@as(u32, 1), map.len()); +} + +test "RobinHoodHashMap: get non-existent key" { + const Map = RobinHoodHashMap(u32, u32, 16); + var map = Map.init(); + + try map.put(1, 100); + + try testing.expectEqual(@as(?u32, null), map.get(999)); +} + +test "RobinHoodHashMap: remove" { + const Map = RobinHoodHashMap(u32, u32, 16); + var map = Map.init(); + + try map.put(1, 100); + try map.put(2, 200); + try map.put(3, 300); + + try testing.expect(map.remove(2)); + try testing.expectEqual(@as(u32, 2), map.len()); + try testing.expectEqual(@as(?u32, null), map.get(2)); + try testing.expectEqual(@as(u32, 100), map.get(1).?); + try testing.expectEqual(@as(u32, 300), map.get(3).?); +} + +test "RobinHoodHashMap: remove non-existent" { + const Map = RobinHoodHashMap(u32, u32, 16); + var map = Map.init(); + + try map.put(1, 100); + + try testing.expect(!map.remove(999)); + try testing.expectEqual(@as(u32, 1), map.len()); +} + +test "RobinHoodHashMap: clear" { + const Map = RobinHoodHashMap(u32, u32, 16); + var map = Map.init(); + + try map.put(1, 100); + try map.put(2, 200); + + map.clear(); + + try testing.expect(map.isEmpty()); + try testing.expectEqual(@as(?u32, null), map.get(1)); +} + +test "RobinHoodHashMap: load factor" { + const Map = RobinHoodHashMap(u32, u32, 16); + var map = Map.init(); + + try map.put(1, 100); + try testing.expectEqual(@as(u32, 6), map.loadFactor()); // 1/16 = 6% + + try map.put(2, 200); + try map.put(3, 300); + try testing.expectEqual(@as(u32, 18), map.loadFactor()); // 3/16 = 18% +} + +test "RobinHoodHashMap: bounded load factor" { + const Map = RobinHoodHashMap(u32, u32, 16); + var map = Map.init(); + + // Fill to 90% capacity + var i: u32 = 0; + while (i < 14) : (i += 1) { // 14/16 = 87.5% + try map.put(i, i * 100); + } + + try testing.expect(map.loadFactor() < MAX_LOAD_FACTOR_PERCENT); + + // One more should still work + try map.put(100, 999); + + // But filling beyond 90% should fail + const result = map.put(200, 888); + try testing.expectError(error.HashMapFull, result); +} + +test "RobinHoodHashMap: Robin Hood swapping" { + const Map = RobinHoodHashMap(u32, u32, 16); + var map = Map.init(); + + // Insert elements that will cause collisions + try map.put(0, 100); // hash to slot 0 + try map.put(16, 200); // hash to slot 0, gets displaced + try map.put(32, 300); // hash to slot 0, gets displaced further + + // All should be retrievable + try testing.expectEqual(@as(u32, 100), map.get(0).?); + try testing.expectEqual(@as(u32, 200), map.get(16).?); + try testing.expectEqual(@as(u32, 300), map.get(32).?); + + // PSL should be bounded + try testing.expect(map.maxPSL() < MAX_PROBE_DISTANCE); +} + +test "RobinHoodHashMap: stress test" { + const Map = RobinHoodHashMap(u32, u32, 128); + var map = Map.init(); + + // Insert many elements + var i: u32 = 0; + while (i < 100) : (i += 1) { + try map.put(i, i * 10); + } + + // Verify all present + i = 0; + while (i < 100) : (i += 1) { + try testing.expectEqual(i * 10, map.get(i).?); + } + + // Remove some + i = 0; + while (i < 50) : (i += 2) { + try testing.expect(map.remove(i)); + } + + // Verify correct ones remain + i = 0; + while (i < 100) : (i += 1) { + if (i % 2 == 0 and i < 50) { + try testing.expectEqual(@as(?u32, null), map.get(i)); + } else { + try testing.expectEqual(i * 10, map.get(i).?); + } + } +} + +test "RobinHoodHashMap: different value types" { + const Map = RobinHoodHashMap(u32, [4]u8, 16); + var map = Map.init(); + + try map.put(1, [_]u8{ 'a', 'b', 'c', 'd' }); + try map.put(2, [_]u8{ 'x', 'y', 'z', 'w' }); + + const val = map.get(1).?; + try testing.expectEqual(@as(u8, 'a'), val[0]); + try testing.expectEqual(@as(u8, 'd'), val[3]); +} diff --git a/tiger_style/skip_list.zig b/tiger_style/skip_list.zig new file mode 100644 index 0000000..1a05507 --- /dev/null +++ b/tiger_style/skip_list.zig @@ -0,0 +1,510 @@ +//! Tiger Style Skip List +//! +//! Probabilistic data structure for ordered sets/maps. +//! Foundation for LSM trees and databases. +//! Follows Tiger Style with: +//! - Bounded maximum level +//! - Deterministic randomness for testing +//! - No recursion (iterative traversal) +//! - Heavy assertions on all operations +//! - Explicit memory management + +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; + +/// Maximum level in skip list (must be bounded) +pub const MAX_LEVEL: u32 = 16; + +/// Probability for level promotion (1/4 for good balance) +const PROMOTION_PROBABILITY: u32 = 4; + +/// Skip List Node +fn SkipListNode(comptime K: type, comptime V: type) type { + return struct { + const Self = @This(); + + key: K, + value: V, + level: u32, + forward: [MAX_LEVEL]?*Self, + + fn init(key: K, value: V, level: u32, allocator: std.mem.Allocator) !*Self { + assert(level > 0); + assert(level <= MAX_LEVEL); + + const node = try allocator.create(Self); + node.* = Self{ + .key = key, + .value = value, + .level = level, + .forward = undefined, + }; + + // Initialize forward pointers + var i: u32 = 0; + while (i < MAX_LEVEL) : (i += 1) { + node.forward[i] = null; + } + + return node; + } + + fn deinit(self: *Self, allocator: std.mem.Allocator) void { + allocator.destroy(self); + } + }; +} + +/// Skip List - ordered map +pub fn SkipList(comptime K: type, comptime V: type) type { + return struct { + const Self = @This(); + const Node = SkipListNode(K, V); + + /// Sentinel head node + head: *Node, + + /// Current maximum level + max_level: u32, + + /// Number of elements + count: u32, + + /// RNG for level generation (deterministic seed for testing) + rng: std.Random.DefaultPrng, + + /// Allocator + allocator: std.mem.Allocator, + + /// Initialize skip list + pub fn init(allocator: std.mem.Allocator, seed: u64) !Self { + // Create sentinel head with maximum level + const head = try Node.init(undefined, undefined, MAX_LEVEL, allocator); + + var list = Self{ + .head = head, + .max_level = 1, + .count = 0, + .rng = std.Random.DefaultPrng.init(seed), + .allocator = allocator, + }; + + // Postconditions + assert(list.count == 0); + assert(list.max_level > 0); + assert(list.max_level <= MAX_LEVEL); + list.validate(); + + return list; + } + + /// Deinitialize skip list + pub fn deinit(self: *Self) void { + self.validate(); + + // Free all nodes iteratively (no recursion!) + var current = self.head.forward[0]; + var iterations: u32 = 0; + const max_iterations = self.count + 1; + + while (current) |node| : (iterations += 1) { + assert(iterations <= max_iterations); + const next = node.forward[0]; + node.deinit(self.allocator); + current = next; + } + + // Free head + self.head.deinit(self.allocator); + } + + /// Validate skip list invariants + fn validate(self: *const Self) void { + assert(self.max_level > 0); + assert(self.max_level <= MAX_LEVEL); + + if (std.debug.runtime_safety) { + // Verify count matches actual nodes + var current = self.head.forward[0]; + var actual_count: u32 = 0; + var prev_key: ?K = null; + + while (current) |node| : (actual_count += 1) { + assert(actual_count <= self.count); // Prevent infinite loop + assert(node.level > 0); + assert(node.level <= MAX_LEVEL); + + // Verify ordering + if (prev_key) |pk| { + assert(pk < node.key); + } + prev_key = node.key; + + current = node.forward[0]; + } + + assert(actual_count == self.count); + } + } + + /// Generate random level for new node (deterministic with seed) + fn randomLevel(self: *Self) u32 { + var level: u32 = 1; + + // Bounded loop for level generation + while (level < MAX_LEVEL) : (level += 1) { + const r = self.rng.random().int(u32); + if (r % PROMOTION_PROBABILITY != 0) { + break; + } + } + + assert(level > 0); + assert(level <= MAX_LEVEL); + return level; + } + + /// Insert key-value pair + pub fn insert(self: *Self, key: K, value: V) !void { + // Preconditions + self.validate(); + + // Track path for insertion + var update: [MAX_LEVEL]?*Node = undefined; + var i: u32 = 0; + while (i < MAX_LEVEL) : (i += 1) { + update[i] = null; + } + + // Find insertion point (iterative, no recursion!) + var current = self.head; + var level = self.max_level; + + while (level > 0) { + level -= 1; + + var iterations: u32 = 0; + while (current.forward[level]) |next| : (iterations += 1) { + assert(iterations <= self.count + 1); // Bounded search + + if (next.key >= key) break; + current = next; + } + + update[level] = current; + } + + // Check if key already exists + if (current.forward[0]) |existing| { + if (existing.key == key) { + // Update existing value + existing.value = value; + self.validate(); + return; + } + } + + // Generate level for new node + const new_level = self.randomLevel(); + + // Update max_level if needed + if (new_level > self.max_level) { + var l = self.max_level; + while (l < new_level) : (l += 1) { + update[l] = self.head; + } + self.max_level = new_level; + } + + // Create new node + const new_node = try Node.init(key, value, new_level, self.allocator); + + // Insert node at all levels + var insert_level: u32 = 0; + while (insert_level < new_level) : (insert_level += 1) { + if (update[insert_level]) |prev| { + new_node.forward[insert_level] = prev.forward[insert_level]; + prev.forward[insert_level] = new_node; + } + } + + self.count += 1; + + // Postconditions + assert(self.count > 0); + self.validate(); + } + + /// Search for key + pub fn get(self: *const Self, key: K) ?V { + self.validate(); + + var current: ?*Node = self.head; + var level = self.max_level; + + while (level > 0 and current != null) { + level -= 1; + + var iterations: u32 = 0; + while (current.?.forward[level]) |next| : (iterations += 1) { + assert(iterations <= self.count + 1); // Bounded search + + if (next.key == key) { + return next.value; + } + if (next.key > key) break; + + current = next; + } + } + + return null; + } + + /// Remove key from skip list + pub fn remove(self: *Self, key: K) bool { + self.validate(); + + // Track nodes to update + var update: [MAX_LEVEL]?*Node = undefined; + var i: u32 = 0; + while (i < MAX_LEVEL) : (i += 1) { + update[i] = null; + } + + // Find node to remove + var current = self.head; + var level = self.max_level; + + while (level > 0) { + level -= 1; + + var iterations: u32 = 0; + while (current.forward[level]) |next| : (iterations += 1) { + assert(iterations <= self.count + 1); + + if (next.key >= key) break; + current = next; + } + + update[level] = current; + } + + // Check if key exists + const target = if (current.forward[0]) |n| + if (n.key == key) n else null + else + null; + + if (target == null) return false; + + // Remove node from all levels + var remove_level: u32 = 0; + while (remove_level < target.?.level) : (remove_level += 1) { + if (update[remove_level]) |prev| { + prev.forward[remove_level] = target.?.forward[remove_level]; + } + } + + // Free node + target.?.deinit(self.allocator); + self.count -= 1; + + // Update max_level if needed + while (self.max_level > 1 and self.head.forward[self.max_level - 1] == null) { + self.max_level -= 1; + } + + // Postconditions + self.validate(); + return true; + } + + /// Check if skip list is empty + pub fn isEmpty(self: *const Self) bool { + assert(self.count <= std.math.maxInt(u32)); + return self.count == 0; + } + + /// Get number of elements + pub fn len(self: *const Self) u32 { + assert(self.count <= std.math.maxInt(u32)); + return self.count; + } + }; +} + +// ============================================================================ +// Tests +// ============================================================================ + +test "SkipList: initialization" { + const List = SkipList(u32, u32); + var list = try List.init(testing.allocator, 42); + defer list.deinit(); + + try testing.expect(list.isEmpty()); + try testing.expectEqual(@as(u32, 0), list.len()); +} + +test "SkipList: insert and get" { + const List = SkipList(u32, u32); + var list = try List.init(testing.allocator, 42); + defer list.deinit(); + + try list.insert(10, 100); + try list.insert(20, 200); + try list.insert(30, 300); + + try testing.expectEqual(@as(u32, 3), list.len()); + try testing.expectEqual(@as(u32, 100), list.get(10).?); + try testing.expectEqual(@as(u32, 200), list.get(20).?); + try testing.expectEqual(@as(u32, 300), list.get(30).?); +} + +test "SkipList: ordered insertion" { + const List = SkipList(u32, u32); + var list = try List.init(testing.allocator, 42); + defer list.deinit(); + + // Insert out of order + try list.insert(30, 300); + try list.insert(10, 100); + try list.insert(20, 200); + + // Should still be accessible + try testing.expectEqual(@as(u32, 100), list.get(10).?); + try testing.expectEqual(@as(u32, 200), list.get(20).?); + try testing.expectEqual(@as(u32, 300), list.get(30).?); +} + +test "SkipList: update existing key" { + const List = SkipList(u32, u32); + var list = try List.init(testing.allocator, 42); + defer list.deinit(); + + try list.insert(10, 100); + try testing.expectEqual(@as(u32, 100), list.get(10).?); + + try list.insert(10, 999); + try testing.expectEqual(@as(u32, 999), list.get(10).?); + try testing.expectEqual(@as(u32, 1), list.len()); +} + +test "SkipList: get non-existent key" { + const List = SkipList(u32, u32); + var list = try List.init(testing.allocator, 42); + defer list.deinit(); + + try list.insert(10, 100); + + try testing.expectEqual(@as(?u32, null), list.get(999)); +} + +test "SkipList: remove" { + const List = SkipList(u32, u32); + var list = try List.init(testing.allocator, 42); + defer list.deinit(); + + try list.insert(10, 100); + try list.insert(20, 200); + try list.insert(30, 300); + + try testing.expect(list.remove(20)); + try testing.expectEqual(@as(u32, 2), list.len()); + try testing.expectEqual(@as(?u32, null), list.get(20)); + try testing.expectEqual(@as(u32, 100), list.get(10).?); + try testing.expectEqual(@as(u32, 300), list.get(30).?); +} + +test "SkipList: remove non-existent" { + const List = SkipList(u32, u32); + var list = try List.init(testing.allocator, 42); + defer list.deinit(); + + try list.insert(10, 100); + + try testing.expect(!list.remove(999)); + try testing.expectEqual(@as(u32, 1), list.len()); +} + +test "SkipList: deterministic with same seed" { + const List = SkipList(u32, u32); + + // First list with seed 42 + var list1 = try List.init(testing.allocator, 42); + defer list1.deinit(); + + try list1.insert(10, 100); + try list1.insert(20, 200); + try list1.insert(30, 300); + + const max_level1 = list1.max_level; + + // Second list with same seed + var list2 = try List.init(testing.allocator, 42); + defer list2.deinit(); + + try list2.insert(10, 100); + try list2.insert(20, 200); + try list2.insert(30, 300); + + const max_level2 = list2.max_level; + + // Should have same structure + try testing.expectEqual(max_level1, max_level2); +} + +test "SkipList: stress test" { + const List = SkipList(u32, u32); + var list = try List.init(testing.allocator, 42); + defer list.deinit(); + + // Insert many elements + var i: u32 = 0; + while (i < 100) : (i += 1) { + try list.insert(i, i * 10); + } + + try testing.expectEqual(@as(u32, 100), list.len()); + + // Verify all present + i = 0; + while (i < 100) : (i += 1) { + try testing.expectEqual(i * 10, list.get(i).?); + } + + // Remove every other element + i = 0; + while (i < 100) : (i += 2) { + try testing.expect(list.remove(i)); + } + + try testing.expectEqual(@as(u32, 50), list.len()); + + // Verify correct elements remain + i = 0; + while (i < 100) : (i += 1) { + if (i % 2 == 0) { + try testing.expectEqual(@as(?u32, null), list.get(i)); + } else { + try testing.expectEqual(i * 10, list.get(i).?); + } + } +} + +test "SkipList: bounded levels" { + const List = SkipList(u32, u32); + var list = try List.init(testing.allocator, 42); + defer list.deinit(); + + // Insert many elements + var i: u32 = 0; + while (i < 1000) : (i += 1) { + try list.insert(i, i); + } + + // Max level should be bounded + try testing.expect(list.max_level <= MAX_LEVEL); +} diff --git a/tiger_style/time_simulation.zig b/tiger_style/time_simulation.zig new file mode 100644 index 0000000..c7ef0cc --- /dev/null +++ b/tiger_style/time_simulation.zig @@ -0,0 +1,427 @@ +//! Time Simulation - Deterministic Time Framework +//! +//! Inspired by TigerBeetle's time simulation testing approach. +//! Provides a virtual clock with nanosecond precision for deterministic, +//! reproducible testing of time-dependent algorithms. +//! +//! Tiger Style principles demonstrated: +//! - Explicit u64 timestamps (never usize) +//! - Bounded event queue with fail-fast +//! - Heavy assertions on all state transitions +//! - No recursion in event processing +//! - All operations have explicit upper bounds + +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; + +/// Maximum number of scheduled events. Must be bounded. +pub const MAX_EVENTS: u32 = 1024; + +/// Nanoseconds in one millisecond +pub const NS_PER_MS: u64 = 1_000_000; + +/// Nanoseconds in one second +pub const NS_PER_SECOND: u64 = 1_000_000_000; + +/// Event callback function type +pub const EventCallback = *const fn (context: *anyopaque) void; + +/// Scheduled event in the simulation +pub const Event = struct { + /// Absolute timestamp when event fires (nanoseconds) + timestamp: u64, + + /// Callback to execute + callback: EventCallback, + + /// Opaque context passed to callback + context: *anyopaque, + + /// Event ID for tracking + id: u32, + + /// Active flag (for event cancellation) + active: bool, +}; + +/// Virtual clock for deterministic time simulation +pub const Clock = struct { + /// Current virtual time (nanoseconds since epoch) + now: u64, + + /// Scheduled events (bounded array) + events: [MAX_EVENTS]Event, + + /// Number of active events + event_count: u32, + + /// Next event ID to assign + next_event_id: u32, + + /// Total events processed (for metrics) + events_processed: u64, + + /// Initialize clock at time zero + pub fn init() Clock { + var clock = Clock{ + .now = 0, + .events = undefined, + .event_count = 0, + .next_event_id = 1, + .events_processed = 0, + }; + + // Initialize all events as inactive + var i: u32 = 0; + while (i < MAX_EVENTS) : (i += 1) { + clock.events[i] = Event{ + .timestamp = 0, + .callback = undefined, + .context = undefined, + .id = 0, + .active = false, + }; + } + + return clock; + } + + /// Schedule an event at absolute timestamp + /// Returns event ID for cancellation, or 0 if queue full + pub fn schedule( + self: *Clock, + timestamp: u64, + callback: EventCallback, + context: *anyopaque, + ) u32 { + // Preconditions + assert(timestamp >= self.now); // Cannot schedule in the past + assert(self.event_count <= MAX_EVENTS); + + // Fail-fast: queue full + if (self.event_count >= MAX_EVENTS) { + return 0; + } + + // Find first inactive slot + var slot_index: u32 = 0; + var found: bool = false; + while (slot_index < MAX_EVENTS) : (slot_index += 1) { + if (!self.events[slot_index].active) { + found = true; + break; + } + } + + assert(found); // Must find slot since event_count < MAX_EVENTS + assert(slot_index < MAX_EVENTS); + + const event_id = self.next_event_id; + self.next_event_id +%= 1; // Wrapping add for ID overflow + if (self.next_event_id == 0) self.next_event_id = 1; // Never use ID 0 + + self.events[slot_index] = Event{ + .timestamp = timestamp, + .callback = callback, + .context = context, + .id = event_id, + .active = true, + }; + + self.event_count += 1; + + // Postconditions + assert(self.events[slot_index].active); + assert(self.events[slot_index].id == event_id); + assert(self.event_count <= MAX_EVENTS); + + return event_id; + } + + /// Cancel a scheduled event by ID + pub fn cancel(self: *Clock, event_id: u32) bool { + assert(event_id != 0); + assert(self.event_count <= MAX_EVENTS); + + var i: u32 = 0; + while (i < MAX_EVENTS) : (i += 1) { + if (self.events[i].active and self.events[i].id == event_id) { + self.events[i].active = false; + self.event_count -= 1; + assert(self.event_count <= MAX_EVENTS); + return true; + } + } + + return false; + } + + /// Advance time and process all events up to target timestamp + /// Returns number of events processed + pub fn tick(self: *Clock, target: u64) u32 { + // Preconditions + assert(target >= self.now); // Time only moves forward + assert(self.event_count <= MAX_EVENTS); + + const initial_count = self.event_count; + var processed: u32 = 0; + + // Process events iteratively (no recursion!) + // Bounded loop: at most MAX_EVENTS iterations per tick + var iterations: u32 = 0; + while (iterations < MAX_EVENTS) : (iterations += 1) { + // Find next event to fire + var next_index: ?u32 = null; + var next_time: u64 = target + 1; // Past target initially + + var i: u32 = 0; + while (i < MAX_EVENTS) : (i += 1) { + if (self.events[i].active and + self.events[i].timestamp <= target and + self.events[i].timestamp < next_time) + { + next_index = i; + next_time = self.events[i].timestamp; + } + } + + // No more events to process + if (next_index == null) break; + + const index = next_index.?; + assert(index < MAX_EVENTS); + assert(self.events[index].active); + + // Advance to event time + self.now = self.events[index].timestamp; + assert(self.now <= target); + + // Execute callback + const callback = self.events[index].callback; + const context = self.events[index].context; + self.events[index].active = false; + self.event_count -= 1; + + callback(context); + + processed += 1; + self.events_processed += 1; + + // Invariant: event_count consistent + assert(self.event_count <= MAX_EVENTS); + } + + // Advance to target + self.now = target; + + // Postconditions + assert(self.now == target); + assert(self.event_count <= MAX_EVENTS); + assert(processed <= initial_count); + + return processed; + } + + /// Get current time + pub fn time(self: *const Clock) u64 { + return self.now; + } + + /// Check if event queue is empty + pub fn isEmpty(self: *const Clock) bool { + assert(self.event_count <= MAX_EVENTS); + return self.event_count == 0; + } +}; + +// ============================================================================ +// Tests demonstrating deterministic time simulation +// ============================================================================ + +const TestContext = struct { + fired: bool, + fire_time: u64, + fire_count: u32, +}; + +fn testCallback(ctx: *anyopaque) void { + const context: *TestContext = @ptrCast(@alignCast(ctx)); + context.fired = true; + context.fire_count += 1; +} + +test "Clock: initialization" { + const clock = Clock.init(); + + try testing.expectEqual(@as(u64, 0), clock.now); + try testing.expectEqual(@as(u32, 0), clock.event_count); + try testing.expect(clock.isEmpty()); +} + +test "Clock: schedule and fire single event" { + var clock = Clock.init(); + var context = TestContext{ + .fired = false, + .fire_time = 0, + .fire_count = 0, + }; + + const event_id = clock.schedule( + 100 * NS_PER_MS, + testCallback, + @ptrCast(&context), + ); + + try testing.expect(event_id != 0); + try testing.expectEqual(@as(u32, 1), clock.event_count); + try testing.expect(!context.fired); + + // Tick to event time + const processed = clock.tick(100 * NS_PER_MS); + + try testing.expectEqual(@as(u32, 1), processed); + try testing.expect(context.fired); + try testing.expectEqual(@as(u32, 1), context.fire_count); + try testing.expectEqual(@as(u64, 100 * NS_PER_MS), clock.time()); + try testing.expect(clock.isEmpty()); +} + +test "Clock: event ordering is deterministic" { + var clock = Clock.init(); + var contexts: [3]TestContext = undefined; + + // Schedule events out of order + for (&contexts, 0..) |*ctx, i| { + ctx.* = TestContext{ + .fired = false, + .fire_time = 0, + .fire_count = 0, + }; + + // Schedule at times: 300ms, 100ms, 200ms + const times = [_]u64{ 300, 100, 200 }; + _ = clock.schedule( + times[i] * NS_PER_MS, + testCallback, + @ptrCast(ctx), + ); + } + + try testing.expectEqual(@as(u32, 3), clock.event_count); + + // Process all events + _ = clock.tick(400 * NS_PER_MS); + + // All events should fire + try testing.expect(contexts[0].fired); + try testing.expect(contexts[1].fired); + try testing.expect(contexts[2].fired); + try testing.expect(clock.isEmpty()); +} + +test "Clock: cancel event" { + var clock = Clock.init(); + var context = TestContext{ + .fired = false, + .fire_time = 0, + .fire_count = 0, + }; + + const event_id = clock.schedule( + 100 * NS_PER_MS, + testCallback, + @ptrCast(&context), + ); + + try testing.expect(event_id != 0); + + // Cancel before firing + const cancelled = clock.cancel(event_id); + try testing.expect(cancelled); + try testing.expect(clock.isEmpty()); + + // Tick past event time + _ = clock.tick(200 * NS_PER_MS); + + // Event should not fire + try testing.expect(!context.fired); +} + +test "Clock: bounded event queue" { + var clock = Clock.init(); + var context = TestContext{ + .fired = false, + .fire_time = 0, + .fire_count = 0, + }; + + // Fill event queue to maximum + var i: u32 = 0; + while (i < MAX_EVENTS) : (i += 1) { + const event_id = clock.schedule( + @as(u64, i) * NS_PER_MS, + testCallback, + @ptrCast(&context), + ); + try testing.expect(event_id != 0); + } + + try testing.expectEqual(MAX_EVENTS, clock.event_count); + + // Attempt to schedule one more (should fail) + const overflow_id = clock.schedule( + 1000 * NS_PER_MS, + testCallback, + @ptrCast(&context), + ); + + try testing.expectEqual(@as(u32, 0), overflow_id); + try testing.expectEqual(MAX_EVENTS, clock.event_count); +} + +test "Clock: time only moves forward" { + var clock = Clock.init(); + + _ = clock.tick(100 * NS_PER_MS); + try testing.expectEqual(@as(u64, 100 * NS_PER_MS), clock.time()); + + _ = clock.tick(200 * NS_PER_MS); + try testing.expectEqual(@as(u64, 200 * NS_PER_MS), clock.time()); + + // Tick to same time (no-op) + _ = clock.tick(200 * NS_PER_MS); + try testing.expectEqual(@as(u64, 200 * NS_PER_MS), clock.time()); +} + +test "Clock: stress test with many events" { + var clock = Clock.init(); + var contexts: [100]TestContext = undefined; + + // Schedule 100 events at different times + for (&contexts, 0..) |*ctx, i| { + ctx.* = TestContext{ + .fired = false, + .fire_time = 0, + .fire_count = 0, + }; + + _ = clock.schedule( + @as(u64, i * 10) * NS_PER_MS, + testCallback, + @ptrCast(ctx), + ); + } + + // Process all + _ = clock.tick(1000 * NS_PER_MS); + + // Verify all fired exactly once + for (contexts) |ctx| { + try testing.expect(ctx.fired); + try testing.expectEqual(@as(u32, 1), ctx.fire_count); + } + + try testing.expect(clock.isEmpty()); + try testing.expectEqual(@as(u64, 100), clock.events_processed); +} diff --git a/tiger_style/two_phase_commit.zig b/tiger_style/two_phase_commit.zig new file mode 100644 index 0000000..1b420e4 --- /dev/null +++ b/tiger_style/two_phase_commit.zig @@ -0,0 +1,407 @@ +//! Tiger Style Two-Phase Commit Protocol +//! +//! Implements distributed transaction commit protocol with Tiger Style: +//! - Explicit state machine transitions +//! - Bounded participant list +//! - Heavy assertions on all state changes +//! - Fail-fast on protocol violations +//! - All timeouts explicitly bounded + +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; + +/// Maximum participants in transaction (must be bounded) +pub const MAX_PARTICIPANTS: u32 = 64; + +/// Transaction ID +pub const TransactionId = u64; + +/// Participant ID +pub const ParticipantId = u32; + +/// Two-phase commit coordinator state +pub const CoordinatorState = enum(u8) { + init, + preparing, + committed, + aborted, + + pub fn validate(self: CoordinatorState) void { + assert(@intFromEnum(self) <= 3); + } +}; + +/// Participant response +pub const ParticipantVote = enum(u8) { + vote_commit, + vote_abort, + no_response, + + pub fn validate(self: ParticipantVote) void { + assert(@intFromEnum(self) <= 2); + } +}; + +/// Two-Phase Commit Coordinator +pub const Coordinator = struct { + /// Transaction ID + txn_id: TransactionId, + + /// Current state + state: CoordinatorState, + + /// Number of participants + participant_count: u32, + + /// Participant votes + votes: [MAX_PARTICIPANTS]ParticipantVote, + + /// Number of votes received + votes_received: u32, + + /// Number of commit votes + commit_votes: u32, + + /// Start time (for timeout detection) + start_time: u64, + + /// Timeout in milliseconds + timeout_ms: u64, + + /// Initialize coordinator + pub fn init(txn_id: TransactionId, participant_count: u32, timeout_ms: u64) Coordinator { + // Preconditions + assert(txn_id > 0); + assert(participant_count > 0); + assert(participant_count <= MAX_PARTICIPANTS); + assert(timeout_ms > 0); + + var coord = Coordinator{ + .txn_id = txn_id, + .state = .init, + .participant_count = participant_count, + .votes = undefined, + .votes_received = 0, + .commit_votes = 0, + .start_time = 0, + .timeout_ms = timeout_ms, + }; + + // Initialize all votes to no_response + var i: u32 = 0; + while (i < MAX_PARTICIPANTS) : (i += 1) { + coord.votes[i] = .no_response; + } + + // Postconditions + assert(coord.state == .init); + assert(coord.votes_received == 0); + coord.validate(); + + return coord; + } + + /// Validate coordinator invariants + pub fn validate(self: *const Coordinator) void { + self.state.validate(); + assert(self.txn_id > 0); + assert(self.participant_count > 0); + assert(self.participant_count <= MAX_PARTICIPANTS); + assert(self.votes_received <= self.participant_count); + assert(self.commit_votes <= self.votes_received); + assert(self.timeout_ms > 0); + } + + /// Start prepare phase + pub fn prepare(self: *Coordinator, current_time: u64) void { + // Preconditions + self.validate(); + assert(self.state == .init); + + self.state = .preparing; + self.start_time = current_time; + + // Postconditions + assert(self.state == .preparing); + self.validate(); + } + + /// Record participant vote + pub fn recordVote(self: *Coordinator, participant: ParticipantId, vote: ParticipantVote) !void { + // Preconditions + self.validate(); + assert(self.state == .preparing); + assert(participant < self.participant_count); + vote.validate(); + + // Fail-fast: already voted + if (self.votes[participant] != .no_response) { + return error.AlreadyVoted; + } + + self.votes[participant] = vote; + self.votes_received += 1; + + if (vote == .vote_commit) { + self.commit_votes += 1; + } + + // Postconditions + assert(self.votes_received <= self.participant_count); + self.validate(); + } + + /// Check if all votes received + pub fn allVotesReceived(self: *const Coordinator) bool { + self.validate(); + return self.votes_received == self.participant_count; + } + + /// Commit transaction + pub fn commit(self: *Coordinator) !void { + // Preconditions + self.validate(); + assert(self.state == .preparing); + assert(self.allVotesReceived()); + + // Can only commit if all voted commit + if (self.commit_votes != self.participant_count) { + return error.CannotCommit; + } + + self.state = .committed; + + // Postconditions + assert(self.state == .committed); + self.validate(); + } + + /// Abort transaction + pub fn abort(self: *Coordinator) void { + // Preconditions + self.validate(); + assert(self.state == .preparing); + + self.state = .aborted; + + // Postconditions + assert(self.state == .aborted); + self.validate(); + } + + /// Check if transaction timed out + pub fn isTimedOut(self: *const Coordinator, current_time: u64) bool { + self.validate(); + if (self.state != .preparing) return false; + + const elapsed = current_time - self.start_time; + return elapsed > self.timeout_ms; + } + + /// Decide commit or abort based on votes + pub fn decide(self: *Coordinator) !void { + // Preconditions + self.validate(); + assert(self.state == .preparing); + assert(self.allVotesReceived()); + + // Check if any aborts + var i: u32 = 0; + var has_abort = false; + while (i < self.participant_count) : (i += 1) { + if (self.votes[i] == .vote_abort) { + has_abort = true; + break; + } + } + + if (has_abort) { + self.abort(); + } else { + try self.commit(); + } + + // Postconditions + assert(self.state == .committed or self.state == .aborted); + self.validate(); + } +}; + +/// Participant in two-phase commit +pub const Participant = struct { + id: ParticipantId, + txn_id: TransactionId, + prepared: bool, + committed: bool, + aborted: bool, + + pub fn init(id: ParticipantId, txn_id: TransactionId) Participant { + assert(txn_id > 0); + + return Participant{ + .id = id, + .txn_id = txn_id, + .prepared = false, + .committed = false, + .aborted = false, + }; + } + + pub fn validate(self: *const Participant) void { + assert(self.txn_id > 0); + // Can't be both committed and aborted + assert(!(self.committed and self.aborted)); + } + + pub fn prepare(self: *Participant) ParticipantVote { + self.validate(); + assert(!self.prepared); + + self.prepared = true; + + // Simplified: always vote commit for testing + // Real system would check local constraints + return .vote_commit; + } + + pub fn commitTransaction(self: *Participant) void { + self.validate(); + assert(self.prepared); + assert(!self.aborted); + + self.committed = true; + self.validate(); + } + + pub fn abortTransaction(self: *Participant) void { + self.validate(); + assert(!self.committed); + + self.aborted = true; + self.validate(); + } +}; + +// ============================================================================ +// Tests +// ============================================================================ + +test "Coordinator: initialization" { + const coord = Coordinator.init(1, 3, 1000); + + try testing.expectEqual(@as(TransactionId, 1), coord.txn_id); + try testing.expectEqual(CoordinatorState.init, coord.state); + try testing.expectEqual(@as(u32, 3), coord.participant_count); + try testing.expectEqual(@as(u32, 0), coord.votes_received); +} + +test "Coordinator: successful commit" { + var coord = Coordinator.init(1, 3, 1000); + + coord.prepare(100); + try testing.expectEqual(CoordinatorState.preparing, coord.state); + + // All participants vote commit + try coord.recordVote(0, .vote_commit); + try coord.recordVote(1, .vote_commit); + try coord.recordVote(2, .vote_commit); + + try testing.expect(coord.allVotesReceived()); + + try coord.decide(); + try testing.expectEqual(CoordinatorState.committed, coord.state); +} + +test "Coordinator: abort on single no vote" { + var coord = Coordinator.init(1, 3, 1000); + + coord.prepare(100); + + // One participant votes abort + try coord.recordVote(0, .vote_commit); + try coord.recordVote(1, .vote_abort); + try coord.recordVote(2, .vote_commit); + + try coord.decide(); + try testing.expectEqual(CoordinatorState.aborted, coord.state); +} + +test "Coordinator: timeout detection" { + var coord = Coordinator.init(1, 3, 1000); + + coord.prepare(100); + + try testing.expect(!coord.isTimedOut(500)); + try testing.expect(coord.isTimedOut(1200)); +} + +test "Coordinator: cannot vote twice" { + var coord = Coordinator.init(1, 3, 1000); + + coord.prepare(100); + + try coord.recordVote(0, .vote_commit); + + // Try to vote again + const result = coord.recordVote(0, .vote_commit); + try testing.expectError(error.AlreadyVoted, result); +} + +test "Coordinator: bounded participants" { + const coord = Coordinator.init(1, MAX_PARTICIPANTS, 1000); + try testing.expectEqual(MAX_PARTICIPANTS, coord.participant_count); +} + +test "Participant: prepare and commit" { + var p = Participant.init(1, 100); + + const vote = p.prepare(); + try testing.expectEqual(ParticipantVote.vote_commit, vote); + try testing.expect(p.prepared); + + p.commitTransaction(); + try testing.expect(p.committed); + try testing.expect(!p.aborted); +} + +test "Participant: prepare and abort" { + var p = Participant.init(1, 100); + + _ = p.prepare(); + + p.abortTransaction(); + try testing.expect(p.aborted); + try testing.expect(!p.committed); +} + +test "Two-phase commit: full protocol" { + var coord = Coordinator.init(1, 3, 1000); + var participants: [3]Participant = undefined; + + // Initialize participants + var i: u32 = 0; + while (i < 3) : (i += 1) { + participants[i] = Participant.init(i, 1); + } + + // Phase 1: Prepare + coord.prepare(100); + + i = 0; + while (i < 3) : (i += 1) { + const vote = participants[i].prepare(); + try coord.recordVote(i, vote); + } + + // Phase 2: Commit + try coord.decide(); + try testing.expectEqual(CoordinatorState.committed, coord.state); + + // Participants commit + i = 0; + while (i < 3) : (i += 1) { + participants[i].commitTransaction(); + try testing.expect(participants[i].committed); + } +} diff --git a/tiger_style/vsr_consensus.zig b/tiger_style/vsr_consensus.zig new file mode 100644 index 0000000..6bfc67e --- /dev/null +++ b/tiger_style/vsr_consensus.zig @@ -0,0 +1,528 @@ +//! Tiger Style VSR (Viewstamped Replication) Consensus +//! +//! TigerBeetle's actual consensus protocol - more sophisticated than Raft. +//! Implements "Viewstamped Replication Revisited" with Tiger Style discipline: +//! - Explicit view numbers and op numbers +//! - Bounded message queues with fail-fast +//! - View change protocol with prepare/commit phases +//! - Heavy assertions on all state transitions +//! - Deterministic testing support +//! +//! Reference: "Viewstamped Replication Revisited" by Liskov & Cowling + +const std = @import("std"); +const assert = std.debug.assert; +const testing = std.testing; + +/// Maximum replicas in cluster (must be bounded) +pub const MAX_REPLICAS: u32 = 16; + +/// Maximum operations in log (bounded for Tiger Style) +pub const MAX_OPS: u32 = 10000; + +/// Maximum pending messages (bounded queue) +pub const MAX_MESSAGES: u32 = 256; + +/// Replica ID type - explicit u32 +pub const ReplicaId = u32; + +/// View number - monotonically increasing +pub const ViewNumber = u64; + +/// Operation number - monotonically increasing +pub const OpNumber = u64; + +/// VSR replica states +pub const ReplicaState = enum(u8) { + normal, // Normal operation + view_change, // View change in progress + recovering, // Replica recovering + + pub fn validate(self: ReplicaState) void { + assert(@intFromEnum(self) <= 2); + } +}; + +/// Operation in the replicated log +pub const Operation = struct { + op: OpNumber, + view: ViewNumber, + command: u64, // Simplified command + committed: bool, + + pub fn validate(self: Operation) void { + assert(self.op > 0); + assert(self.view > 0); + } +}; + +/// VSR message types +pub const MessageType = enum(u8) { + prepare, + prepare_ok, + commit, + start_view_change, + do_view_change, + start_view, +}; + +/// VSR protocol message +pub const Message = struct { + msg_type: MessageType, + view: ViewNumber, + op: OpNumber, + from: ReplicaId, + to: ReplicaId, + committed_op: OpNumber, +}; + +/// VSR Replica +pub const VSRReplica = struct { + /// Replica ID + id: ReplicaId, + + /// Current state + state: ReplicaState, + + /// Current view number + view: ViewNumber, + + /// Operation log + log: [MAX_OPS]Operation, + log_length: u32, + + /// Operation number (next op to execute) + op: OpNumber, + + /// Commit number (last committed op) + commit_number: OpNumber, + + /// Cluster configuration + replica_count: u32, + + /// View change tracking + view_change_messages: u32, + do_view_change_messages: u32, + + /// Last heartbeat time + last_heartbeat: u64, + heartbeat_timeout: u64, + + /// Initialize VSR replica + pub fn init(id: ReplicaId, replica_count: u32) VSRReplica { + // Preconditions + assert(id > 0); + assert(id <= MAX_REPLICAS); + assert(replica_count > 0); + assert(replica_count <= MAX_REPLICAS); + assert(replica_count % 2 == 1); // Odd number for quorum + + var replica = VSRReplica{ + .id = id, + .state = .normal, + .view = 1, + .log = undefined, + .log_length = 0, + .op = 1, + .commit_number = 0, + .replica_count = replica_count, + .view_change_messages = 0, + .do_view_change_messages = 0, + .last_heartbeat = 0, + .heartbeat_timeout = 100, // milliseconds + }; + + // Initialize log + var i: u32 = 0; + while (i < MAX_OPS) : (i += 1) { + replica.log[i] = Operation{ + .op = 0, + .view = 0, + .command = 0, + .committed = false, + }; + } + + // Postconditions + assert(replica.state == .normal); + assert(replica.view > 0); + replica.validate(); + + return replica; + } + + /// Validate replica invariants + pub fn validate(self: *const VSRReplica) void { + self.state.validate(); + assert(self.id > 0); + assert(self.id <= MAX_REPLICAS); + assert(self.view > 0); + assert(self.op > 0); + assert(self.commit_number < self.op); + assert(self.log_length <= MAX_OPS); + assert(self.replica_count > 0); + assert(self.replica_count <= MAX_REPLICAS); + assert(self.view_change_messages <= self.replica_count); + assert(self.do_view_change_messages <= self.replica_count); + } + + /// Check if this replica is the leader in current view + pub fn isLeader(self: *const VSRReplica) bool { + self.validate(); + const leader_id = (self.view % @as(u64, self.replica_count)) + 1; + return self.id == leader_id; + } + + /// Prepare operation (leader only) + pub fn prepare(self: *VSRReplica, command: u64) !OpNumber { + // Preconditions + self.validate(); + assert(self.state == .normal); + assert(self.isLeader()); + + // Fail-fast: log full + if (self.log_length >= MAX_OPS) { + return error.LogFull; + } + + const op_num = self.op; + const index = @as(u32, @intCast(op_num - 1)); + assert(index < MAX_OPS); + + self.log[index] = Operation{ + .op = op_num, + .view = self.view, + .command = command, + .committed = false, + }; + self.log[index].validate(); + + self.log_length += 1; + self.op += 1; + + // Postconditions + assert(self.log_length <= MAX_OPS); + self.validate(); + + return op_num; + } + + /// Receive prepare-ok (leader only) + pub fn receivePrepareOk(self: *VSRReplica, op_num: OpNumber) void { + // Preconditions + self.validate(); + assert(self.state == .normal); + assert(self.isLeader()); + assert(op_num < self.op); + + // In full implementation, would track quorum + // For simplicity, commit immediately + if (op_num > self.commit_number) { + self.commitUpTo(op_num); + } + + self.validate(); + } + + /// Commit operations up to op_num + fn commitUpTo(self: *VSRReplica, op_num: OpNumber) void { + self.validate(); + assert(op_num < self.op); + + while (self.commit_number < op_num) { + self.commit_number += 1; + const index = @as(u32, @intCast(self.commit_number - 1)); + if (index < self.log_length) { + self.log[index].committed = true; + } + } + + self.validate(); + } + + /// Start view change + pub fn startViewChange(self: *VSRReplica) void { + // Preconditions + self.validate(); + assert(self.state == .normal); + + self.state = .view_change; + self.view += 1; + self.view_change_messages = 1; // Count self + self.do_view_change_messages = 0; + + // Postconditions + assert(self.state == .view_change); + self.validate(); + } + + /// Receive start-view-change message + pub fn receiveStartViewChange(self: *VSRReplica, from: ReplicaId, new_view: ViewNumber) void { + // Preconditions + self.validate(); + assert(from > 0); + assert(from <= MAX_REPLICAS); + assert(new_view >= self.view); + + // If we see a higher view, transition to view change + if (new_view > self.view and self.state == .normal) { + self.view = new_view; + self.state = .view_change; + self.view_change_messages = 1; // Count self + self.do_view_change_messages = 0; + } + + // Count message if for current view and we're in view change + if (new_view == self.view and self.state == .view_change) { + self.view_change_messages += 1; + + // Check if we have quorum (f+1 where f = (n-1)/2) + const f = (self.replica_count - 1) / 2; + const quorum = f + 1; + + if (self.view_change_messages >= quorum) { + // Send do-view-change + self.do_view_change_messages = 1; + } + } + + self.validate(); + } + + /// Receive do-view-change message + pub fn receiveDoViewChange( + self: *VSRReplica, + from: ReplicaId, + view: ViewNumber, + _: u32, // log_length - used in full implementation + ) void { + // Preconditions + self.validate(); + assert(from > 0); + assert(from <= MAX_REPLICAS); + assert(view == self.view); + assert(self.state == .view_change); + + self.do_view_change_messages += 1; + + // Check for quorum + const f = (self.replica_count - 1) / 2; + const quorum = f + 1; + + if (self.do_view_change_messages >= quorum and self.isLeader()) { + self.startView(); + } + + self.validate(); + } + + /// Start new view (new leader) + fn startView(self: *VSRReplica) void { + // Preconditions + self.validate(); + assert(self.state == .view_change); + assert(self.isLeader()); + + self.state = .normal; + self.view_change_messages = 0; + self.do_view_change_messages = 0; + + // Postconditions + assert(self.state == .normal); + self.validate(); + } + + /// Receive start-view message (followers) + pub fn receiveStartView(self: *VSRReplica, view: ViewNumber) void { + // Preconditions + self.validate(); + assert(view >= self.view); + + if (self.state == .view_change and view == self.view) { + self.state = .normal; + self.view_change_messages = 0; + self.do_view_change_messages = 0; + } + + // Postconditions + assert(self.state == .normal); + self.validate(); + } + + /// Check if heartbeat timeout expired + pub fn isHeartbeatTimedOut(self: *const VSRReplica, current_time: u64) bool { + self.validate(); + const elapsed = current_time - self.last_heartbeat; + return elapsed > self.heartbeat_timeout; + } + + /// Reset heartbeat timer + pub fn resetHeartbeat(self: *VSRReplica, current_time: u64) void { + self.validate(); + self.last_heartbeat = current_time; + } + + /// Get committed operations count + pub fn committedOps(self: *const VSRReplica) OpNumber { + self.validate(); + return self.commit_number; + } +}; + +// ============================================================================ +// Tests - VSR protocol verification +// ============================================================================ + +test "VSRReplica: initialization" { + const replica = VSRReplica.init(1, 3); + + try testing.expectEqual(@as(ReplicaId, 1), replica.id); + try testing.expectEqual(ReplicaState.normal, replica.state); + try testing.expectEqual(@as(ViewNumber, 1), replica.view); + try testing.expectEqual(@as(OpNumber, 1), replica.op); + try testing.expectEqual(@as(OpNumber, 0), replica.commit_number); +} + +test "VSRReplica: leader detection" { + var r1 = VSRReplica.init(1, 3); + var r2 = VSRReplica.init(2, 3); + var r3 = VSRReplica.init(3, 3); + + // View 1: leader is (1 % 3) + 1 = 2 + try testing.expect(!r1.isLeader()); + try testing.expect(r2.isLeader()); + try testing.expect(!r3.isLeader()); + + // View 2: leader is (2 % 3) + 1 = 3 + r1.view = 2; + r2.view = 2; + r3.view = 2; + + try testing.expect(!r1.isLeader()); + try testing.expect(!r2.isLeader()); + try testing.expect(r3.isLeader()); +} + +test "VSRReplica: prepare operation" { + var replica = VSRReplica.init(2, 3); // Replica 2 is leader in view 1 + + const op1 = try replica.prepare(100); + const op2 = try replica.prepare(200); + + try testing.expectEqual(@as(OpNumber, 1), op1); + try testing.expectEqual(@as(OpNumber, 2), op2); + try testing.expectEqual(@as(u32, 2), replica.log_length); +} + +test "VSRReplica: commit operations" { + var replica = VSRReplica.init(2, 3); + + _ = try replica.prepare(100); + _ = try replica.prepare(200); + + replica.receivePrepareOk(1); + try testing.expectEqual(@as(OpNumber, 1), replica.commit_number); + + replica.receivePrepareOk(2); + try testing.expectEqual(@as(OpNumber, 2), replica.commit_number); +} + +test "VSRReplica: view change initiation" { + var replica = VSRReplica.init(1, 3); + + try testing.expectEqual(ReplicaState.normal, replica.state); + try testing.expectEqual(@as(ViewNumber, 1), replica.view); + + replica.startViewChange(); + + try testing.expectEqual(ReplicaState.view_change, replica.state); + try testing.expectEqual(@as(ViewNumber, 2), replica.view); +} + +test "VSRReplica: view change quorum" { + var replica = VSRReplica.init(1, 5); // Cluster of 5, starts at view 1 + + replica.startViewChange(); // Now at view 2 + try testing.expectEqual(@as(ViewNumber, 2), replica.view); + try testing.expectEqual(@as(u32, 1), replica.view_change_messages); + + // Need f+1 = 2+1 = 3 messages for quorum + // Messages must be for view 2 (current view after startViewChange) + replica.receiveStartViewChange(2, 2); + try testing.expectEqual(@as(u32, 2), replica.view_change_messages); + + replica.receiveStartViewChange(3, 2); + try testing.expectEqual(@as(u32, 3), replica.view_change_messages); + try testing.expectEqual(@as(u32, 1), replica.do_view_change_messages); +} + +test "VSRReplica: do-view-change and start-view" { + var leader = VSRReplica.init(3, 3); // Will be leader in view 2 + leader.view = 2; + leader.state = .view_change; + leader.do_view_change_messages = 0; // Start at 0, will count messages + + try testing.expect(leader.isLeader()); + try testing.expectEqual(ReplicaState.view_change, leader.state); + + // Receive first do-view-change message + leader.receiveDoViewChange(1, 2, 0); + try testing.expectEqual(@as(u32, 1), leader.do_view_change_messages); + try testing.expectEqual(ReplicaState.view_change, leader.state); + + // Receive second message - quorum reached (2 out of 3), should start view + leader.receiveDoViewChange(2, 2, 0); + + // After quorum, leader automatically starts new view + try testing.expectEqual(ReplicaState.normal, leader.state); +} + +test "VSRReplica: heartbeat timeout" { + var replica = VSRReplica.init(1, 3); + + replica.resetHeartbeat(100); + + try testing.expect(!replica.isHeartbeatTimedOut(150)); + try testing.expect(replica.isHeartbeatTimedOut(250)); +} + +test "VSRReplica: bounded log" { + var replica = VSRReplica.init(2, 3); + + // Fill log to maximum + var i: u32 = 0; + while (i < MAX_OPS) : (i += 1) { + _ = try replica.prepare(@intCast(i)); + } + + try testing.expectEqual(MAX_OPS, replica.log_length); + + // Next prepare should fail + const result = replica.prepare(9999); + try testing.expectError(error.LogFull, result); +} + +test "VSRReplica: operation validation" { + var replica = VSRReplica.init(2, 3); + + _ = try replica.prepare(42); + + const op = replica.log[0]; + op.validate(); // Should not fail + + try testing.expectEqual(@as(u64, 42), op.command); + try testing.expectEqual(@as(OpNumber, 1), op.op); + try testing.expectEqual(@as(ViewNumber, 1), op.view); +} + +test "VSRReplica: committed operations count" { + var replica = VSRReplica.init(2, 3); + + _ = try replica.prepare(100); + _ = try replica.prepare(200); + _ = try replica.prepare(300); + + replica.receivePrepareOk(2); + + try testing.expectEqual(@as(OpNumber, 2), replica.committedOps()); +}