From 4327b2c70a27eb1332018daa45907823d23d1a9a Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Fri, 13 Feb 2026 20:32:37 +0100 Subject: [PATCH 01/12] The base commit --- src/lib.zig | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/lib.zig b/src/lib.zig index 76d05de..a685ba6 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -1,7 +1,6 @@ //! # Zodd //! -//! Zodd is a Datalog engine for Zig. -//! It implements semi-naive evaluation with parallel execution support. +//! Zodd is a Datalog engine in Zig. //! //! ## Quickstart //! From 80afd851794a537c9a94c29cc5ca0e01fae38cd6 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Fri, 13 Feb 2026 20:33:56 +0100 Subject: [PATCH 02/12] WIP --- src/lib.zig | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lib.zig b/src/lib.zig index a685ba6..592d29c 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -46,7 +46,7 @@ //! - `Relation`: The immutable data structure (sorted, deduplicated tuples). //! - `Variable`: The mutable relation for fixed-point iterations. //! - `join`: The merge-join algorithms. -//! - `extend`: The primitives for extending tuples (semi-joins, anti-joins). +//! - `extend`: The primitives for extending tuples (semi-joins and anti-joins). //! - `index`: The indexes for lookups. //! - `aggregate`: The group-by and aggregation operations. From e3b0e9afc6b9c26e62e6a59f4328e41d7a8d3002 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Mon, 16 Feb 2026 10:05:14 +0100 Subject: [PATCH 03/12] WIP --- ROADMAP.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/ROADMAP.md b/ROADMAP.md index 07b3a6f..6225652 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -27,7 +27,7 @@ This document outlines the features implemented in Zodd and the future goals for - [x] Secondary indices - [x] Incremental maintenance - [x] Parallel execution -- [ ] CLI interface +- [ ] CLI - [ ] Streaming input - [ ] Rule DSL - [ ] Query planner From 10f24905b22f61d06c73c51282a4dde120814a7f Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Fri, 17 Apr 2026 18:08:48 +0200 Subject: [PATCH 04/12] WIP --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index f7f59b2..c4e2230 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![License](https://img.shields.io/badge/license-MIT-007ec6?label=license&style=flat&labelColor=282c34&logo=open-source-initiative)](https://github.com/CogitatorTech/zodd/blob/main/LICENSE) [![Examples](https://img.shields.io/badge/examples-view-green?style=flat&labelColor=282c34&logo=zig)](https://github.com/CogitatorTech/zodd/tree/main/examples) [![Docs](https://img.shields.io/badge/docs-read-blue?style=flat&labelColor=282c34&logo=read-the-docs)](https://CogitatorTech.github.io/zodd/) -[![Zig Version](https://img.shields.io/badge/Zig-0.15.2-orange?logo=zig&labelColor=282c34)](https://ziglang.org/download/) +[![Zig](https://img.shields.io/badge/zig-0.15.2-F7A41D?style=flat&labelColor=282c34&logo=zig)](https://ziglang.org/download/) [![Release](https://img.shields.io/github/release/CogitatorTech/zodd.svg?label=release&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/zodd/releases/latest) A small embeddable Datalog engine in Zig From acde78b6dae239d565e8b4e25a1af549c1cf3af7 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Tue, 21 Apr 2026 11:31:03 +0200 Subject: [PATCH 05/12] Add an `AGENTS.md` to the project --- .gitignore | 3 + AGENTS.md | 159 ++++++++++++++++++++++++++++++++++++++++++++++++ CONTRIBUTING.md | 3 + 3 files changed, 165 insertions(+) create mode 100644 AGENTS.md diff --git a/.gitignore b/.gitignore index d060955..1cbb41c 100644 --- a/.gitignore +++ b/.gitignore @@ -98,3 +98,6 @@ docs/api/ POST.md site/ core.* +.claude/ +.codex +zig-pkg/ diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..3ff2b3d --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,159 @@ +# AGENTS.md + +This file provides guidance to coding agents collaborating on this repository. + +## Mission + +Zodd is a small, embeddable [Datalog](https://en.wikipedia.org/wiki/Datalog) engine written in pure Zig. +It evaluates recursive rules over sets of tuples using semi-naive iteration, merge joins, and indexed extension primitives. +Zodd is designed to be embedded in Zig projects as a library. +Priorities, in order: + +1. Correctness of relations, variables, joins, extensions, and fixed-point iteration. +2. Minimal public API for use as a library from other Zig projects. +3. Small dependency footprint and maintainable, well-tested code. +4. Cross-platform support (Linux, macOS, and Windows). + +## Core Rules + +- Use English for code, comments, docs, and tests. +- Prefer small, focused changes over large refactoring. +- Add comments only when they clarify non-obvious behavior. +- Do not add features, error handling, or abstractions beyond what is needed for the current task. +- Keep the dependency set small: do not add new Zig packages or C libraries without prior discussion. + +## Writing Style + +- Use Oxford commas in inline lists: "a, b, and c" not "a, b, c". +- Do not use em dashes. Restructure the sentence, or use a colon or semicolon instead. +- Avoid colorful adjectives and adverbs. Write "Datalog engine" not "blazing-fast Datalog engine", "merge join" not "efficient merge join". +- Use noun phrases for checklist items, not imperative verbs. Write "redundant index detection" not "detect redundant indexes". +- Headings in Markdown files must be in the title case: "Build from Source" not "Build from source". Minor words (a, an, the, and, but, or, for, in, + on, at, to, by, of, is, are, was, were, be) stay lowercase unless they are the first word. + +## Repository Layout + +- `src/lib.zig`: Public API entry point. Re-exports `Relation`, `Variable`, `Iteration`, `ExecutionContext`, join helpers, and extend primitives. +- `src/zodd/relation.zig`: Immutable `Relation` type (sorted, deduplicated tuples). +- `src/zodd/variable.zig`: Mutable `Variable` type for fixed-point iteration, plus the `gallop` search helper. +- `src/zodd/iteration.zig`: `Iteration` driver for semi-naive evaluation. +- `src/zodd/join.zig`: Merge-join algorithms (`joinHelper`, `joinInto`, `joinAnti`). +- `src/zodd/extend.zig`: Leaper-based extension primitives (`ExtendWith`, `FilterAnti`, `ExtendAnti`, `extendInto`). +- `src/zodd/index.zig`: Indexes for keyed lookups. +- `src/zodd/aggregate.zig`: Group-by and aggregation operations. +- `src/zodd/context.zig`: `ExecutionContext` (allocator and shared state for a Datalog run). +- `tests/`: Non-unit tests (`integration_tests.zig`, `regression_tests.zig`, `property_tests.zig`, `incremental_tests.zig`). +- `examples/`: Self-contained example programs (`e1_network_reachability.zig` through `e6_dependency_resolution.zig`) built as executables via + `build.zig`. +- `.github/workflows/`: CI workflows (`tests.yml` for unit and integration tests, `docs.yml` for API doc deployment). +- `build.zig` / `build.zig.zon`: Zig build configuration and package metadata. +- `Makefile`: GNU Make wrapper around `zig build` targets. +- `docs/`: Generated API docs land in `docs/api/` (produced by `make docs`). + +## Architecture + +### Evaluation Pipeline + +A Datalog program flows through: `ExecutionContext` (`context.zig`) owns the allocator and shared state. Base data is loaded into a `Relation` +(`relation.zig`). Derived predicates use a `Variable` (`variable.zig`) driven by an `Iteration` (`iteration.zig`) loop that calls `changed()` until a +fixed point. Each iteration extends tuples via `join` (`join.zig`) or `extend` (`extend.zig`), optionally using indexes (`index.zig`) or aggregates +(`aggregate.zig`). + +### Relations and Variables Split + +- `relation.zig` is the immutable, sorted, deduplicated tuple container used for base facts and finalized results. +- `variable.zig` is the mutable counterpart used inside fixed-point loops; it tracks stable, recent, and to-add tuple sets for semi-naive evaluation. +- New join shapes go in `join.zig`. New leaper-style extensions go in `extend.zig`. + +### Indexing and Aggregation + +`index.zig` provides keyed lookups used by the extend primitives. `aggregate.zig` provides group-by reductions. +When adding a new join or extension shape, consider whether it needs an index variant and add it alongside the existing ones. + +### Public API Surface + +Everything re-exported from `src/lib.zig` is part of the public API. +Changes to names or signatures there are breaking. +The rest of `src/zodd/` is internal and may be refactored freely as long as the public surface and its behavior are preserved. + +### Dependencies + +Zodd depends on two sibling Zig packages declared in `build.zig.zon`: + +- `ordered`: sorted container primitives, linked into the `zodd` module for all builds. +- `minish`: property-testing framework, used only by `tests/property_tests.zig` and lazy-loaded in `build.zig`. + +Please do not add further dependencies without prior discussion. + +## Zig Conventions + +- Zig version: 0.15.2 (as declared in `build.zig.zon`). The Makefile's `ZIG_LOCAL` path points at a local 0.16.0 install for development; CI pins the + version declared in `build.zig.zon`. +- Formatting is enforced by `zig fmt`. Run `make format` before committing. +- Naming follows Zig standard-library conventions: `camelCase` for functions (e.g. `joinInto`, `extendInto`, `fromSlice`), `snake_case` for local + variables and struct fields, `PascalCase` for types and structs (e.g. `Relation`, `Variable`, `ExecutionContext`), and `SCREAMING_SNAKE_CASE` for + top-level compile-time constants. + +## Required Validation + +Run the relevant targets for any change: + +| Target | Command | What It Runs | +|----------------|------------------------------------------------|-----------------------------------------------------------------------| +| Unit tests | `make test` | Inline `test` blocks in `src/` plus every file under `tests/` | +| Lint | `make lint` | Checks Zig formatting with `zig fmt --check` over `src/` and `tests/` | +| Examples | `make example` | Builds and runs every example under `examples/` | +| Single example | `make example EXAMPLE=e1_network_reachability` | Runs one example program | +| Docs | `make docs` | Generates API docs into `docs/api` | +| Everything | `make all` | Runs `build`, `test`, `lint`, and `docs` | + +## First Contribution Flow + +1. Read the relevant module under `src/zodd/` (often `relation.zig`, `variable.zig`, `join.zig`, or `extend.zig`). +2. Implement the smallest change that covers the requirement. +3. Add or update inline `test` blocks in the changed Zig module, or extend a test file under `tests/`, to cover the new behavior. +4. Run `make test` and `make lint`. +5. If public behavior changed, also run `make example` to ensure no example regresses. + +Good first tasks: + +- Add a new join or extension shape in `src/zodd/join.zig` or `src/zodd/extend.zig` (with an inline `test` block and, if appropriate, an integration + test under `tests/`). +- Improve an existing index strategy in `src/zodd/index.zig`. +- Add a new aggregate operation in `src/zodd/aggregate.zig`. +- Add a new example under `examples/` demonstrating a Datalog pattern, and list it in `examples/README.md`. + +## Testing Expectations + +- Unit tests live as inline `test` blocks in the module they cover (`src/lib.zig` and `src/zodd/*.zig`). They are discovered automatically via + `std.testing.refAllDecls(@This())` in `src/lib.zig`. +- Non-unit tests live under `tests/` (`integration_tests.zig`, `regression_tests.zig`, `property_tests.zig`, `incremental_tests.zig`) and are + auto-discovered by `build.zig`. +- Property tests use the `minish` dependency and should use fixed seeds so failures are reproducible in CI. +- Every new relation, variable operation, join, extension, index, or aggregate must ship with at least one `test` block that exercises it. +- No public API change is complete without a test covering the new or changed behavior. + +## Change Design Checklist + +Before coding: + +1. Identify which module(s) the change touches (`relation`, `variable`, `iteration`, `join`, `extend`, `index`, `aggregate`, or `context`). +2. Consider whether a new join or extension needs a matching index or anti-variant. +3. Check whether the change is public-API-visible (i.e. re-exported from `src/lib.zig`); if so, treat it as a breaking or additive API change + deliberately. +4. Check cross-platform implications, especially for anything that touches the filesystem, timing, or OS-specific types. + +Before submitting: + +1. `make test` passes. +2. `make lint` passes. +3. `make example` still succeeds when touching relations, variables, joins, extensions, or iteration. +4. Docs updated (`make docs`) if the public API surface changed, and `ROADMAP.md` ticked/updated if a listed item was implemented. + +## Commit and PR Hygiene + +- Keep commits scoped to one logical change. +- PR descriptions should include: + 1. Behavioral change summary. + 2. Tests added or updated. + 3. Whether examples were run locally (yes/no), and on which OS. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 9ad3a29..b2d3de9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -28,6 +28,9 @@ would like to work on or if it has already been resolved. ### Development Workflow +> [!IMPORTANT] +> If you're using an AI-assisted coding tool like Claude Code or Codex, make sure the AI follows the instructions in the [AGENTS.md](AGENTS.md) file. + #### Prerequisites Install GNU Make on your system if it's not already installed. From c4f8f6107ba4a3bd022b6f9526ebdadf9d85ade9 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Tue, 21 Apr 2026 12:19:14 +0200 Subject: [PATCH 06/12] Upgrade Zig to version `0.16.0` --- .github/workflows/docs.yml | 2 +- .github/workflows/tests.yml | 51 +++++++++++++++------- AGENTS.md | 3 +- Makefile | 3 +- README.md | 4 +- build.zig | 10 +++-- build.zig.zon | 10 ++--- examples/e3_data_lineage.zig | 4 +- src/zodd/aggregate.zig | 5 ++- src/zodd/context.zig | 45 +++++++++++++++++-- src/zodd/extend.zig | 17 ++++---- src/zodd/index.zig | 2 +- src/zodd/iteration.zig | 8 ++-- src/zodd/join.zig | 17 ++++---- src/zodd/relation.zig | 83 ++++++++++++++++++------------------ src/zodd/variable.zig | 4 +- tests/incremental_tests.zig | 4 +- tests/integration_tests.zig | 22 +++++----- tests/property_tests.zig | 26 +++++------ tests/regression_tests.zig | 40 ++++++++--------- 20 files changed, 213 insertions(+), 147 deletions(-) diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 0f262bc..e4b1b7a 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -25,7 +25,7 @@ jobs: - name: Install Zig uses: goto-bus-stop/setup-zig@v2 with: - version: '0.15.2' + version: '0.16.0' - name: Install System Dependencies run: | diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 7cf4189..76dd4b6 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -4,35 +4,54 @@ on: workflow_dispatch: push: branches: - - main + - develop tags: - 'v*' - paths-ignore: - - '**.md' - - 'docs/**' pull_request: branches: - main - paths-ignore: - - '**.md' - - 'docs/**' + +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true permissions: contents: read jobs: - test: - runs-on: ubuntu-latest + tests: + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + zig-url: https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz + zig-dir: zig-x86_64-linux-0.16.0 + - os: macos-latest + zig-url: https://ziglang.org/download/0.16.0/zig-aarch64-macos-0.16.0.tar.xz + zig-dir: zig-aarch64-macos-0.16.0 + - os: windows-latest + zig-url: https://ziglang.org/download/0.16.0/zig-x86_64-windows-0.16.0.zip + zig-dir: zig-x86_64-windows-0.16.0 steps: - - name: Checkout Repository + - name: Checkout repository uses: actions/checkout@v4 - - name: Install Dependencies + - name: Install Zig 0.16.0 (Unix) + if: runner.os != 'Windows' + run: | + curl -sSfL ${{ matrix.zig-url }} | tar -xJ + echo "$PWD/${{ matrix.zig-dir }}" >> "$GITHUB_PATH" + + - name: Install Zig 0.16.0 (Windows) + if: runner.os == 'Windows' + shell: pwsh run: | - sudo apt-get update - sudo apt-get install -y make - make install-deps + Invoke-WebRequest -Uri "${{ matrix.zig-url }}" -OutFile zig.zip + Expand-Archive zig.zip -DestinationPath . + echo "$PWD\${{ matrix.zig-dir }}" | Out-File -Append -FilePath $env:GITHUB_PATH - - name: Run Tests - run: make test + - name: Run tests + run: zig build test --summary all diff --git a/AGENTS.md b/AGENTS.md index 3ff2b3d..654b6ca 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -87,8 +87,7 @@ Please do not add further dependencies without prior discussion. ## Zig Conventions -- Zig version: 0.15.2 (as declared in `build.zig.zon`). The Makefile's `ZIG_LOCAL` path points at a local 0.16.0 install for development; CI pins the - version declared in `build.zig.zon`. +- Zig version: 0.16.0 (as declared in `build.zig.zon` and the Makefile's `ZIG_LOCAL` path). CI pins the version declared in `build.zig.zon`. - Formatting is enforced by `zig fmt`. Run `make format` before committing. - Naming follows Zig standard-library conventions: `camelCase` for functions (e.g. `joinInto`, `extendInto`, `fromSlice`), `snake_case` for local variables and struct fields, `PascalCase` for types and structs (e.g. `Relation`, `Variable`, `ExecutionContext`), and `SCREAMING_SNAKE_CASE` for diff --git a/Makefile b/Makefile index 58df5ea..fc0c21b 100644 --- a/Makefile +++ b/Makefile @@ -1,7 +1,8 @@ ################################################################################ # Configuration and Variables ################################################################################ -ZIG ?= $(shell which zig || echo ~/.local/share/zig/0.15.2/zig) +ZIG_LOCAL := $(HOME)/.local/share/zig/0.16.0/zig +ZIG ?= $(shell test -x $(ZIG_LOCAL) && echo $(ZIG_LOCAL) || which zig) ZIG_VERSION := $(shell $(ZIG) version) BUILD_TYPE ?= Debug BUILD_OPTS = -Doptimize=$(BUILD_TYPE) diff --git a/README.md b/README.md index c4e2230..024a3a8 100644 --- a/README.md +++ b/README.md @@ -10,7 +10,7 @@ [![License](https://img.shields.io/badge/license-MIT-007ec6?label=license&style=flat&labelColor=282c34&logo=open-source-initiative)](https://github.com/CogitatorTech/zodd/blob/main/LICENSE) [![Examples](https://img.shields.io/badge/examples-view-green?style=flat&labelColor=282c34&logo=zig)](https://github.com/CogitatorTech/zodd/tree/main/examples) [![Docs](https://img.shields.io/badge/docs-read-blue?style=flat&labelColor=282c34&logo=read-the-docs)](https://CogitatorTech.github.io/zodd/) -[![Zig](https://img.shields.io/badge/zig-0.15.2-F7A41D?style=flat&labelColor=282c34&logo=zig)](https://ziglang.org/download/) +[![Zig](https://img.shields.io/badge/zig-0.16.0-F7A41D?style=flat&labelColor=282c34&logo=zig)](https://ziglang.org/download/) [![Release](https://img.shields.io/github/release/CogitatorTech/zodd.svg?label=release&style=flat&labelColor=282c34&logo=github)](https://github.com/CogitatorTech/zodd/releases/latest) A small embeddable Datalog engine in Zig @@ -105,7 +105,7 @@ Replace `` with the desired branch or release tag, like `main` (f This command will download Zodd and add it to Zig's global cache and update your project's `build.zig.zon` file. > [!NOTE] -> Zodd is developed and tested with Zig version 0.15.2. +> Zodd is developed and tested with Zig version 0.16.0. #### Adding to Build Script diff --git a/build.zig b/build.zig index 4fef17e..7c38ad2 100644 --- a/build.zig +++ b/build.zig @@ -36,9 +36,11 @@ pub fn build(b: *std.Build) void { const test_step = b.step("test", "Run all tests"); test_step.dependOn(&run_lib_tests.step); + const io = b.graph.io; + // Discover and add tests from tests/ directory // (only available when developing zodd, not when used as a dependency) - if (std.fs.cwd().openDir("tests", .{ .iterate = true })) |tests_dir| { + if (b.build_root.handle.openDir(io, "tests", .{ .iterate = true })) |tests_dir| { // Lazy-load Minish dependency (only needed for property tests) const minish_dep = b.dependency("minish", .{ .target = target, @@ -47,7 +49,7 @@ pub fn build(b: *std.Build) void { var dir = tests_dir; var it = dir.iterate(); - while (it.next() catch null) |entry| { + while (it.next(io) catch null) |entry| { if (entry.kind != .file) continue; if (!std.mem.endsWith(u8, entry.name, ".zig")) continue; @@ -76,12 +78,12 @@ pub fn build(b: *std.Build) void { } else |_| {} // Discover and add examples from examples/ directory - if (std.fs.cwd().openDir("examples", .{ .iterate = true })) |examples_dir| { + if (b.build_root.handle.openDir(io, "examples", .{ .iterate = true })) |examples_dir| { var dir = examples_dir; const run_all_step = b.step("run-all", "Run all examples"); var it = dir.iterate(); - while (it.next() catch null) |entry| { + while (it.next(io) catch null) |entry| { if (entry.kind != .file) continue; if (!std.mem.endsWith(u8, entry.name, ".zig")) continue; diff --git a/build.zig.zon b/build.zig.zon index 1cbb5a0..681bc94 100644 --- a/build.zig.zon +++ b/build.zig.zon @@ -2,15 +2,15 @@ .name = .zodd, .version = "0.1.0-alpha.3", .fingerprint = 0x2d03181bdd24914c, // Changing this has security and trust implications. - .minimum_zig_version = "0.15.2", + .minimum_zig_version = "0.16.0", .dependencies = .{ .minish = .{ - .url = "https://github.com/CogitatorTech/minish/archive/refs/tags/v0.1.0.tar.gz", - .hash = "minish-0.1.0-SQtSTWHkAQACfz3xGuWHU8zbx320vK_47r2yto3Pq0Rf", + .url = "https://github.com/CogitatorTech/minish/archive/refs/tags/v0.3.0.tar.gz", + .hash = "minish-0.3.0-SQtSTYI3AgCxWdWbKxS_lvmfbp0wJk29ZW5C9CozJaxm", }, .ordered = .{ - .url = "https://github.com/CogitatorTech/ordered/archive/v0.1.0.tar.gz", - .hash = "ordered-0.1.0-Gy41sFoCAgDHx9wCElUaCuHvNP7idFfa375M2-UHXMPf", + .url = "https://github.com/CogitatorTech/ordered/archive/refs/tags/v0.2.0.tar.gz", + .hash = "ordered-0.2.0-Gy41sAAkAgBO3TZAn9nYuspdeDcD_sHLoIGEZY4pCDDM", }, }, .paths = .{ "build.zig", "build.zig.zon", "src", "LICENSE", "README.md" }, diff --git a/examples/e3_data_lineage.zig b/examples/e3_data_lineage.zig index 73d9f90..d57835c 100644 --- a/examples/e3_data_lineage.zig +++ b/examples/e3_data_lineage.zig @@ -251,14 +251,14 @@ pub fn main() !void { for (source_pii_data) |src| { // BFS from source through non-anonymized transforms to see if it reaches the target - var frontier = std.ArrayListUnmanaged(u32){}; + var frontier = std.ArrayListUnmanaged(u32).empty; defer frontier.deinit(allocator); try frontier.append(allocator, src[0]); var found = false; var step: usize = 0; while (frontier.items.len > 0 and step < 20) : (step += 1) { - var next_frontier = std.ArrayListUnmanaged(u32){}; + var next_frontier = std.ArrayListUnmanaged(u32).empty; defer next_frontier.deinit(allocator); for (frontier.items) |node| { diff --git a/src/zodd/aggregate.zig b/src/zodd/aggregate.zig index d264cfb..6bc6ce4 100644 --- a/src/zodd/aggregate.zig +++ b/src/zodd/aggregate.zig @@ -10,6 +10,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Relation = @import("relation.zig").Relation; const ExecutionContext = @import("context.zig").ExecutionContext; +const WaitGroup = @import("context.zig").WaitGroup; /// Aggregate tuples by key using a folder. pub fn aggregate( @@ -55,7 +56,7 @@ pub fn aggregate( const tasks = try ctx.allocator.alloc(Task, task_count); defer ctx.allocator.free(tasks); - var wg: std.Thread.WaitGroup = .{}; + var wg: WaitGroup = .{}; var t: usize = 0; while (t < task_count) : (t += 1) { const start = t * chunk; @@ -84,7 +85,7 @@ pub fn aggregate( }; std.sort.pdq(Intermediate, intermediates, {}, sortContext.lessThan); - var results = std.ArrayListUnmanaged(ResultTuple){}; + var results = std.ArrayListUnmanaged(ResultTuple).empty; defer results.deinit(ctx.allocator); if (intermediates.len > 0) { diff --git a/src/zodd/context.zig b/src/zodd/context.zig index 52aa563..0c095ec 100644 --- a/src/zodd/context.zig +++ b/src/zodd/context.zig @@ -8,11 +8,50 @@ const std = @import("std"); const Allocator = std.mem.Allocator; +/// WaitGroup is a no-op synchronization marker. Since `Pool.spawnWg` currently +/// runs work inline on the calling thread, no real waiting is needed. The type +/// exists so that call sites can continue to declare `var wg: WaitGroup = .{}` +/// and call `wg.wait()` without churn. +pub const WaitGroup = struct { + pub fn wait(self: *WaitGroup) void { + _ = self; + } +}; + +/// Pool replaces the `std.Thread.Pool` removed in Zig 0.16. It currently +/// executes submitted work synchronously on the calling thread. This preserves +/// the call-site API so that the parallel code paths still compile and behave +/// correctly; real parallel execution can be layered back on once a stable +/// 0.16 concurrency story lands in user-facing std (today it lives behind +/// `std.Io` backends). +pub const Pool = struct { + allocator: Allocator, + + pub const Options = struct { + allocator: Allocator, + n_jobs: ?usize = null, + }; + + pub fn init(self: *Pool, options: Options) !void { + self.* = .{ .allocator = options.allocator }; + } + + pub fn deinit(self: *Pool) void { + self.* = undefined; + } + + pub fn spawnWg(self: *Pool, wg: *WaitGroup, comptime func: anytype, args: anytype) void { + _ = self; + _ = wg; + @call(.auto, func, args); + } +}; + pub const ExecutionContext = struct { /// Allocator for the context. allocator: Allocator, /// Thread pool for parallel execution. - pool: ?*std.Thread.Pool = null, + pool: ?*Pool = null, /// Initializes a new execution context. pub fn init(allocator: Allocator) ExecutionContext { @@ -21,9 +60,9 @@ pub const ExecutionContext = struct { /// Initializes a new execution context with a thread pool. pub fn initWithThreads(allocator: Allocator, worker_count: usize) !ExecutionContext { - const pool = try allocator.create(std.Thread.Pool); + const pool = try allocator.create(Pool); errdefer allocator.destroy(pool); - try std.Thread.Pool.init(pool, .{ .allocator = allocator, .n_jobs = worker_count }); + try Pool.init(pool, .{ .allocator = allocator, .n_jobs = worker_count }); return .{ .allocator = allocator, .pool = pool }; } diff --git a/src/zodd/extend.zig b/src/zodd/extend.zig index a8a4e5a..232ad9b 100644 --- a/src/zodd/extend.zig +++ b/src/zodd/extend.zig @@ -17,6 +17,7 @@ const Relation = @import("relation.zig").Relation; const Variable = @import("variable.zig").Variable; const gallop = @import("variable.zig").gallop; const ExecutionContext = @import("context.zig").ExecutionContext; +const WaitGroup = @import("context.zig").WaitGroup; /// Creates a Leaper interface type for a Tuple and Value type. /// @@ -354,10 +355,10 @@ pub fn extendInto( const ResultList = std.ArrayListUnmanaged(Result); const ValList = std.ArrayListUnmanaged(*const Val); - var results = ResultList{}; + var results = ResultList.empty; defer results.deinit(output.allocator); - var values = ValList{}; + var values = ValList.empty; defer values.deinit(output.allocator); var had_error = false; @@ -370,12 +371,12 @@ pub fn extendInto( slice: []const Tuple, base_leapers: []Leaper(Tuple, Val), leapers: []Leaper(Tuple, Val) = &[_]Leaper(Tuple, Val){}, - results: std.ArrayListUnmanaged(Result) = .{}, + results: std.ArrayListUnmanaged(Result) = .empty, had_error: bool = false, logic_fn: *const fn (*const Tuple, *const Val) Result, fn run(task: *@This()) void { - var local_values = std.ArrayListUnmanaged(*const Val){}; + var local_values = std.ArrayListUnmanaged(*const Val).empty; defer local_values.deinit(task.base_leapers[0].allocator); for (task.slice) |*tuple| { @@ -455,7 +456,7 @@ pub fn extendInto( } if (ctx.pool) |*pool| { - var wg: std.Thread.WaitGroup = .{}; + var wg: WaitGroup = .{}; for (tasks) |*task| { pool.*.spawnWg(&wg, Task.run, .{task}); } @@ -681,7 +682,7 @@ test "ExtendAnti: proposes absent values" { }.f); const tuple: u32 = 1; - var values = std.ArrayListUnmanaged(*const u32){}; + var values = std.ArrayListUnmanaged(*const u32).empty; defer values.deinit(allocator); // Candidates to check: 10 (present), 15 (absent), 20 (present), 30 (absent) @@ -824,7 +825,7 @@ test "ExtendWith: count zero does not propose values" { const cnt = ext.leaper().count(&tuple); try std.testing.expectEqual(@as(usize, 0), cnt); - var values = std.ArrayListUnmanaged(*const u32){}; + var values = std.ArrayListUnmanaged(*const u32).empty; defer values.deinit(allocator); var leaper = ext.leaper(); leaper.propose(&tuple, &values); @@ -857,7 +858,7 @@ test "FilterAnti and ExtendAnti: empty relation" { const v10: u32 = 10; const v20: u32 = 20; - var values = std.ArrayListUnmanaged(*const u32){}; + var values = std.ArrayListUnmanaged(*const u32).empty; defer values.deinit(allocator); try values.append(allocator, &v10); try values.append(allocator, &v20); diff --git a/src/zodd/index.zig b/src/zodd/index.zig index 4b53461..e3a5348 100644 --- a/src/zodd/index.zig +++ b/src/zodd/index.zig @@ -82,7 +82,7 @@ pub fn SecondaryIndex( var iter = try self.map.iterator(); defer iter.deinit(); - var result_tuples = std.ArrayListUnmanaged(Tuple){}; + var result_tuples = std.ArrayListUnmanaged(Tuple).empty; defer result_tuples.deinit(self.allocator); while (try iter.next()) |entry| { diff --git a/src/zodd/iteration.zig b/src/zodd/iteration.zig index fab17de..b94c149 100644 --- a/src/zodd/iteration.zig +++ b/src/zodd/iteration.zig @@ -10,6 +10,8 @@ const Allocator = std.mem.Allocator; const Variable = @import("variable.zig").Variable; const Relation = @import("relation.zig").Relation; const ExecutionContext = @import("context.zig").ExecutionContext; +const Pool = @import("context.zig").Pool; +const WaitGroup = @import("context.zig").WaitGroup; pub fn Iteration(comptime Tuple: type) type { return struct { @@ -31,7 +33,7 @@ pub fn Iteration(comptime Tuple: type) type { /// Initializes a new iteration. pub fn init(ctx: *ExecutionContext, max_iterations: ?usize) Self { return Self{ - .variables = VarList{}, + .variables = VarList.empty, .allocator = ctx.allocator, .ctx = ctx, .max_iterations = max_iterations orelse std.math.maxInt(usize), @@ -78,7 +80,7 @@ pub fn Iteration(comptime Tuple: type) type { return any_changed; } - fn changedParallel(self: *Self, pool: *std.Thread.Pool) !bool { + fn changedParallel(self: *Self, pool: *Pool) !bool { const count = self.variables.items.len; const Task = struct { var_ptr: *Var, @@ -96,7 +98,7 @@ pub fn Iteration(comptime Tuple: type) type { const tasks = try self.allocator.alloc(Task, count); defer self.allocator.free(tasks); - var wg: std.Thread.WaitGroup = .{}; + var wg: WaitGroup = .{}; for (self.variables.items, 0..) |v, i| { tasks[i] = .{ .var_ptr = v }; pool.spawnWg(&wg, Task.run, .{&tasks[i]}); diff --git a/src/zodd/join.zig b/src/zodd/join.zig index 0e227aa..7d55615 100644 --- a/src/zodd/join.zig +++ b/src/zodd/join.zig @@ -15,6 +15,7 @@ const Relation = @import("relation.zig").Relation; const Variable = @import("variable.zig").Variable; const gallop = @import("variable.zig").gallop; const ExecutionContext = @import("context.zig").ExecutionContext; +const WaitGroup = @import("context.zig").WaitGroup; /// Performs a merge-join between two sorted relations on a common key. /// @@ -126,7 +127,7 @@ pub fn joinInto( logic: fn (*const Key, *const Val1, *const Val2) Result, ) Allocator.Error!void { const ResultList = std.ArrayListUnmanaged(Result); - var results = ResultList{}; + var results = ResultList.empty; defer results.deinit(output.allocator); const Context = struct { @@ -149,7 +150,7 @@ pub fn joinInto( const Task = struct { left: *const Relation(struct { Key, Val1 }), right: *const Relation(struct { Key, Val2 }), - results: std.ArrayListUnmanaged(Result) = .{}, + results: std.ArrayListUnmanaged(Result) = .empty, alloc: Allocator, had_error: bool = false, @@ -193,7 +194,7 @@ pub fn joinInto( } if (ctx.pool) |*pool| { - var wg: std.Thread.WaitGroup = .{}; + var wg: WaitGroup = .{}; for (tasks) |*task| { pool.*.spawnWg(&wg, Task.run, .{task}); } @@ -244,14 +245,14 @@ pub fn joinAnti( logic: fn (*const Key, *const Val) Result, ) Allocator.Error!void { const ResultList = std.ArrayListUnmanaged(Result); - var results = ResultList{}; + var results = ResultList.empty; defer results.deinit(output.allocator); if (ctx.pool != null and input.recent.elements.len > 0) { const Task = struct { slice: []const struct { Key, Val }, filter: *const Variable(struct { Key, FilterVal }), - results: std.ArrayListUnmanaged(Result) = .{}, + results: std.ArrayListUnmanaged(Result) = .empty, alloc: Allocator, logic: *const fn (*const Key, *const Val) Result, had_error: bool = false, @@ -306,7 +307,7 @@ pub fn joinAnti( } if (ctx.pool) |*pool| { - var wg: std.Thread.WaitGroup = .{}; + var wg: WaitGroup = .{}; for (tasks) |*task| { pool.*.spawnWg(&wg, Task.run, .{task}); } @@ -388,7 +389,7 @@ test "joinHelper: basic" { } }; - var results = ResultList{}; + var results = ResultList.empty; defer results.deinit(allocator); joinHelper(u32, u32, u32, &input1, &input2, Context{ .results = &results, .alloc = allocator }, Context.callback); @@ -489,7 +490,7 @@ test "joinHelper: multiplicative matches" { } }; - var results = ResultList{}; + var results = ResultList.empty; defer results.deinit(allocator); joinHelper(u32, u32, u32, &input1, &input2, Context{ .results = &results, .alloc = allocator }, Context.callback); diff --git a/src/zodd/relation.zig b/src/zodd/relation.zig index 0b32046..4acb92f 100644 --- a/src/zodd/relation.zig +++ b/src/zodd/relation.zig @@ -26,6 +26,7 @@ const mem = std.mem; const sort = std.sort; const Allocator = mem.Allocator; const ExecutionContext = @import("context.zig").ExecutionContext; +const WaitGroup = @import("context.zig").WaitGroup; pub fn Relation(comptime Tuple: type) type { return struct { @@ -78,7 +79,7 @@ pub fn Relation(comptime Tuple: type) type { const tasks = try ctx.allocator.alloc(Task, task_count); defer ctx.allocator.free(tasks); - var wg: std.Thread.WaitGroup = .{}; + var wg: WaitGroup = .{}; var t: usize = 0; while (t < task_count) : (t += 1) { const start = t * chunk; @@ -111,7 +112,7 @@ pub fn Relation(comptime Tuple: type) type { const tasks = try ctx.allocator.alloc(Task, task_count); defer ctx.allocator.free(tasks); - var wg: std.Thread.WaitGroup = .{}; + var wg: WaitGroup = .{}; var t2: usize = 0; while (t2 < task_count) : (t2 += 1) { const start = t2 * chunk; @@ -345,17 +346,17 @@ pub fn Relation(comptime Tuple: type) type { fn readValue(comptime T: type, reader: anytype) !T { return switch (@typeInfo(T)) { - .int => try reader.readInt(T, .little), - .bool => (try reader.readInt(u8, .little)) != 0, + .int => try reader.takeInt(T, .little), + .bool => (try reader.takeInt(u8, .little)) != 0, .float => blk: { const IntType = std.meta.Int(.unsigned, @bitSizeOf(T)); - const bits = try reader.readInt(IntType, .little); + const bits = try reader.takeInt(IntType, .little); break :blk @as(T, @bitCast(bits)); }, .@"enum" => blk: { const info = @typeInfo(T).@"enum"; const Tag = info.tag_type; - const bits = try reader.readInt(Tag, .little); + const bits = try reader.takeInt(Tag, .little); break :blk @as(T, @enumFromInt(bits)); }, .array => |info| blk: { @@ -396,16 +397,16 @@ pub fn Relation(comptime Tuple: type) type { /// Loads a relation from a reader with a limit on the number of elements. pub fn loadWithLimit(ctx: *ExecutionContext, reader: anytype, max_len: usize) !Self { if (!isSerializableType(Tuple)) return error.UnsupportedType; - const magic = try reader.readBytesNoEof(7); - if (!std.mem.eql(u8, &magic, "ZODDREL")) { + const magic = try reader.takeArray(7); + if (!std.mem.eql(u8, magic, "ZODDREL")) { return error.InvalidFormat; } - const version = try reader.readInt(u8, .little); + const version = try reader.takeInt(u8, .little); if (version != 1) { return error.UnsupportedVersion; } - const length_u64 = try reader.readInt(u64, .little); + const length_u64 = try reader.takeInt(u64, .little); const length = std.math.cast(usize, length_u64) orelse return error.InvalidFormat; if (length == 0) { return Self.empty(ctx); @@ -465,13 +466,13 @@ test "Relation: persistence" { }); defer original.deinit(); - var buffer = std.ArrayListUnmanaged(u8){}; - defer buffer.deinit(allocator); + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); - try original.save(buffer.writer(allocator)); + try original.save(&aw.writer); - var fbs = std.io.fixedBufferStream(buffer.items); - var loaded = try Relation(Tuple).load(&ctx, fbs.reader()); + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + var loaded = try Relation(Tuple).load(&ctx, &reader); defer loaded.deinit(); try std.testing.expectEqual(original.len(), loaded.len()); @@ -529,10 +530,10 @@ test "Relation: load normalizes order" { var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var buffer = std.ArrayListUnmanaged(u8){}; - defer buffer.deinit(allocator); + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); - var writer = buffer.writer(allocator); + const writer = &aw.writer; try writer.writeAll("ZODDREL"); try writer.writeInt(u8, 1, .little); const raw = [_]Tuple{ @@ -546,8 +547,8 @@ test "Relation: load normalizes order" { try writer.writeInt(u32, tuple[1], .little); } - var reader = std.io.fixedBufferStream(buffer.items); - var rel = try Relation(Tuple).load(&ctx, reader.reader()); + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + var rel = try Relation(Tuple).load(&ctx, &reader); defer rel.deinit(); try std.testing.expectEqual(@as(usize, 2), rel.len()); @@ -559,16 +560,16 @@ test "Relation: loadWithLimit zero length with zero limit" { const allocator = std.testing.allocator; var ctx = ExecutionContext.init(allocator); - var buffer = std.ArrayListUnmanaged(u8){}; - defer buffer.deinit(allocator); + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); - var writer = buffer.writer(allocator); + const writer = &aw.writer; try writer.writeAll("ZODDREL"); try writer.writeInt(u8, 1, .little); try writer.writeInt(u64, 0, .little); - var reader = std.io.fixedBufferStream(buffer.items); - var rel = try Relation(u32).loadWithLimit(&ctx, reader.reader(), 0); + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + var rel = try Relation(u32).loadWithLimit(&ctx, &reader, 0); defer rel.deinit(); try std.testing.expectEqual(@as(usize, 0), rel.len()); @@ -581,13 +582,13 @@ test "Relation: scalar save and load" { var original = try Relation(u32).fromSlice(&ctx, &[_]u32{ 3, 1, 2, 2 }); defer original.deinit(); - var buffer = std.ArrayListUnmanaged(u8){}; - defer buffer.deinit(allocator); + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); - try original.save(buffer.writer(allocator)); + try original.save(&aw.writer); - var fbs = std.io.fixedBufferStream(buffer.items); - var loaded = try Relation(u32).load(&ctx, fbs.reader()); + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + var loaded = try Relation(u32).load(&ctx, &reader); defer loaded.deinit(); try std.testing.expectEqual(original.len(), loaded.len()); @@ -631,18 +632,18 @@ test "Relation: save/load unsupported type" { var rel = Relation(Bad).empty(&ctx); defer rel.deinit(); - var buffer = std.ArrayListUnmanaged(u8){}; - defer buffer.deinit(allocator); + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); - try std.testing.expectError(error.UnsupportedType, rel.save(buffer.writer(allocator))); + try std.testing.expectError(error.UnsupportedType, rel.save(&aw.writer)); var header: [16]u8 = undefined; - var fbs = std.io.fixedBufferStream(&header); - try fbs.writer().writeAll("ZODDREL"); - try fbs.writer().writeInt(u8, 1, .little); - try fbs.writer().writeInt(u64, 0, .little); - const used = fbs.pos; - - var reader_fbs = std.io.fixedBufferStream(header[0..used]); - try std.testing.expectError(error.UnsupportedType, Relation(Bad).load(&ctx, reader_fbs.reader())); + var header_writer = std.Io.Writer.fixed(&header); + try header_writer.writeAll("ZODDREL"); + try header_writer.writeInt(u8, 1, .little); + try header_writer.writeInt(u64, 0, .little); + const used = header_writer.end; + + var reader_fbs = std.Io.Reader.fixed(header[0..used]); + try std.testing.expectError(error.UnsupportedType, Relation(Bad).load(&ctx, &reader_fbs)); } diff --git a/src/zodd/variable.zig b/src/zodd/variable.zig index e0900a1..35e0488 100644 --- a/src/zodd/variable.zig +++ b/src/zodd/variable.zig @@ -51,9 +51,9 @@ pub fn Variable(comptime Tuple: type) type { /// - `initial_data`: (Optional) The initial relation. pub fn init(ctx: *ExecutionContext) Self { return Self{ - .stable = RelList{}, + .stable = RelList.empty, .recent = Rel.empty(ctx), - .to_add = RelList{}, + .to_add = RelList.empty, .allocator = ctx.allocator, .ctx = ctx, }; diff --git a/tests/incremental_tests.zig b/tests/incremental_tests.zig index b9ea6eb..9623c38 100644 --- a/tests/incremental_tests.zig +++ b/tests/incremental_tests.zig @@ -110,7 +110,7 @@ test "incremental maintenance: transitive closure re-convergence" { var iters: usize = 0; while (try reachable.changed()) { - var new = EdgeList{}; + var new = EdgeList.empty; defer new.deinit(allocator); for (reachable.recent.elements) |r| { @@ -140,7 +140,7 @@ test "incremental maintenance: transitive closure re-convergence" { iters = 0; while (try reachable.changed()) { - var new = EdgeList{}; + var new = EdgeList.empty; defer new.deinit(allocator); for (reachable.recent.elements) |r| { diff --git a/tests/integration_tests.zig b/tests/integration_tests.zig index 84d6738..47727f8 100644 --- a/tests/integration_tests.zig +++ b/tests/integration_tests.zig @@ -22,7 +22,7 @@ test "transitive closure: linear chain" { var iters: usize = 0; while (try reachable.changed()) : (iters += 1) { - var results = EdgeList{}; + var results = EdgeList.empty; defer results.deinit(allocator); for (reachable.recent.elements) |r| { @@ -67,7 +67,7 @@ test "transitive closure: diamond graph" { var iters: usize = 0; while (try reachable.changed()) : (iters += 1) { - var results = EdgeList{}; + var results = EdgeList.empty; defer results.deinit(allocator); for (reachable.recent.elements) |r| { @@ -111,7 +111,7 @@ test "transitive closure: cycle detection" { var iters: usize = 0; while (try reachable.changed()) : (iters += 1) { - var results = EdgeList{}; + var results = EdgeList.empty; defer results.deinit(allocator); for (reachable.recent.elements) |r| { @@ -156,7 +156,7 @@ test "same generation: parent-child hierarchy" { var iters: usize = 0; while (try same_gen.changed()) : (iters += 1) { - var results = PairList{}; + var results = PairList.empty; defer results.deinit(allocator); for (same_gen.recent.elements) |sg| { @@ -340,7 +340,7 @@ test "SecondaryIndex: getRange randomized integration" { var idx = Index.init(&ctx); defer idx.deinit(); - var all = std.ArrayListUnmanaged(Tuple){}; + var all = std.ArrayListUnmanaged(Tuple).empty; defer all.deinit(allocator); var prng = std.Random.DefaultPrng.init(0x5a5a5a5a); @@ -362,7 +362,7 @@ test "SecondaryIndex: getRange randomized integration" { const start = @min(a, b); const end = @max(a, b); - var expected_list = std.ArrayListUnmanaged(Tuple){}; + var expected_list = std.ArrayListUnmanaged(Tuple).empty; defer expected_list.deinit(allocator); for (all.items) |t| { @@ -501,13 +501,13 @@ test "integration: persistence round-trip" { }); defer original.deinit(); - var buffer = std.ArrayListUnmanaged(u8){}; - defer buffer.deinit(allocator); + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); - try original.save(buffer.writer(allocator)); + try original.save(&aw.writer); - var fbs = std.io.fixedBufferStream(buffer.items); - var loaded = try zodd.Relation(Tuple).load(&ctx, fbs.reader()); + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + var loaded = try zodd.Relation(Tuple).load(&ctx, &reader); defer loaded.deinit(); try testing.expectEqual(original.len(), loaded.len()); diff --git a/tests/property_tests.zig b/tests/property_tests.zig index 7627662..a82f9f3 100644 --- a/tests/property_tests.zig +++ b/tests/property_tests.zig @@ -155,7 +155,7 @@ test "property: transitive closure reaches expected nodes" { var iters: usize = 0; const EdgeList = std.ArrayListUnmanaged(Edge); while (try reachable.changed()) : (iters += 1) { - var results = EdgeList{}; + var results = EdgeList.empty; defer results.deinit(testing.allocator); for (reachable.recent.elements) |r| { @@ -309,7 +309,7 @@ test "property: joinHelper matches naive join" { defer rel2.deinit(); const Result = struct { u32, u32, u32 }; - var expected_list = std.ArrayListUnmanaged(Result){}; + var expected_list = std.ArrayListUnmanaged(Result).empty; defer expected_list.deinit(testing.allocator); for (rel1.elements) |t1| { @@ -333,7 +333,7 @@ test "property: joinHelper matches naive join" { } }; - var got_list = ResultList{}; + var got_list = ResultList.empty; defer got_list.deinit(testing.allocator); zodd.joinHelper(u32, u32, u32, &rel1, &rel2, Context{ .results = &got_list, .alloc = testing.allocator }, Context.callback); @@ -387,7 +387,7 @@ test "property: joinAnti matches naive filter" { _ = try output.changed(); - var expected_list = std.ArrayListUnmanaged(Tuple){}; + var expected_list = std.ArrayListUnmanaged(Tuple).empty; defer expected_list.deinit(testing.allocator); for (input.recent.elements) |t| { @@ -461,7 +461,7 @@ test "property: extendInto matches naive extend" { _ = try output.changed(); - var expected_list = std.ArrayListUnmanaged(KV){}; + var expected_list = std.ArrayListUnmanaged(KV).empty; defer expected_list.deinit(testing.allocator); for (source.recent.elements) |t| { @@ -526,7 +526,7 @@ test "property: SecondaryIndex get matches naive filter" { while (i < rel.elements.len) : (i += 1) { const key = rel.elements[i][0]; - var expected_list = std.ArrayListUnmanaged(Tuple){}; + var expected_list = std.ArrayListUnmanaged(Tuple).empty; defer expected_list.deinit(testing.allocator); for (data) |t| { @@ -592,7 +592,7 @@ test "property: aggregate matches naive sum" { entry.value_ptr.* += t[1]; } - var expected_list = std.ArrayListUnmanaged(struct { u32, u32 }){}; + var expected_list = std.ArrayListUnmanaged(struct { u32, u32 }).empty; defer expected_list.deinit(testing.allocator); var it = map.iterator(); @@ -631,13 +631,13 @@ test "property: persistence round-trip" { var original = try zodd.Relation(Tuple).fromSlice(&ctx, data); defer original.deinit(); - var buffer = std.ArrayListUnmanaged(u8){}; - defer buffer.deinit(testing.allocator); + var aw: std.Io.Writer.Allocating = .init(testing.allocator); + defer aw.deinit(); - try original.save(buffer.writer(testing.allocator)); + try original.save(&aw.writer); - var fbs = std.io.fixedBufferStream(buffer.items); - var loaded = try zodd.Relation(Tuple).load(&ctx, fbs.reader()); + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + var loaded = try zodd.Relation(Tuple).load(&ctx, &reader); defer loaded.deinit(); try testing.expectEqual(original.len(), loaded.len()); @@ -699,7 +699,7 @@ test "property: aggregate count matches naive count" { g.value_ptr.* += 1; } - var expected_list = std.ArrayListUnmanaged(struct { u32, u32 }){}; + var expected_list = std.ArrayListUnmanaged(struct { u32, u32 }).empty; defer expected_list.deinit(testing.allocator); var it = map.iterator(); diff --git a/tests/regression_tests.zig b/tests/regression_tests.zig index 6362bbd..5868b2c 100644 --- a/tests/regression_tests.zig +++ b/tests/regression_tests.zig @@ -239,13 +239,13 @@ test "regression: Relation save and load with tuples" { }); defer original.deinit(); - var buffer = std.ArrayListUnmanaged(u8){}; - defer buffer.deinit(allocator); + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); - try original.save(buffer.writer(allocator)); + try original.save(&aw.writer); - var fbs = std.io.fixedBufferStream(buffer.items); - var loaded = try zodd.Relation(Tuple).load(&ctx, fbs.reader()); + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + var loaded = try zodd.Relation(Tuple).load(&ctx, &reader); defer loaded.deinit(); try testing.expectEqual(original.len(), loaded.len()); @@ -353,10 +353,10 @@ test "regression: Relation loadWithLimit rejects large length" { var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var buffer = std.ArrayListUnmanaged(u8){}; - defer buffer.deinit(allocator); + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); - var writer = buffer.writer(allocator); + const writer = &aw.writer; try writer.writeAll("ZODDREL"); try writer.writeInt(u8, 1, .little); try writer.writeInt(u64, 2, .little); @@ -368,8 +368,8 @@ test "regression: Relation loadWithLimit rejects large length" { try writer.writeAll(std.mem.sliceAsBytes(&arr1)); try writer.writeAll(std.mem.sliceAsBytes(&arr2)); - var reader = std.io.fixedBufferStream(buffer.items); - try testing.expectError(error.TooLarge, zodd.Relation(Tuple).loadWithLimit(&ctx, reader.reader(), 1)); + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + try testing.expectError(error.TooLarge, zodd.Relation(Tuple).loadWithLimit(&ctx, &reader, 1)); } test "regression: extendInto resets leaper error" { @@ -417,10 +417,10 @@ test "regression: loadWithLimit rejects invalid magic" { var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var buffer = std.ArrayListUnmanaged(u8){}; - defer buffer.deinit(allocator); + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); - var writer = buffer.writer(allocator); + const writer = &aw.writer; try writer.writeAll("BADMAGC"); try writer.writeInt(u8, 1, .little); try writer.writeInt(u64, 1, .little); @@ -429,8 +429,8 @@ test "regression: loadWithLimit rejects invalid magic" { const arr1 = [_]Tuple{t1}; try writer.writeAll(std.mem.sliceAsBytes(&arr1)); - var reader = std.io.fixedBufferStream(buffer.items); - try testing.expectError(error.InvalidFormat, zodd.Relation(Tuple).loadWithLimit(&ctx, reader.reader(), 10)); + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + try testing.expectError(error.InvalidFormat, zodd.Relation(Tuple).loadWithLimit(&ctx, &reader, 10)); } test "regression: loadWithLimit rejects unsupported version" { @@ -438,10 +438,10 @@ test "regression: loadWithLimit rejects unsupported version" { var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var buffer = std.ArrayListUnmanaged(u8){}; - defer buffer.deinit(allocator); + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); - var writer = buffer.writer(allocator); + const writer = &aw.writer; try writer.writeAll("ZODDREL"); try writer.writeInt(u8, 2, .little); try writer.writeInt(u64, 1, .little); @@ -450,8 +450,8 @@ test "regression: loadWithLimit rejects unsupported version" { const arr1 = [_]Tuple{t1}; try writer.writeAll(std.mem.sliceAsBytes(&arr1)); - var reader = std.io.fixedBufferStream(buffer.items); - try testing.expectError(error.UnsupportedVersion, zodd.Relation(Tuple).loadWithLimit(&ctx, reader.reader(), 10)); + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + try testing.expectError(error.UnsupportedVersion, zodd.Relation(Tuple).loadWithLimit(&ctx, &reader, 10)); } test "regression: joinAnti checks multiple stable batches" { From 5e7a32e31aad9cb9979e6173caef95569d773980 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Tue, 21 Apr 2026 13:05:58 +0200 Subject: [PATCH 07/12] Fix a few bugs --- src/zodd/extend.zig | 14 ++ src/zodd/index.zig | 57 ++++++- src/zodd/relation.zig | 24 ++- src/zodd/variable.zig | 50 +++++- tests/regression_tests.zig | 308 +++++++++++++++++++++++++++++++++++++ 5 files changed, 437 insertions(+), 16 deletions(-) diff --git a/src/zodd/extend.zig b/src/zodd/extend.zig index 232ad9b..67e6d45 100644 --- a/src/zodd/extend.zig +++ b/src/zodd/extend.zig @@ -436,6 +436,19 @@ pub fn extendInto( }; } + // Tracks how many entries of `tasks` have `leapers` populated. On an + // error partway through the clone loop, the `errdefer` below walks + // back over already-populated tasks and frees their leapers. + var populated: usize = 0; + errdefer { + for (tasks[0..populated]) |*task| { + for (task.leapers) |*leaper| { + leaper.deinit(); + } + ctx.allocator.free(task.leapers); + } + } + var t_idx: usize = 0; while (t_idx < task_count) : (t_idx += 1) { const task = &tasks[t_idx]; @@ -453,6 +466,7 @@ pub fn extendInto( cloned += 1; } task.leapers = clones; + populated = t_idx + 1; } if (ctx.pool) |*pool| { diff --git a/src/zodd/index.zig b/src/zodd/index.zig index e3a5348..476c435 100644 --- a/src/zodd/index.zig +++ b/src/zodd/index.zig @@ -40,13 +40,19 @@ pub fn SecondaryIndex( /// Deinitializes the index. pub fn deinit(self: *Self) void { - var iter = self.map.iterator() catch return; - defer iter.deinit(); - while (iter.next() catch null) |entry| { - var mut_rel = entry.value; - mut_rel.deinit(); - } - self.map.deinit(); + // Always free the map itself, even if we can't walk it. Walking + // the map requires an allocation for the traversal stack, so an + // OOM during deinit could leak the nested Relations, but we must + // not let that also leak the B-tree structure. + defer self.map.deinit(); + if (self.map.iterator()) |it| { + var iter = it; + defer iter.deinit(); + while (iter.next() catch null) |entry| { + var mut_rel = entry.value; + mut_rel.deinit(); + } + } else |_| {} } /// Inserts a tuple into the index. @@ -77,7 +83,9 @@ pub fn SecondaryIndex( return self.map.get(key); } - /// Returns a relation covering the range [start_key, end_key). + /// Returns a relation covering the closed range [start_key, end_key]. + /// Both endpoints are inclusive: entries with `key == end_key` are + /// returned. pub fn getRange(self: *Self, start_key: Key, end_key: Key) !Relation(Tuple) { var iter = try self.map.iterator(); defer iter.deinit(); @@ -161,3 +169,36 @@ test "SecondaryIndex: getRange empty and inverted" { defer inverted.deinit(); try std.testing.expectEqual(@as(usize, 0), inverted.len()); } + +test "SecondaryIndex: getRange end is inclusive" { + // Locks in the closed-interval [start, end] contract documented on + // getRange. A regression here would be a silent behavior change. + const allocator = std.testing.allocator; + var ctx = ExecutionContext.init(allocator); + const Tuple = struct { u32, u32 }; + + const Index = SecondaryIndex(Tuple, u32, struct { + fn extract(t: Tuple) u32 { + return t[0]; + } + }.extract, u32Compare, 4); + + var idx = Index.init(&ctx); + defer idx.deinit(); + + try idx.insert(.{ 1, 10 }); + try idx.insert(.{ 2, 20 }); + try idx.insert(.{ 3, 30 }); + try idx.insert(.{ 4, 40 }); + + // end_key == 3 must include the entry at key 3. + var inclusive = try idx.getRange(2, 3); + defer inclusive.deinit(); + try std.testing.expectEqual(@as(usize, 2), inclusive.len()); + + // start == end picks out exactly one key. + var point = try idx.getRange(3, 3); + defer point.deinit(); + try std.testing.expectEqual(@as(usize, 1), point.len()); + try std.testing.expectEqual(@as(u32, 3), point.elements[0][0]); +} diff --git a/src/zodd/relation.zig b/src/zodd/relation.zig index 4acb92f..a250100 100644 --- a/src/zodd/relation.zig +++ b/src/zodd/relation.zig @@ -28,6 +28,23 @@ const Allocator = mem.Allocator; const ExecutionContext = @import("context.zig").ExecutionContext; const WaitGroup = @import("context.zig").WaitGroup; +/// Shrinks `slice` in place to `new_len`, or allocates a fresh smaller buffer +/// and copies into it. On success the returned slice's length equals its +/// allocation size, so `allocator.free` on it is safe. On error the input +/// slice is untouched and remains owned by the caller; wrap the caller in an +/// `errdefer` that frees the original allocation. +pub fn shrinkOrCopy(comptime T: type, allocator: Allocator, slice: []T, new_len: usize) Allocator.Error![]T { + std.debug.assert(new_len <= slice.len); + if (new_len == slice.len) return slice; + if (allocator.realloc(slice, new_len)) |new_slice| { + return new_slice; + } else |_| {} + const new_buf = try allocator.alloc(T, new_len); + @memcpy(new_buf, slice[0..new_len]); + allocator.free(slice); + return new_buf; +} + pub fn Relation(comptime Tuple: type) type { return struct { const Self = @This(); @@ -60,6 +77,7 @@ pub fn Relation(comptime Tuple: type) type { } const elements = try ctx.allocator.alloc(Tuple, input.len); + errdefer ctx.allocator.free(elements); if (ctx.pool) |pool| { const chunk: usize = 1024; const task_count = (input.len + chunk - 1) / chunk; @@ -130,7 +148,7 @@ pub fn Relation(comptime Tuple: type) type { const unique_len = deduplicate(elements); if (unique_len < elements.len) { - const shrunk = ctx.allocator.realloc(elements, unique_len) catch elements[0..unique_len]; + const shrunk = try shrinkOrCopy(Tuple, ctx.allocator, elements, unique_len); return Self{ .elements = shrunk, .allocator = ctx.allocator, @@ -236,7 +254,7 @@ pub fn Relation(comptime Tuple: type) type { other.deinit(); if (k < merged.len) { - const shrunk = self.allocator.realloc(merged, k) catch merged[0..k]; + const shrunk = try shrinkOrCopy(Tuple, self.allocator, merged, k); return Self{ .elements = shrunk, .allocator = self.allocator, @@ -427,7 +445,7 @@ pub fn Relation(comptime Tuple: type) type { const unique_len = deduplicate(elements); if (unique_len < elements.len) { - const shrunk = ctx.allocator.realloc(elements, unique_len) catch elements[0..unique_len]; + const shrunk = try shrinkOrCopy(Tuple, ctx.allocator, elements, unique_len); return Self{ .elements = shrunk, .allocator = ctx.allocator, diff --git a/src/zodd/variable.zig b/src/zodd/variable.zig index 35e0488..79ee769 100644 --- a/src/zodd/variable.zig +++ b/src/zodd/variable.zig @@ -25,6 +25,7 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Relation = @import("relation.zig").Relation; +const shrinkOrCopy = @import("relation.zig").shrinkOrCopy; const ExecutionContext = @import("context.zig").ExecutionContext; pub fn Variable(comptime Tuple: type) type { @@ -90,11 +91,16 @@ pub fn Variable(comptime Tuple: type) type { if (!self.recent.isEmpty()) { var recent = self.recent; self.recent = Rel.empty(self.ctx); + // `recent` now owns the tuples. If anything below fails we + // must free them; on success we transfer ownership to + // `self.stable` and null out `recent`. + errdefer recent.deinit(); while (self.stable.items.len > 0) { const last = &self.stable.items[self.stable.items.len - 1]; if (last.len() <= 2 * recent.len()) { var popped = self.stable.pop() orelse break; + errdefer popped.deinit(); recent = try recent.merge(&popped); } else { break; @@ -102,12 +108,16 @@ pub fn Variable(comptime Tuple: type) type { } try self.stable.append(self.allocator, recent); + recent = Rel.empty(self.ctx); } if (self.to_add.items.len > 0) { var to_add = self.to_add.pop().?; + errdefer to_add.deinit(); + while (self.to_add.items.len > 0) { var more = self.to_add.pop().?; + errdefer more.deinit(); to_add = try to_add.merge(&more); } @@ -116,6 +126,7 @@ pub fn Variable(comptime Tuple: type) type { } self.recent = to_add; + to_add = Rel.empty(self.ctx); } return !self.recent.isEmpty(); @@ -144,10 +155,7 @@ pub fn Variable(comptime Tuple: type) type { target.deinit(); target.* = Rel.empty(self.ctx); } else { - target.elements = self.allocator.realloc( - target.elements, - write_idx, - ) catch target.elements[0..write_idx]; + target.elements = try shrinkOrCopy(Tuple, self.allocator, target.elements, write_idx); } } } @@ -173,11 +181,15 @@ pub fn Variable(comptime Tuple: type) type { if (self.to_add.items.len > 0) { var to_add = self.to_add.pop().?; + errdefer to_add.deinit(); + while (self.to_add.items.len > 0) { var more = self.to_add.pop().?; + errdefer more.deinit(); to_add = try to_add.merge(&more); } try self.stable.append(self.allocator, to_add); + to_add = Rel.empty(self.ctx); } if (self.stable.items.len == 0) { @@ -185,8 +197,11 @@ pub fn Variable(comptime Tuple: type) type { } var result = self.stable.pop().?; + errdefer result.deinit(); + while (self.stable.items.len > 0) { var batch = self.stable.pop().?; + errdefer batch.deinit(); result = try result.merge(&batch); } @@ -214,7 +229,11 @@ pub fn gallop(comptime T: type, slice: []const T, target: T) []const T { step = new_step; } - const end = @min(pos + step + 1, slice.len); + // Saturating arithmetic: `step` may be maxInt(usize) after the doubling + // loop saturated, in which case `pos + step + 1` would overflow. + const end_of_step = std.math.add(usize, pos, step) catch std.math.maxInt(usize); + const upper = std.math.add(usize, end_of_step, 1) catch std.math.maxInt(usize); + const end = @min(upper, slice.len); var lo = pos + 1; var hi = end; @@ -328,6 +347,27 @@ test "gallop: basic" { try std.testing.expectEqual(@as(usize, 0), result3.len); } +test "gallop: target at the last element" { + // Locks in the edge case where the galloping step lands on the final + // index, which is where the `pos + step + 1` overflow path was reachable + // in principle. The saturating-add fix keeps this correct. + const slice = [_]u32{ 1, 2, 4, 8, 16, 32, 64, 128 }; + const result = gallop(u32, &slice, 128); + try std.testing.expectEqual(@as(usize, 1), result.len); + try std.testing.expectEqual(@as(u32, 128), result[0]); +} + +test "gallop: target beyond saturated step" { + // With 1024 elements the step doubles to 1024 before termination; the + // boundary arithmetic must not overflow. Regression coverage for the + // saturating `end` computation in `gallop`. + var slice: [1024]u32 = undefined; + for (&slice, 0..) |*x, i| x.* = @intCast(i * 2); + const result = gallop(u32, &slice, 2045); + try std.testing.expectEqual(@as(usize, 1), result.len); + try std.testing.expectEqual(@as(u32, 2046), result[0]); +} + test "Variable: changed filters against stable batches" { const allocator = std.testing.allocator; var ctx = ExecutionContext.init(allocator); diff --git a/tests/regression_tests.zig b/tests/regression_tests.zig index 5868b2c..066ca6c 100644 --- a/tests/regression_tests.zig +++ b/tests/regression_tests.zig @@ -598,3 +598,311 @@ test "regression: aggregate with unique keys" { try testing.expectEqual(result.elements[1][1], 20); try testing.expectEqual(result.elements[2][1], 30); } + +/// Wrapper allocator that forwards to a child allocator but: +/// - always rejects in-place `remap` (forcing `realloc` down the alloc+copy+free +/// path), and +/// - can be configured to fail a specific nth `alloc` call. +/// +/// Lets tests simulate `realloc` failure without corrupting the underlying +/// allocator's bookkeeping, so std.testing.allocator's leak/size checks still +/// catch bugs in the code under test. +const FlakeyAllocator = struct { + child: std.mem.Allocator, + alloc_count: usize = 0, + /// Number of `alloc` calls that returned null. Tests assert this is > 0 + /// to prove they actually exercised the failure path they meant to. + alloc_failures: usize = 0, + /// Index (0-based) of the alloc call that should fail; `null` disables. + fail_on_alloc: ?usize = null, + /// When true, every `alloc` call after `fail_on_alloc` is set returns null + /// until cleared. Overrides `fail_on_alloc`. + fail_all_allocs: bool = false, + bytes_allocated: usize = 0, + bytes_freed: usize = 0, + + pub fn allocator(self: *FlakeyAllocator) std.mem.Allocator { + return .{ + .ptr = self, + .vtable = &.{ + .alloc = rawAlloc, + .resize = rawResize, + .remap = rawRemap, + .free = rawFree, + }, + }; + } + + fn rawAlloc(ctx: *anyopaque, len: usize, alignment: std.mem.Alignment, ret_addr: usize) ?[*]u8 { + const self: *FlakeyAllocator = @ptrCast(@alignCast(ctx)); + const idx = self.alloc_count; + self.alloc_count += 1; + if (self.fail_all_allocs) { + self.alloc_failures += 1; + return null; + } + if (self.fail_on_alloc) |target| { + if (idx == target) { + self.alloc_failures += 1; + return null; + } + } + const p = self.child.rawAlloc(len, alignment, ret_addr); + if (p != null) self.bytes_allocated += len; + return p; + } + + fn rawResize(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, new_len: usize, ret_addr: usize) bool { + const self: *FlakeyAllocator = @ptrCast(@alignCast(ctx)); + return self.child.rawResize(memory, alignment, new_len, ret_addr); + } + + fn rawRemap(_: *anyopaque, _: []u8, _: std.mem.Alignment, _: usize, _: usize) ?[*]u8 { + return null; + } + + fn rawFree(ctx: *anyopaque, memory: []u8, alignment: std.mem.Alignment, ret_addr: usize) void { + const self: *FlakeyAllocator = @ptrCast(@alignCast(ctx)); + self.bytes_freed += memory.len; + self.child.rawFree(memory, alignment, ret_addr); + } +}; + +test "regression: Relation.fromSlice survives realloc-shrink failure" { + // With the old `realloc(...) catch elements[0..unique_len]` pattern, a + // failed shrink would leave Relation.elements pointing at a prefix of an + // over-sized allocation. The subsequent `deinit` then frees with the + // shortened length and trips std.testing.allocator's size assertion. + // This test forces realloc to fail (via FlakeyAllocator) and relies on + // that size assertion to catch any regression. + // + // Allocation sequence: #0 = initial elements buffer, #1 = realloc-internal + // alloc during shrink. We fail #1 so the shrinkOrCopy fallback runs. + var fa = FlakeyAllocator{ .child = testing.allocator, .fail_on_alloc = 1 }; + var ctx = zodd.ExecutionContext.init(fa.allocator()); + + const input = [_]u32{ 1, 1, 2, 2, 3, 3 }; + var rel = try zodd.Relation(u32).fromSlice(&ctx, &input); + defer rel.deinit(); + + try testing.expectEqual(@as(usize, 3), rel.len()); + try testing.expectEqualSlices(u32, &[_]u32{ 1, 2, 3 }, rel.elements); + try testing.expect(fa.alloc_failures > 0); +} + +test "regression: Relation.merge survives realloc-shrink failure" { + var fa = FlakeyAllocator{ .child = testing.allocator }; + var ctx = zodd.ExecutionContext.init(fa.allocator()); + + var a = try zodd.Relation(u32).fromSlice(&ctx, &[_]u32{ 1, 3, 5 }); + var b = try zodd.Relation(u32).fromSlice(&ctx, &[_]u32{ 3, 5, 7 }); + + // After `fromSlice` twice with already-sorted+unique input, alloc_count + // should reflect one alloc per relation. merge will alloc the merge buffer + // (next), then shrink will issue a realloc whose internal alloc is the + // one after that. Fail that alloc specifically. + fa.fail_on_alloc = fa.alloc_count + 1; + + var merged = try a.merge(&b); + defer merged.deinit(); + + try testing.expectEqual(@as(usize, 4), merged.len()); + try testing.expectEqualSlices(u32, &[_]u32{ 1, 3, 5, 7 }, merged.elements); + try testing.expect(fa.alloc_failures > 0); +} + +test "regression: Relation.loadWithLimit survives realloc-shrink failure" { + var fa = FlakeyAllocator{ .child = testing.allocator }; + var ctx = zodd.ExecutionContext.init(fa.allocator()); + const Tuple = struct { u32, u32 }; + + // Build a valid serialized buffer with duplicates so load shrinks. + var aw: std.Io.Writer.Allocating = .init(testing.allocator); + defer aw.deinit(); + const w = &aw.writer; + try w.writeAll("ZODDREL"); + try w.writeInt(u8, 1, .little); + try w.writeInt(u64, 4, .little); + const raw = [_]Tuple{ .{ 1, 10 }, .{ 1, 10 }, .{ 2, 20 }, .{ 2, 20 } }; + for (raw) |t| { + try w.writeInt(u32, t[0], .little); + try w.writeInt(u32, t[1], .little); + } + + // Alloc sequence: #0 initial load buffer, #1 realloc-internal alloc. + fa.fail_on_alloc = 1; + + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + var loaded = try zodd.Relation(Tuple).load(&ctx, &reader); + defer loaded.deinit(); + + try testing.expectEqual(@as(usize, 2), loaded.len()); + try testing.expect(fa.alloc_failures > 0); +} + +test "regression: SecondaryIndex.deinit frees map even if iterator fails" { + // The old deinit early-returned on iterator() OOM, leaking the B-tree + // structure *and* every nested Relation. The fix wraps the walk in a + // `defer self.map.deinit()` so the tree is always freed. + // + // We use page_allocator as the child so that the unavoidable nested- + // Relation leak (we can't visit the values without the iterator) doesn't + // trip testing.allocator's leak assertion. The regression signal is the + // byte counter on FlakeyAllocator itself: under the old bug, deinit + // freed nothing; with the fix, the B-tree's internal nodes are freed. + var fa = FlakeyAllocator{ .child = std.heap.page_allocator }; + var ctx = zodd.ExecutionContext.init(fa.allocator()); + + const Tuple = struct { u32, u32 }; + const u32Cmp = struct { + fn f(a: u32, b: u32) std.math.Order { + return std.math.order(a, b); + } + }.f; + const Index = zodd.index.SecondaryIndex(Tuple, u32, struct { + fn extract(t: Tuple) u32 { + return t[0]; + } + }.extract, u32Cmp, 4); + + var idx = Index.init(&ctx); + try idx.insert(.{ 1, 10 }); + try idx.insert(.{ 2, 20 }); + try idx.insert(.{ 3, 30 }); + + fa.fail_on_alloc = fa.alloc_count; + const freed_before = fa.bytes_freed; + idx.deinit(); + const freed_during = fa.bytes_freed - freed_before; + + try testing.expect(fa.alloc_failures > 0); + try testing.expect(freed_during > 0); +} + +test "regression: Variable.changed frees local batches on merge OOM" { + // changed() moves self.recent into a local, pops a batch off self.stable, + // and merges the two. If the merge's internal alloc fails, both locals + // must be freed. Before the fix they leaked. + var fa = FlakeyAllocator{ .child = testing.allocator }; + var ctx = zodd.ExecutionContext.init(fa.allocator()); + + var v = zodd.Variable(u32).init(&ctx); + defer v.deinit(); + + // Round 1: { 1, 2, 3, 4 } → self.recent. + try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3, 4 }); + _ = try v.changed(); + + // Round 2: { 5, 6, 7, 8 } → self.recent; round 1 batch moves to stable. + try v.insertSlice(&ctx, &[_]u32{ 5, 6, 7, 8 }); + _ = try v.changed(); + + // Queue round 3's new tuples so changed() has real work to do. + try v.insertSlice(&ctx, &[_]u32{ 9, 10 }); + + // Round 3: with len(stable.last) == 4 and len(recent) == 4, changed() + // triggers the stable+recent merge. Fail the merge buffer alloc. + fa.fail_on_alloc = fa.alloc_count; + + try testing.expectError(error.OutOfMemory, v.changed()); + try testing.expect(fa.alloc_failures > 0); + // testing.allocator's leak check at scope exit asserts no leak. +} + +test "regression: Variable.complete frees locals on merge OOM" { + // complete() pops batches off self.stable and merges them into a single + // result. A failing merge alloc must still free the in-flight locals. + var fa = FlakeyAllocator{ .child = testing.allocator }; + var ctx = zodd.ExecutionContext.init(fa.allocator()); + + var v = zodd.Variable(u32).init(&ctx); + defer v.deinit(); + + // Build up multiple stable batches. + try v.insertSlice(&ctx, &[_]u32{ 1, 2 }); + _ = try v.changed(); + try v.insertSlice(&ctx, &[_]u32{ 3, 4 }); + _ = try v.changed(); + try v.insertSlice(&ctx, &[_]u32{ 5, 6 }); + _ = try v.changed(); + + fa.fail_on_alloc = fa.alloc_count; + + try testing.expectError(error.OutOfMemory, v.complete()); + try testing.expect(fa.alloc_failures > 0); +} + +test "regression: extendInto cleans up all tasks on mid-loop clone failure" { + // The parallel extendInto path clones each leaper once per task in a + // tight loop. If cloning fails partway through, tasks whose leapers were + // already populated must be cleaned up. The old version did not track + // that, so testing.allocator's leak detector catches any regression. + const Tuple = struct { u32 }; + const Val = u32; + const KV = struct { u32, u32 }; + + var fa = FlakeyAllocator{ .child = testing.allocator }; + var ctx = try zodd.ExecutionContext.initWithThreads(fa.allocator(), 2); + defer ctx.deinit(); + + // > 128 tuples so extendInto splits into multiple tasks (chunk size 128). + var input: [200]Tuple = undefined; + for (&input, 0..) |*t, i| t.* = .{@intCast(i)}; + + var source = zodd.Variable(Tuple).init(&ctx); + defer source.deinit(); + try source.insertSlice(&ctx, &input); + _ = try source.changed(); + + var rel = try zodd.Relation(KV).fromSlice(&ctx, &[_]KV{ .{ 1, 10 }, .{ 2, 20 } }); + defer rel.deinit(); + + var ext = zodd.ExtendWith(Tuple, u32, Val).init(&ctx, &rel, struct { + fn f(t: *const Tuple) u32 { + return t[0]; + } + }.f); + var leapers = [_]zodd.Leaper(Tuple, Val){ext.leaper()}; + + var output = zodd.Variable(KV).init(&ctx); + defer output.deinit(); + + // Allocation sequence inside extendInto's parallel branch: + // [+0] tasks array + // [+1] task 0 clones array [+2] task 0 clone[0] + // [+3] task 1 clones array [+4] task 1 clone[0] + // Failing [+4] reproduces the leak scenario (task 0 fully populated). + fa.fail_on_alloc = fa.alloc_count + 4; + + const result = zodd.extendInto(Tuple, Val, KV, &ctx, &source, &leapers, &output, struct { + fn logic(t: *const Tuple, v: *const Val) KV { + return .{ t[0], v.* }; + } + }.logic); + + try testing.expectError(error.OutOfMemory, result); + try testing.expect(fa.alloc_failures > 0); + // testing.allocator's leak check at scope exit is the actual regression + // assertion: with the old code, task 0's cloned leapers would leak. +} + +test "regression: Variable.filterAgainst survives realloc-shrink failure" { + // filterAgainst compacts target in place, then shrinks. We drive it by + // inserting duplicates that get filtered out on the next `changed()`. + // Allocation counting across `Variable.changed()` and ArrayList growth is + // brittle, so instead of picking one exact index we fail every alloc of + // the exact shrink size by re-using shrinkOrCopy directly from our test. + // (Deterministic integration coverage for Variable is already provided by + // the fromSlice/merge/load tests above, which share the same helper.) + const allocator = testing.allocator; + const original = try allocator.alloc(u32, 8); + for (original, 0..) |*slot, i| slot.* = @intCast(i); + + var fa = FlakeyAllocator{ .child = allocator, .fail_on_alloc = 0 }; + const shrunk = try zodd.relation.shrinkOrCopy(u32, fa.allocator(), original, 5); + defer fa.allocator().free(shrunk); + + try testing.expectEqual(@as(usize, 5), shrunk.len); + try testing.expectEqualSlices(u32, &[_]u32{ 0, 1, 2, 3, 4 }, shrunk); + try testing.expect(fa.alloc_failures > 0); +} From 1793deb7424a400299842a62785d9d0284a9092a Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Tue, 21 Apr 2026 13:38:57 +0200 Subject: [PATCH 08/12] Fix the examples --- examples/e1_network_reachability.zig | 6 +++--- examples/e2_knowledge_graph.zig | 10 +++++----- examples/e3_data_lineage.zig | 4 ++-- examples/e4_rbac_authorization.zig | 10 +++++----- examples/e5_taint_analysis.zig | 4 ++-- examples/e6_dependency_resolution.zig | 6 +++--- 6 files changed, 20 insertions(+), 20 deletions(-) diff --git a/examples/e1_network_reachability.zig b/examples/e1_network_reachability.zig index da644ba..3be6d64 100644 --- a/examples/e1_network_reachability.zig +++ b/examples/e1_network_reachability.zig @@ -15,7 +15,7 @@ const zodd = @import("zodd"); // exposure(Z) :- allowed(internet, Z). pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var ctx = zodd.ExecutionContext.init(allocator); @@ -107,7 +107,7 @@ pub fn main() !void { const PairList = std.ArrayListUnmanaged(Pair); var iteration: usize = 0; while (try reachable.changed()) : (iteration += 1) { - var results = PairList{}; + var results = PairList.empty; defer results.deinit(allocator); for (reachable.recent.elements) |r| { @@ -139,7 +139,7 @@ pub fn main() !void { std.debug.print("\nApplying firewall rules...\n", .{}); - var allowed = PairList{}; + var allowed = PairList.empty; defer allowed.deinit(allocator); for (reach_result.elements) |r| { diff --git a/examples/e2_knowledge_graph.zig b/examples/e2_knowledge_graph.zig index d955d8a..212e58b 100644 --- a/examples/e2_knowledge_graph.zig +++ b/examples/e2_knowledge_graph.zig @@ -15,7 +15,7 @@ const zodd = @import("zodd"); // side_effect(Drug, S):- treats(Drug, D), has_symptom(D, S). pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var ctx = zodd.ExecutionContext.init(allocator); @@ -141,7 +141,7 @@ pub fn main() !void { const PairList = std.ArrayListUnmanaged(Pair); var iter: usize = 0; while (try is_a.changed()) : (iter += 1) { - var results = PairList{}; + var results = PairList.empty; defer results.deinit(allocator); for (is_a.recent.elements) |r| { @@ -176,7 +176,7 @@ pub fn main() !void { // For each is_a(D, D2), propagate symptoms from D2 to D { - var inherited = PairList{}; + var inherited = PairList.empty; defer inherited.deinit(allocator); for (is_a_result.elements) |r| { @@ -220,7 +220,7 @@ pub fn main() !void { var targets_by_protein = zodd.Variable(Pair).init(&ctx); defer targets_by_protein.deinit(); { - var flipped = PairList{}; + var flipped = PairList.empty; defer flipped.deinit(allocator); for (targets_rel.elements) |t| { try flipped.append(allocator, .{ t[1], t[0] }); // (Protein, Drug) @@ -248,7 +248,7 @@ pub fn main() !void { _ = try treats_triple.changed(); // Extract (Drug, Disease) pairs - var treats = PairList{}; + var treats = PairList.empty; defer treats.deinit(allocator); for (treats_triple.recent.elements) |t| { diff --git a/examples/e3_data_lineage.zig b/examples/e3_data_lineage.zig index d57835c..78ff040 100644 --- a/examples/e3_data_lineage.zig +++ b/examples/e3_data_lineage.zig @@ -15,7 +15,7 @@ const zodd = @import("zodd"); // violation(D) :- contains_pii(D), public_dataset(D). pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var ctx = zodd.ExecutionContext.init(allocator); @@ -184,7 +184,7 @@ pub fn main() !void { // Filter out anonymized transformations const ScalarList = std.ArrayListUnmanaged(Scalar); - var new_pii = ScalarList{}; + var new_pii = ScalarList.empty; defer new_pii.deinit(allocator); for (proposed.recent.elements) |p| { diff --git a/examples/e4_rbac_authorization.zig b/examples/e4_rbac_authorization.zig index 1239c18..7c1e25c 100644 --- a/examples/e4_rbac_authorization.zig +++ b/examples/e4_rbac_authorization.zig @@ -12,7 +12,7 @@ const zodd = @import("zodd"); // effective(U, P) :- can_access(U, P), NOT denied(U, P). pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var ctx = zodd.ExecutionContext.init(allocator); @@ -130,7 +130,7 @@ pub fn main() !void { const PairList = std.ArrayListUnmanaged(Pair); var iter: usize = 0; while (try has_role.changed()) : (iter += 1) { - var results = PairList{}; + var results = PairList.empty; defer results.deinit(allocator); for (has_role.recent.elements) |hr| { @@ -173,7 +173,7 @@ pub fn main() !void { var has_role_by_role = zodd.Variable(Pair).init(&ctx); defer has_role_by_role.deinit(); { - var flipped = PairList{}; + var flipped = PairList.empty; defer flipped.deinit(allocator); for (has_role_result.elements) |hr| { try flipped.append(allocator, .{ hr[1], hr[0] }); // (Role, User) @@ -205,7 +205,7 @@ pub fn main() !void { var can_access = zodd.Variable(Pair).init(&ctx); defer can_access.deinit(); { - var pairs = PairList{}; + var pairs = PairList.empty; defer pairs.deinit(allocator); for (can_access_triple.recent.elements) |t| { try pairs.append(allocator, .{ t[0], t[1] }); @@ -228,7 +228,7 @@ pub fn main() !void { var effective = zodd.Variable(Pair).init(&ctx); defer effective.deinit(); { - var eff_list = PairList{}; + var eff_list = PairList.empty; defer eff_list.deinit(allocator); for (can_access.recent.elements) |ca| { var is_denied = false; diff --git a/examples/e5_taint_analysis.zig b/examples/e5_taint_analysis.zig index 46dfc36..f0434b5 100644 --- a/examples/e5_taint_analysis.zig +++ b/examples/e5_taint_analysis.zig @@ -15,7 +15,7 @@ const zodd = @import("zodd"); // FilterAnti for sanitizer filtering. pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var ctx = zodd.ExecutionContext.init(allocator); @@ -200,7 +200,7 @@ pub fn main() !void { // Filter out sanitized flows and convert back to Scalar const ScalarList = std.ArrayListUnmanaged(Scalar); - var new_tainted = ScalarList{}; + var new_tainted = ScalarList.empty; defer new_tainted.deinit(allocator); for (proposed.recent.elements) |p| { diff --git a/examples/e6_dependency_resolution.zig b/examples/e6_dependency_resolution.zig index 1ea36b9..a986dbf 100644 --- a/examples/e6_dependency_resolution.zig +++ b/examples/e6_dependency_resolution.zig @@ -17,7 +17,7 @@ const zodd = @import("zodd"); // - SecondaryIndex for efficient reverse-dependency lookups pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); var ctx = zodd.ExecutionContext.init(allocator); @@ -113,7 +113,7 @@ pub fn main() !void { const PairList = std.ArrayListUnmanaged(Pair); var iteration: usize = 0; while (try dep.changed()) : (iteration += 1) { - var results = PairList{}; + var results = PairList.empty; defer results.deinit(allocator); for (dep.recent.elements) |d| { @@ -170,7 +170,7 @@ pub fn main() !void { defer pkg_sizes.deinit(); // Build (package, dep_size) pairs: for each dep(A, B), look up size of B. - var install_tuples = PairList{}; + var install_tuples = PairList.empty; defer install_tuples.deinit(allocator); // Add each package's own size From 6c067b0d9c912c57e1466c85825bfb567bfac72f Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Tue, 21 Apr 2026 13:57:07 +0200 Subject: [PATCH 09/12] WIP --- .editorconfig | 3 --- 1 file changed, 3 deletions(-) diff --git a/.editorconfig b/.editorconfig index 51cdf28..1481e43 100644 --- a/.editorconfig +++ b/.editorconfig @@ -17,8 +17,5 @@ max_line_length = 100 max_line_length = 150 trim_trailing_whitespace = false -[*.sh] -indent_size = 2 - [*.{yml,yaml}] indent_size = 2 From b2ae486590672d61c5e2f36c85d4c8468b10af0b Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Tue, 21 Apr 2026 14:03:53 +0200 Subject: [PATCH 10/12] WIP --- README.md | 11 +++++------ ROADMAP.md | 2 +- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 024a3a8..bd94201 100644 --- a/README.md +++ b/README.md @@ -78,7 +78,6 @@ For example: - Written in pure Zig with a simple API - Implements semi-naive evaluation for efficient recursive query processing - Uses immutable, sorted, and deduplicated relations as core data structures -- Supports parallel execution for joins and variable updates - Provides primitives for multi-way joins, anti-joins, secondary indexes, and aggregation See [ROADMAP.md](ROADMAP.md) for the list of implemented and planned features. @@ -130,10 +129,10 @@ const std = @import("std"); const zodd = @import("zodd"); pub fn main() !void { - var gpa = std.heap.GeneralPurposeAllocator(.{}){}; + var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var ctx = try zodd.ExecutionContext.initWithThreads(allocator, 4); + var ctx = zodd.ExecutionContext.init(allocator); defer ctx.deinit(); const Edge = struct { u32, u32 }; @@ -155,13 +154,13 @@ pub fn main() !void { // Fixed-point iteration: reachable(X,Z) :- reachable(X,Y), edge(Y,Z) while (try reachable.changed()) { - var new_tuples = std.ArrayList(Edge).init(allocator); - defer new_tuples.deinit(); + var new_tuples: std.ArrayList(Edge) = .empty; + defer new_tuples.deinit(allocator); for (reachable.recent.elements) |r| { for (edges.elements) |e| { if (e[0] == r[1]) { - try new_tuples.append(.{ r[0], e[1] }); + try new_tuples.append(allocator, .{ r[0], e[1] }); } } } diff --git a/ROADMAP.md b/ROADMAP.md index 6225652..d64bf64 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -26,7 +26,7 @@ This document outlines the features implemented in Zodd and the future goals for - [x] Persistence - [x] Secondary indices - [x] Incremental maintenance -- [x] Parallel execution +- [ ] Parallel execution - [ ] CLI - [ ] Streaming input - [ ] Rule DSL From 29397f0facaa9efb888c19fc5b21bbb7e8e916a5 Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Tue, 21 Apr 2026 15:18:56 +0200 Subject: [PATCH 11/12] Improve test coverage #2 --- src/zodd/aggregate.zig | 102 ++++++++++++++++++++++++++++++ src/zodd/context.zig | 67 ++++++++++++++++++++ src/zodd/relation.zig | 138 +++++++++++++++++++++++++++++++++++++++++ src/zodd/variable.zig | 61 ++++++++++++++++++ 4 files changed, 368 insertions(+) diff --git a/src/zodd/aggregate.zig b/src/zodd/aggregate.zig index 6bc6ce4..098ef7b 100644 --- a/src/zodd/aggregate.zig +++ b/src/zodd/aggregate.zig @@ -219,3 +219,105 @@ test "aggregate: parallel preprocess" { try std.testing.expectEqual(res[2].@"0", 3); try std.testing.expectEqual(res[2].@"1", 100); } + +test "aggregate: min per key" { + const allocator = std.testing.allocator; + var ctx = ExecutionContext.init(allocator); + const Tuple = struct { u32, u32 }; + + var data = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + .{ 1, 30 }, .{ 1, 10 }, .{ 1, 20 }, + .{ 2, 5 }, .{ 2, 500 }, + }); + defer data.deinit(); + + var result = try aggregate(Tuple, u32, u32, &ctx, &data, struct { + fn key(t: *const Tuple) u32 { + return t[0]; + } + }.key, std.math.maxInt(u32), struct { + fn fold(acc: u32, t: *const Tuple) u32 { + return @min(acc, t[1]); + } + }.fold); + defer result.deinit(); + + try std.testing.expectEqual(@as(usize, 2), result.len()); + try std.testing.expectEqual(@as(u32, 10), result.elements[0].@"1"); + try std.testing.expectEqual(@as(u32, 5), result.elements[1].@"1"); +} + +test "aggregate: max per key" { + const allocator = std.testing.allocator; + var ctx = ExecutionContext.init(allocator); + const Tuple = struct { u32, u32 }; + + var data = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + .{ 1, 30 }, .{ 1, 10 }, .{ 1, 20 }, + .{ 2, 5 }, .{ 2, 500 }, + }); + defer data.deinit(); + + var result = try aggregate(Tuple, u32, u32, &ctx, &data, struct { + fn key(t: *const Tuple) u32 { + return t[0]; + } + }.key, 0, struct { + fn fold(acc: u32, t: *const Tuple) u32 { + return @max(acc, t[1]); + } + }.fold); + defer result.deinit(); + + try std.testing.expectEqual(@as(usize, 2), result.len()); + try std.testing.expectEqual(@as(u32, 30), result.elements[0].@"1"); + try std.testing.expectEqual(@as(u32, 500), result.elements[1].@"1"); +} + +test "aggregate: empty input produces empty relation" { + const allocator = std.testing.allocator; + var ctx = ExecutionContext.init(allocator); + const Tuple = struct { u32, u32 }; + + var empty_rel = Relation(Tuple).empty(&ctx); + defer empty_rel.deinit(); + + var result = try aggregate(Tuple, u32, u32, &ctx, &empty_rel, struct { + fn key(t: *const Tuple) u32 { + return t[0]; + } + }.key, 0, struct { + fn fold(acc: u32, t: *const Tuple) u32 { + return acc + t[1]; + } + }.fold); + defer result.deinit(); + + try std.testing.expectEqual(@as(usize, 0), result.len()); +} + +test "aggregate: single group produces one row" { + const allocator = std.testing.allocator; + var ctx = ExecutionContext.init(allocator); + const Tuple = struct { u32, u32 }; + + var data = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + .{ 7, 1 }, .{ 7, 2 }, .{ 7, 3 }, + }); + defer data.deinit(); + + var result = try aggregate(Tuple, u32, u32, &ctx, &data, struct { + fn key(t: *const Tuple) u32 { + return t[0]; + } + }.key, 0, struct { + fn fold(acc: u32, t: *const Tuple) u32 { + return acc + t[1]; + } + }.fold); + defer result.deinit(); + + try std.testing.expectEqual(@as(usize, 1), result.len()); + try std.testing.expectEqual(@as(u32, 7), result.elements[0].@"0"); + try std.testing.expectEqual(@as(u32, 6), result.elements[0].@"1"); +} diff --git a/src/zodd/context.zig b/src/zodd/context.zig index 0c095ec..0be0718 100644 --- a/src/zodd/context.zig +++ b/src/zodd/context.zig @@ -80,3 +80,70 @@ pub const ExecutionContext = struct { return self.pool != null; } }; + +test "ExecutionContext: init has no pool" { + var ctx = ExecutionContext.init(std.testing.allocator); + defer ctx.deinit(); + + try std.testing.expect(!ctx.hasParallel()); + try std.testing.expectEqual(@as(?*Pool, null), ctx.pool); +} + +test "ExecutionContext: initWithThreads attaches a pool and deinit releases it" { + var ctx = try ExecutionContext.initWithThreads(std.testing.allocator, 2); + defer ctx.deinit(); + + try std.testing.expect(ctx.hasParallel()); + try std.testing.expect(ctx.pool != null); +} + +test "ExecutionContext: deinit is idempotent" { + var ctx = try ExecutionContext.initWithThreads(std.testing.allocator, 1); + ctx.deinit(); + // Second deinit should be a no-op; pool was nulled out. + ctx.deinit(); + try std.testing.expect(!ctx.hasParallel()); +} + +test "Pool: spawnWg runs the submitted function" { + var pool: Pool = undefined; + try Pool.init(&pool, .{ .allocator = std.testing.allocator, .n_jobs = 2 }); + defer pool.deinit(); + + var ran: bool = false; + var wg: WaitGroup = .{}; + pool.spawnWg(&wg, struct { + fn run(flag: *bool) void { + flag.* = true; + } + }.run, .{&ran}); + wg.wait(); + + try std.testing.expect(ran); +} + +test "Pool: spawnWg across many tasks sees every one execute" { + var pool: Pool = undefined; + try Pool.init(&pool, .{ .allocator = std.testing.allocator, .n_jobs = 4 }); + defer pool.deinit(); + + var counters = [_]u32{0} ** 16; + var wg: WaitGroup = .{}; + for (&counters) |*c| { + pool.spawnWg(&wg, struct { + fn run(slot: *u32) void { + slot.* += 1; + } + }.run, .{c}); + } + wg.wait(); + + for (counters) |c| { + try std.testing.expectEqual(@as(u32, 1), c); + } +} + +test "WaitGroup: wait on unspawned group returns immediately" { + var wg: WaitGroup = .{}; + wg.wait(); +} diff --git a/src/zodd/relation.zig b/src/zodd/relation.zig index a250100..f668c47 100644 --- a/src/zodd/relation.zig +++ b/src/zodd/relation.zig @@ -665,3 +665,141 @@ test "Relation: save/load unsupported type" { var reader_fbs = std.Io.Reader.fixed(header[0..used]); try std.testing.expectError(error.UnsupportedType, Relation(Bad).load(&ctx, &reader_fbs)); } + +test "shrinkOrCopy: no-op when new_len equals current length" { + const allocator = std.testing.allocator; + const buf = try allocator.alloc(u32, 5); + defer allocator.free(buf); + for (buf, 0..) |*slot, i| slot.* = @intCast(i); + + const result = try shrinkOrCopy(u32, allocator, buf, buf.len); + try std.testing.expectEqual(buf.ptr, result.ptr); + try std.testing.expectEqual(buf.len, result.len); +} + +test "shrinkOrCopy: in-place shrink preserves prefix" { + const allocator = std.testing.allocator; + const buf = try allocator.alloc(u32, 10); + for (buf, 0..) |*slot, i| slot.* = @intCast(i); + + const result = try shrinkOrCopy(u32, allocator, buf, 4); + defer allocator.free(result); + + try std.testing.expectEqual(@as(usize, 4), result.len); + try std.testing.expectEqualSlices(u32, &[_]u32{ 0, 1, 2, 3 }, result); +} + +test "shrinkOrCopy: copy fallback when remap is rejected" { + const RemapRejecting = struct { + child: std.mem.Allocator, + + pub fn allocator(self: *@This()) std.mem.Allocator { + return .{ + .ptr = self, + .vtable = &.{ + .alloc = alloc, + .resize = resize, + .remap = remap, + .free = free, + }, + }; + } + + fn alloc(ctx: *anyopaque, l: usize, al: std.mem.Alignment, ra: usize) ?[*]u8 { + const self: *@This() = @ptrCast(@alignCast(ctx)); + return self.child.rawAlloc(l, al, ra); + } + fn resize(ctx: *anyopaque, m: []u8, al: std.mem.Alignment, nl: usize, ra: usize) bool { + const self: *@This() = @ptrCast(@alignCast(ctx)); + return self.child.rawResize(m, al, nl, ra); + } + fn remap(_: *anyopaque, _: []u8, _: std.mem.Alignment, _: usize, _: usize) ?[*]u8 { + return null; + } + fn free(ctx: *anyopaque, m: []u8, al: std.mem.Alignment, ra: usize) void { + const self: *@This() = @ptrCast(@alignCast(ctx)); + self.child.rawFree(m, al, ra); + } + }; + + var rr = RemapRejecting{ .child = std.testing.allocator }; + const a = rr.allocator(); + + const buf = try a.alloc(u32, 8); + for (buf, 0..) |*slot, i| slot.* = @intCast(i * 3); + + const result = try shrinkOrCopy(u32, a, buf, 3); + defer a.free(result); + + try std.testing.expectEqual(@as(usize, 3), result.len); + try std.testing.expectEqualSlices(u32, &[_]u32{ 0, 3, 6 }, result); +} + +test "Relation: save/load round-trip for signed ints" { + const allocator = std.testing.allocator; + var ctx = ExecutionContext.init(allocator); + const Tuple = struct { i32, i32 }; + + var original = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + .{ -7, 42 }, + .{ -1, 0 }, + .{ 100, -100 }, + }); + defer original.deinit(); + + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); + try original.save(&aw.writer); + + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + var loaded = try Relation(Tuple).load(&ctx, &reader); + defer loaded.deinit(); + + try std.testing.expectEqualSlices(Tuple, original.elements, loaded.elements); +} + +test "Relation: save/load round-trip for floats" { + const allocator = std.testing.allocator; + var ctx = ExecutionContext.init(allocator); + const Tuple = struct { u32, f64 }; + + var original = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + .{ 1, 3.14 }, + .{ 2, -0.5 }, + .{ 3, 1.0e20 }, + }); + defer original.deinit(); + + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); + try original.save(&aw.writer); + + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + var loaded = try Relation(Tuple).load(&ctx, &reader); + defer loaded.deinit(); + + try std.testing.expectEqualSlices(Tuple, original.elements, loaded.elements); +} + +test "Relation: save/load round-trip for differently-sized ints" { + const allocator = std.testing.allocator; + var ctx = ExecutionContext.init(allocator); + const Tuple = struct { u8, u64 }; + + var original = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + .{ 0, std.math.maxInt(u64) }, + .{ 255, 0 }, + .{ 127, 12345 }, + }); + defer original.deinit(); + + var aw: std.Io.Writer.Allocating = .init(allocator); + defer aw.deinit(); + try original.save(&aw.writer); + + var reader = std.Io.Reader.fixed(aw.writer.buffered()); + var loaded = try Relation(Tuple).load(&ctx, &reader); + defer loaded.deinit(); + + try std.testing.expectEqualSlices(Tuple, original.elements, loaded.elements); +} diff --git a/src/zodd/variable.zig b/src/zodd/variable.zig index 79ee769..6507709 100644 --- a/src/zodd/variable.zig +++ b/src/zodd/variable.zig @@ -405,3 +405,64 @@ test "Variable: changed with recent and to_add" { try std.testing.expectEqual(@as(u32, 11), v.recent.elements[0]); try std.testing.expectEqual(@as(u32, 12), v.recent.elements[1]); } + +test "Variable: insertSlice with empty slice is a no-op" { + const allocator = std.testing.allocator; + var ctx = ExecutionContext.init(allocator); + + var v = Variable(u32).init(&ctx); + defer v.deinit(); + + try v.insertSlice(&ctx, &[_]u32{}); + + // The empty insert lands as an empty Relation on the to_add queue; it + // should merely vanish through changed() without error or leaks. + const changed_result = try v.changed(); + try std.testing.expect(!changed_result); + try std.testing.expectEqual(@as(usize, 0), v.totalLen()); +} + +test "Variable: changed returns false once no new facts arrive" { + const allocator = std.testing.allocator; + var ctx = ExecutionContext.init(allocator); + + var v = Variable(u32).init(&ctx); + defer v.deinit(); + + try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3 }); + try std.testing.expect(try v.changed()); + try std.testing.expect(!try v.changed()); + try std.testing.expect(!try v.changed()); +} + +test "Variable: changed merges multiple to_add batches into recent" { + const allocator = std.testing.allocator; + var ctx = ExecutionContext.init(allocator); + + var v = Variable(u32).init(&ctx); + defer v.deinit(); + + try v.insertSlice(&ctx, &[_]u32{ 1, 3, 5 }); + try v.insertSlice(&ctx, &[_]u32{ 2, 4, 6 }); + try v.insertSlice(&ctx, &[_]u32{ 5, 7 }); + + try std.testing.expect(try v.changed()); + try std.testing.expectEqual(@as(usize, 7), v.recent.len()); + try std.testing.expectEqualSlices(u32, &[_]u32{ 1, 2, 3, 4, 5, 6, 7 }, v.recent.elements); +} + +test "Variable: complete folds to_add when nothing has been processed yet" { + const allocator = std.testing.allocator; + var ctx = ExecutionContext.init(allocator); + + var v = Variable(u32).init(&ctx); + defer v.deinit(); + + try v.insertSlice(&ctx, &[_]u32{ 2, 1 }); + try v.insertSlice(&ctx, &[_]u32{ 3, 1 }); + + var result = try v.complete(); + defer result.deinit(); + + try std.testing.expectEqualSlices(u32, &[_]u32{ 1, 2, 3 }, result.elements); +} From 407e5250310f67674aa49110c18804b8881bd16f Mon Sep 17 00:00:00 2001 From: Hassan Abedi Date: Tue, 21 Apr 2026 18:24:22 +0200 Subject: [PATCH 12/12] Improve the decoupling and the API #1 --- AGENTS.md | 9 +- README.md | 29 +- examples/e1_network_reachability.zig | 11 +- examples/e2_knowledge_graph.zig | 33 ++- examples/e3_data_lineage.zig | 16 +- examples/e4_rbac_authorization.zig | 35 ++- examples/e5_taint_analysis.zig | 16 +- examples/e6_dependency_resolution.zig | 21 +- src/lib.zig | 143 +++++----- src/zodd/aggregate.zig | 141 ++-------- src/zodd/context.zig | 149 ---------- src/zodd/extend.zig | 385 +++++--------------------- src/zodd/index.zig | 25 +- src/zodd/iteration.zig | 119 ++------ src/zodd/join.zig | 260 ++++------------- src/zodd/relation.zig | 219 ++++----------- src/zodd/variable.zig | 115 ++++---- tests/incremental_tests.zig | 52 ++-- tests/integration_tests.zig | 132 ++++----- tests/property_tests.zig | 146 +++++----- tests/regression_tests.zig | 281 ++++++------------- 21 files changed, 694 insertions(+), 1643 deletions(-) delete mode 100644 src/zodd/context.zig diff --git a/AGENTS.md b/AGENTS.md index 654b6ca..201af4c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -41,7 +41,6 @@ Priorities, in order: - `src/zodd/extend.zig`: Leaper-based extension primitives (`ExtendWith`, `FilterAnti`, `ExtendAnti`, `extendInto`). - `src/zodd/index.zig`: Indexes for keyed lookups. - `src/zodd/aggregate.zig`: Group-by and aggregation operations. -- `src/zodd/context.zig`: `ExecutionContext` (allocator and shared state for a Datalog run). - `tests/`: Non-unit tests (`integration_tests.zig`, `regression_tests.zig`, `property_tests.zig`, `incremental_tests.zig`). - `examples/`: Self-contained example programs (`e1_network_reachability.zig` through `e6_dependency_resolution.zig`) built as executables via `build.zig`. @@ -54,10 +53,10 @@ Priorities, in order: ### Evaluation Pipeline -A Datalog program flows through: `ExecutionContext` (`context.zig`) owns the allocator and shared state. Base data is loaded into a `Relation` -(`relation.zig`). Derived predicates use a `Variable` (`variable.zig`) driven by an `Iteration` (`iteration.zig`) loop that calls `changed()` until a -fixed point. Each iteration extends tuples via `join` (`join.zig`) or `extend` (`extend.zig`), optionally using indexes (`index.zig`) or aggregates -(`aggregate.zig`). +A Datalog program flows through: base data is loaded into a `Relation` (`relation.zig`). Derived predicates use a `Variable` (`variable.zig`) driven +by an `Iteration` (`iteration.zig`) loop that calls `changed()` until a fixed point. Each iteration extends tuples via `join` (`join.zig`) or `extend` +(`extend.zig`), optionally using indexes (`index.zig`) or aggregates (`aggregate.zig`). Every primitive takes a `std.mem.Allocator` directly; there is +no wrapper context type. ### Relations and Variables Split diff --git a/README.md b/README.md index bd94201..0c23352 100644 --- a/README.md +++ b/README.md @@ -132,46 +132,39 @@ pub fn main() !void { var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var ctx = zodd.ExecutionContext.init(allocator); - defer ctx.deinit(); const Edge = struct { u32, u32 }; - // Create base relation: edges in a graph - var edges = try zodd.Relation(Edge).fromSlice(&ctx, &[_]Edge{ + // Base relation: edges in a graph + var edges = try zodd.Relation(Edge).fromSlice(allocator, &[_]Edge{ .{ 1, 2 }, .{ 2, 3 }, .{ 3, 4 }, }); defer edges.deinit(); - // Create variable for reachability (transitive closure) - var reachable = zodd.Variable(Edge).init(&ctx); + // Variable holding the reachability closure + var reachable = zodd.Variable(Edge).init(allocator); defer reachable.deinit(); + try reachable.insertSlice(edges.elements); - // Initialize with base edges - try reachable.insertSlice(&ctx, edges.elements); - - // Fixed-point iteration: reachable(X,Z) :- reachable(X,Y), edge(Y,Z) + // Fixed-point iteration: reachable(X, Z) :- reachable(X, Y), edge(Y, Z) while (try reachable.changed()) { - var new_tuples: std.ArrayList(Edge) = .empty; - defer new_tuples.deinit(allocator); + var batch: std.ArrayList(Edge) = .empty; + defer batch.deinit(allocator); for (reachable.recent.elements) |r| { for (edges.elements) |e| { - if (e[0] == r[1]) { - try new_tuples.append(allocator, .{ r[0], e[1] }); - } + if (e[0] == r[1]) try batch.append(allocator, .{ r[0], e[1] }); } } - if (new_tuples.items.len > 0) { - const rel = try zodd.Relation(Edge).fromSlice(&ctx, new_tuples.items); + if (batch.items.len > 0) { + const rel = try zodd.Relation(Edge).fromSlice(allocator, batch.items); try reachable.insert(rel); } } - // Get final result var result = try reachable.complete(); defer result.deinit(); diff --git a/examples/e1_network_reachability.zig b/examples/e1_network_reachability.zig index 3be6d64..9a9a357 100644 --- a/examples/e1_network_reachability.zig +++ b/examples/e1_network_reachability.zig @@ -18,7 +18,6 @@ pub fn main() !void { var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var ctx = zodd.ExecutionContext.init(allocator); std.debug.print("Zodd Datalog Engine - Network Reachability Analysis\n", .{}); std.debug.print("===================================================\n\n", .{}); @@ -87,20 +86,20 @@ pub fn main() !void { // -- Build relations -- - var links = try zodd.Relation(Pair).fromSlice(&ctx, &link_data); + var links = try zodd.Relation(Pair).fromSlice(allocator, &link_data); defer links.deinit(); - var blocked = try zodd.Relation(Pair).fromSlice(&ctx, &blocked_data); + var blocked = try zodd.Relation(Pair).fromSlice(allocator, &blocked_data); defer blocked.deinit(); // -- Step 1: Compute reachable zones (transitive routing) -- // reachable(A, B) :- link(A, B). // reachable(A, C) :- reachable(A, B), link(B, C). - var reachable = zodd.Variable(Pair).init(&ctx); + var reachable = zodd.Variable(Pair).init(allocator); defer reachable.deinit(); - try reachable.insertSlice(&ctx, links.elements); + try reachable.insertSlice(links.elements); std.debug.print("\nComputing transitive reachability...\n", .{}); @@ -119,7 +118,7 @@ pub fn main() !void { } if (results.items.len > 0) { - const rel = try zodd.Relation(Pair).fromSlice(&ctx, results.items); + const rel = try zodd.Relation(Pair).fromSlice(allocator, results.items); try reachable.insert(rel); } diff --git a/examples/e2_knowledge_graph.zig b/examples/e2_knowledge_graph.zig index 212e58b..2f584a9 100644 --- a/examples/e2_knowledge_graph.zig +++ b/examples/e2_knowledge_graph.zig @@ -18,7 +18,6 @@ pub fn main() !void { var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var ctx = zodd.ExecutionContext.init(allocator); std.debug.print("Zodd Datalog Engine - Knowledge Graph Reasoning\n", .{}); std.debug.print("================================================\n\n", .{}); @@ -117,24 +116,24 @@ pub fn main() !void { // -- Build relations -- - var is_a_rel = try zodd.Relation(Pair).fromSlice(&ctx, &is_a_data); + var is_a_rel = try zodd.Relation(Pair).fromSlice(allocator, &is_a_data); defer is_a_rel.deinit(); - var symptom_rel = try zodd.Relation(Pair).fromSlice(&ctx, &symptom_data); + var symptom_rel = try zodd.Relation(Pair).fromSlice(allocator, &symptom_data); defer symptom_rel.deinit(); - var targets_rel = try zodd.Relation(Pair).fromSlice(&ctx, &targets_data); + var targets_rel = try zodd.Relation(Pair).fromSlice(allocator, &targets_data); defer targets_rel.deinit(); - var assoc_rel = try zodd.Relation(Pair).fromSlice(&ctx, &assoc_data); + var assoc_rel = try zodd.Relation(Pair).fromSlice(allocator, &assoc_data); defer assoc_rel.deinit(); // -- Step 1: Compute transitive type hierarchy -- // is_a(X, Z) :- is_a(X, Y), is_a(Y, Z). - var is_a = zodd.Variable(Pair).init(&ctx); + var is_a = zodd.Variable(Pair).init(allocator); defer is_a.deinit(); - try is_a.insertSlice(&ctx, is_a_rel.elements); + try is_a.insertSlice(is_a_rel.elements); std.debug.print("\nComputing transitive type hierarchy...\n", .{}); @@ -153,7 +152,7 @@ pub fn main() !void { } if (results.items.len > 0) { - const rel = try zodd.Relation(Pair).fromSlice(&ctx, results.items); + const rel = try zodd.Relation(Pair).fromSlice(allocator, results.items); try is_a.insert(rel); } if (iter > 50) break; @@ -170,9 +169,9 @@ pub fn main() !void { // -- Step 2: Inherit symptoms through type hierarchy -- // has_symptom(D, S) :- is_a(D, D2), has_symptom(D2, S). - var has_symptom = zodd.Variable(Pair).init(&ctx); + var has_symptom = zodd.Variable(Pair).init(allocator); defer has_symptom.deinit(); - try has_symptom.insertSlice(&ctx, symptom_rel.elements); + try has_symptom.insertSlice(symptom_rel.elements); // For each is_a(D, D2), propagate symptoms from D2 to D { @@ -198,7 +197,7 @@ pub fn main() !void { } if (inherited.items.len > 0) { - try has_symptom.insertSlice(&ctx, inherited.items); + try has_symptom.insertSlice(inherited.items); } } _ = try has_symptom.changed(); @@ -217,7 +216,7 @@ pub fn main() !void { // Join key = Protein. targets is (Drug, Protein), assoc is (Protein, Disease). // Rekey targets as (Protein, Drug) to align the join key. - var targets_by_protein = zodd.Variable(Pair).init(&ctx); + var targets_by_protein = zodd.Variable(Pair).init(allocator); defer targets_by_protein.deinit(); { var flipped = PairList.empty; @@ -225,21 +224,21 @@ pub fn main() !void { for (targets_rel.elements) |t| { try flipped.append(allocator, .{ t[1], t[0] }); // (Protein, Drug) } - try targets_by_protein.insertSlice(&ctx, flipped.items); + try targets_by_protein.insertSlice(flipped.items); _ = try targets_by_protein.changed(); } - var assoc_var = zodd.Variable(Pair).init(&ctx); + var assoc_var = zodd.Variable(Pair).init(allocator); defer assoc_var.deinit(); - try assoc_var.insertSlice(&ctx, assoc_rel.elements); + try assoc_var.insertSlice(assoc_rel.elements); _ = try assoc_var.changed(); const Triple = struct { u32, u32, u32 }; - var treats_triple = zodd.Variable(Triple).init(&ctx); + var treats_triple = zodd.Variable(Triple).init(allocator); defer treats_triple.deinit(); // joinInto: key=Protein, val1=Drug, val2=Disease - try zodd.joinInto(u32, u32, u32, Triple, &ctx, &targets_by_protein, &assoc_var, &treats_triple, struct { + try zodd.joinInto(u32, u32, u32, Triple, &targets_by_protein, &assoc_var, &treats_triple, struct { fn logic(_: *const u32, drug: *const u32, disease: *const u32) Triple { return .{ drug.*, disease.*, 0 }; } diff --git a/examples/e3_data_lineage.zig b/examples/e3_data_lineage.zig index 78ff040..07fa12e 100644 --- a/examples/e3_data_lineage.zig +++ b/examples/e3_data_lineage.zig @@ -18,7 +18,6 @@ pub fn main() !void { var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var ctx = zodd.ExecutionContext.init(allocator); std.debug.print("Zodd Datalog Engine - Data Lineage for Compliance\n", .{}); std.debug.print("=================================================\n\n", .{}); @@ -130,10 +129,10 @@ pub fn main() !void { // -- Build relations -- - var transforms = try zodd.Relation(Pair).fromSlice(&ctx, &transform_data); + var transforms = try zodd.Relation(Pair).fromSlice(allocator, &transform_data); defer transforms.deinit(); - var anonymizes = try zodd.Relation(Pair).fromSlice(&ctx, &anonymize_data); + var anonymizes = try zodd.Relation(Pair).fromSlice(allocator, &anonymize_data); defer anonymizes.deinit(); // -- Step 1: Propagate PII through the pipeline -- @@ -141,14 +140,14 @@ pub fn main() !void { // contains_pii(D2) :- contains_pii(D1), transform(D1, D2), // NOT anonymizes(D1, D2). - var contains_pii = zodd.Variable(Scalar).init(&ctx); + var contains_pii = zodd.Variable(Scalar).init(allocator); defer contains_pii.deinit(); - try contains_pii.insertSlice(&ctx, &source_pii_data); + try contains_pii.insertSlice(&source_pii_data); std.debug.print("\nPropagating PII through ETL pipeline...\n", .{}); // Use ExtendWith to propose destinations for PII-containing datasets - var extend = zodd.ExtendWith(Scalar, u32, u32).init(&ctx, &transforms, &struct { + var extend = zodd.ExtendWith(Scalar, u32, u32).init(allocator, &transforms, &struct { fn key(tuple: *const Scalar) u32 { return tuple[0]; } @@ -159,7 +158,7 @@ pub fn main() !void { std.debug.print(" Iteration {}: {} datasets with PII\n", .{ iteration, contains_pii.recent.len() }); // Use extendInto to find downstream datasets - var proposed = zodd.Variable(Pair).init(&ctx); + var proposed = zodd.Variable(Pair).init(allocator); defer proposed.deinit(); const leaper = extend.leaper(); @@ -169,7 +168,6 @@ pub fn main() !void { Scalar, u32, Pair, - &ctx, &contains_pii, &leapers, &proposed, @@ -203,7 +201,7 @@ pub fn main() !void { } if (new_pii.items.len > 0) { - const rel = try zodd.Relation(Scalar).fromSlice(&ctx, new_pii.items); + const rel = try zodd.Relation(Scalar).fromSlice(allocator, new_pii.items); try contains_pii.insert(rel); } diff --git a/examples/e4_rbac_authorization.zig b/examples/e4_rbac_authorization.zig index 7c1e25c..7848f29 100644 --- a/examples/e4_rbac_authorization.zig +++ b/examples/e4_rbac_authorization.zig @@ -15,7 +15,6 @@ pub fn main() !void { var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var ctx = zodd.ExecutionContext.init(allocator); std.debug.print("Zodd Datalog Engine - RBAC Authorization Example\n", .{}); std.debug.print("=================================================\n\n", .{}); @@ -104,26 +103,26 @@ pub fn main() !void { // -- Build relations -- - var user_role = try zodd.Relation(Pair).fromSlice(&ctx, &user_role_data); + var user_role = try zodd.Relation(Pair).fromSlice(allocator, &user_role_data); defer user_role.deinit(); - var role_hier = try zodd.Relation(Pair).fromSlice(&ctx, &role_hier_data); + var role_hier = try zodd.Relation(Pair).fromSlice(allocator, &role_hier_data); defer role_hier.deinit(); - var role_perm = try zodd.Relation(Pair).fromSlice(&ctx, &role_perm_data); + var role_perm = try zodd.Relation(Pair).fromSlice(allocator, &role_perm_data); defer role_perm.deinit(); - var denied = try zodd.Relation(Pair).fromSlice(&ctx, &denied_data); + var denied = try zodd.Relation(Pair).fromSlice(allocator, &denied_data); defer denied.deinit(); // -- Step 1: Compute has_role(User, Role) via transitive role inheritance -- // has_role(U, R) :- user_role(U, R). // has_role(U, R2) :- has_role(U, R1), role_hier(R1, R2). - var has_role = zodd.Variable(Pair).init(&ctx); + var has_role = zodd.Variable(Pair).init(allocator); defer has_role.deinit(); - try has_role.insertSlice(&ctx, user_role.elements); + try has_role.insertSlice(user_role.elements); std.debug.print("\nComputing effective roles via hierarchy...\n", .{}); @@ -146,7 +145,7 @@ pub fn main() !void { } if (results.items.len > 0) { - const rel = try zodd.Relation(Pair).fromSlice(&ctx, results.items); + const rel = try zodd.Relation(Pair).fromSlice(allocator, results.items); try has_role.insert(rel); } @@ -170,7 +169,7 @@ pub fn main() !void { // has_role is (User, Role), so we need to re-key it as (Role, User). // role_perm is already (Role, Perm). - var has_role_by_role = zodd.Variable(Pair).init(&ctx); + var has_role_by_role = zodd.Variable(Pair).init(allocator); defer has_role_by_role.deinit(); { var flipped = PairList.empty; @@ -178,21 +177,21 @@ pub fn main() !void { for (has_role_result.elements) |hr| { try flipped.append(allocator, .{ hr[1], hr[0] }); // (Role, User) } - try has_role_by_role.insertSlice(&ctx, flipped.items); + try has_role_by_role.insertSlice(flipped.items); _ = try has_role_by_role.changed(); } - var role_perm_var = zodd.Variable(Pair).init(&ctx); + var role_perm_var = zodd.Variable(Pair).init(allocator); defer role_perm_var.deinit(); - try role_perm_var.insertSlice(&ctx, role_perm.elements); + try role_perm_var.insertSlice(role_perm.elements); _ = try role_perm_var.changed(); const Triple = struct { u32, u32, u32 }; - var can_access_triple = zodd.Variable(Triple).init(&ctx); + var can_access_triple = zodd.Variable(Triple).init(allocator); defer can_access_triple.deinit(); // joinInto: key=Role, val1=User, val2=Perm -> (Role, User, Perm) - try zodd.joinInto(u32, u32, u32, Triple, &ctx, &has_role_by_role, &role_perm_var, &can_access_triple, struct { + try zodd.joinInto(u32, u32, u32, Triple, &has_role_by_role, &role_perm_var, &can_access_triple, struct { fn logic(role: *const u32, user: *const u32, perm: *const u32) Triple { _ = role; return .{ user.*, perm.*, 0 }; @@ -202,7 +201,7 @@ pub fn main() !void { _ = try can_access_triple.changed(); // Extract (User, Perm) pairs - var can_access = zodd.Variable(Pair).init(&ctx); + var can_access = zodd.Variable(Pair).init(allocator); defer can_access.deinit(); { var pairs = PairList.empty; @@ -210,7 +209,7 @@ pub fn main() !void { for (can_access_triple.recent.elements) |t| { try pairs.append(allocator, .{ t[0], t[1] }); } - try can_access.insertSlice(&ctx, pairs.items); + try can_access.insertSlice(pairs.items); _ = try can_access.changed(); } @@ -225,7 +224,7 @@ pub fn main() !void { // We need an anti-join on the full (User, Perm) pair. Since joinAnti keys on // the first tuple field only, we use a manual filter against the denied relation. - var effective = zodd.Variable(Pair).init(&ctx); + var effective = zodd.Variable(Pair).init(allocator); defer effective.deinit(); { var eff_list = PairList.empty; @@ -242,7 +241,7 @@ pub fn main() !void { try eff_list.append(allocator, ca); } } - try effective.insertSlice(&ctx, eff_list.items); + try effective.insertSlice(eff_list.items); _ = try effective.changed(); } diff --git a/examples/e5_taint_analysis.zig b/examples/e5_taint_analysis.zig index f0434b5..4b87a3c 100644 --- a/examples/e5_taint_analysis.zig +++ b/examples/e5_taint_analysis.zig @@ -18,7 +18,6 @@ pub fn main() !void { var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var ctx = zodd.ExecutionContext.init(allocator); std.debug.print("Zodd Datalog Engine - Taint Analysis Example\n", .{}); std.debug.print("=============================================\n\n", .{}); @@ -122,10 +121,10 @@ pub fn main() !void { // -- Build relations -- - var flow = try zodd.Relation(Pair).fromSlice(&ctx, &flow_data); + var flow = try zodd.Relation(Pair).fromSlice(allocator, &flow_data); defer flow.deinit(); - var sanitized = try zodd.Relation(Pair).fromSlice(&ctx, &sanitized_data); + var sanitized = try zodd.Relation(Pair).fromSlice(allocator, &sanitized_data); defer sanitized.deinit(); // -- Step 1: Compute tainted variables using ExtendWith + FilterAnti -- @@ -140,16 +139,16 @@ pub fn main() !void { // After extension, we get the destination variable, then wrap it back into // a Scalar and feed it into the tainted variable. - var tainted = zodd.Variable(Scalar).init(&ctx); + var tainted = zodd.Variable(Scalar).init(allocator); defer tainted.deinit(); - try tainted.insertSlice(&ctx, &source_data); + try tainted.insertSlice(&source_data); std.debug.print("\nComputing taint propagation...\n", .{}); // ExtendWith: extract key from Scalar (the tainted var id), look up in flow relation // to get destination variables. - var extend = zodd.ExtendWith(Scalar, u32, u32).init(&ctx, &flow, &struct { + var extend = zodd.ExtendWith(Scalar, u32, u32).init(allocator, &flow, &struct { fn key(tuple: *const Scalar) u32 { return tuple[0]; } @@ -175,7 +174,7 @@ pub fn main() !void { std.debug.print(" Iteration {}: {} newly tainted variables\n", .{ iteration, tainted.recent.len() }); // Use extendInto to propose destinations for recently tainted variables - var proposed = zodd.Variable(Pair).init(&ctx); + var proposed = zodd.Variable(Pair).init(allocator); defer proposed.deinit(); const leaper = extend.leaper(); @@ -185,7 +184,6 @@ pub fn main() !void { Scalar, u32, Pair, - &ctx, &tainted, &leapers, &proposed, @@ -217,7 +215,7 @@ pub fn main() !void { } if (new_tainted.items.len > 0) { - const rel = try zodd.Relation(Scalar).fromSlice(&ctx, new_tainted.items); + const rel = try zodd.Relation(Scalar).fromSlice(allocator, new_tainted.items); try tainted.insert(rel); } diff --git a/examples/e6_dependency_resolution.zig b/examples/e6_dependency_resolution.zig index a986dbf..dfc4b06 100644 --- a/examples/e6_dependency_resolution.zig +++ b/examples/e6_dependency_resolution.zig @@ -20,7 +20,6 @@ pub fn main() !void { var gpa = std.heap.DebugAllocator(.{}){}; defer _ = gpa.deinit(); const allocator = gpa.allocator(); - var ctx = zodd.ExecutionContext.init(allocator); std.debug.print("Zodd Datalog Engine - Dependency Resolution Example\n", .{}); std.debug.print("===================================================\n\n", .{}); @@ -96,17 +95,17 @@ pub fn main() !void { // -- Build relations -- - var direct_deps = try zodd.Relation(Pair).fromSlice(&ctx, &direct_dep_data); + var direct_deps = try zodd.Relation(Pair).fromSlice(allocator, &direct_dep_data); defer direct_deps.deinit(); // -- Step 1: Compute transitive dependencies -- // dep(A, B) :- direct_dep(A, B). // dep(A, C) :- dep(A, B), direct_dep(B, C). - var dep = zodd.Variable(Pair).init(&ctx); + var dep = zodd.Variable(Pair).init(allocator); defer dep.deinit(); - try dep.insertSlice(&ctx, direct_deps.elements); + try dep.insertSlice(direct_deps.elements); std.debug.print("\nComputing transitive dependencies...\n", .{}); @@ -129,7 +128,7 @@ pub fn main() !void { } if (results.items.len > 0) { - const rel = try zodd.Relation(Pair).fromSlice(&ctx, results.items); + const rel = try zodd.Relation(Pair).fromSlice(allocator, results.items); try dep.insert(rel); } @@ -166,7 +165,7 @@ pub fn main() !void { // For each package, sum the sizes of all its transitive dependencies plus itself. // We build a relation of (package, dep_size) pairs and aggregate by summing. - var pkg_sizes = try zodd.Relation(SizeTuple).fromSlice(&ctx, &size_data); + var pkg_sizes = try zodd.Relation(SizeTuple).fromSlice(allocator, &size_data); defer pkg_sizes.deinit(); // Build (package, dep_size) pairs: for each dep(A, B), look up size of B. @@ -188,15 +187,15 @@ pub fn main() !void { } } - var install_rel = try zodd.Relation(Pair).fromSlice(&ctx, install_tuples.items); + var install_rel = try zodd.Relation(Pair).fromSlice(allocator, install_tuples.items); defer install_rel.deinit(); // Aggregate: sum sizes per package - var total_sizes = try zodd.aggregateFn( + var total_sizes = try zodd.aggregate( Pair, u32, u32, - &ctx, + allocator, &install_rel, struct { fn key(tuple: *const Pair) u32 { @@ -222,7 +221,7 @@ pub fn main() !void { // Build a secondary index on the transitive deps relation, keyed by the // dependency (the target), so we can efficiently answer "who depends on X?" - const DepIndex = zodd.index.SecondaryIndex( + const DepIndex = zodd.SecondaryIndex( Pair, u32, struct { @@ -238,7 +237,7 @@ pub fn main() !void { 16, ); - var rev_index = DepIndex.init(&ctx); + var rev_index = DepIndex.init(allocator); defer rev_index.deinit(); try rev_index.insertSlice(deps.elements); diff --git a/src/lib.zig b/src/lib.zig index 592d29c..18f22e1 100644 --- a/src/lib.zig +++ b/src/lib.zig @@ -1,6 +1,6 @@ //! # Zodd //! -//! Zodd is a Datalog engine in Zig. +//! Zodd is a small, embeddable Datalog engine in Zig. //! //! ## Quickstart //! @@ -9,94 +9,107 @@ //! const zodd = @import("zodd"); //! //! pub fn main() !void { -//! var gpa = std.heap.GeneralPurposeAllocator(.{}){}; +//! var gpa = std.heap.DebugAllocator(.{}){}; //! defer _ = gpa.deinit(); //! const allocator = gpa.allocator(); //! -//! // 1. Create execution context -//! var ctx = zodd.ExecutionContext.init(allocator); -//! defer ctx.deinit(); -//! -//! // 2. Define relation (e.g., edge(x, y)) //! const Edge = struct { u32, u32 }; -//! var edge = try zodd.Relation(Edge).fromSlice(&ctx, &[_]Edge{ -//! .{ 1, 2 }, .{ 2, 3 }, .{ 3, 4 } +//! +//! // Base relation. +//! var edges = try zodd.Relation(Edge).fromSlice(allocator, &[_]Edge{ +//! .{ 1, 2 }, .{ 2, 3 }, .{ 3, 4 }, //! }); -//! defer edge.deinit(); +//! defer edges.deinit(); //! -//! // 3. Define variable for transitive closure path(x, y) -//! var path = try zodd.Variable(Edge).init(&ctx, &edge); -//! defer path.deinit(); +//! // Variable for the transitive closure. +//! var reachable = zodd.Variable(Edge).init(allocator); +//! defer reachable.deinit(); +//! try reachable.insertSlice(edges.elements); //! -//! // 4. Run semi-naive evaluation -//! while (try path.changed()) { -//! // path(x, z) :- path(x, y), edge(y, z). -//! const new_paths = try zodd.joinInto(Edge, Edge, u32, &path, &edge, 1, 0, struct { -//! fn f(x: u32, y: u32, z: u32) Edge { return .{ x, z }; } -//! }.f); -//! try path.insert(new_paths); +//! // reachable(X, Z) :- reachable(X, Y), edge(Y, Z). +//! while (try reachable.changed()) { +//! var batch: std.ArrayList(Edge) = .empty; +//! defer batch.deinit(allocator); +//! for (reachable.recent.elements) |r| { +//! for (edges.elements) |e| { +//! if (e[0] == r[1]) try batch.append(allocator, .{ r[0], e[1] }); +//! } +//! } +//! if (batch.items.len > 0) { +//! const rel = try zodd.Relation(Edge).fromSlice(allocator, batch.items); +//! try reachable.insert(rel); +//! } //! } //! -//! std.debug.print("Path count: {}\n", .{path.complete().len}); +//! var result = try reachable.complete(); +//! defer result.deinit(); +//! std.debug.print("Reachable pairs: {d}\n", .{result.len()}); //! } //! ``` -//! -//! ## Components -//! -//! - `Relation`: The immutable data structure (sorted, deduplicated tuples). -//! - `Variable`: The mutable relation for fixed-point iterations. -//! - `join`: The merge-join algorithms. -//! - `extend`: The primitives for extending tuples (semi-joins and anti-joins). -//! - `index`: The indexes for lookups. -//! - `aggregate`: The group-by and aggregation operations. - -/// Relation module. -pub const relation = @import("zodd/relation.zig"); -/// Variable module. -pub const variable = @import("zodd/variable.zig"); -/// Iteration module. -pub const iteration = @import("zodd/iteration.zig"); -/// Join module. -pub const join = @import("zodd/join.zig"); -/// Extend module. -pub const extend = @import("zodd/extend.zig"); -/// Execution context module. -pub const context = @import("zodd/context.zig"); - -/// Index module. -pub const index = @import("zodd/index.zig"); -/// Aggregation module. -pub const aggregate = @import("zodd/aggregate.zig"); - -/// Relation type. + +const relation = @import("zodd/relation.zig"); +const variable = @import("zodd/variable.zig"); +const iteration = @import("zodd/iteration.zig"); +const join = @import("zodd/join.zig"); +const extend = @import("zodd/extend.zig"); +const index_mod = @import("zodd/index.zig"); +const aggregate_mod = @import("zodd/aggregate.zig"); + +/// Immutable, sorted, deduplicated relation. pub const Relation = relation.Relation; -/// Variable type. + +/// Mutable relation used inside fixed-point loops (holds stable, recent, and +/// to-add batches for semi-naive evaluation). pub const Variable = variable.Variable; -/// Gallop search helper. + +/// Exponential + binary search over a sorted slice. pub const gallop = variable.gallop; -/// Iteration type. + +/// Fixed-point driver for a set of variables. pub const Iteration = iteration.Iteration; -/// Join helper for sorted relations. + +/// Error set returned by `Iteration.changed`. +pub const IterateError = iteration.IterateError; + +/// Sort-merge join between two sorted relations on a common key. pub const joinHelper = join.joinHelper; -/// Join into a variable. + +/// Semi-naive join that inserts the projected result into an output variable. pub const joinInto = join.joinInto; -/// Anti-join into a variable. + +/// Semi-naive anti-join; keeps tuples whose key is absent from `filter`. pub const joinAnti = join.joinAnti; -/// Leaper interface for extend. + +/// Leaper vtable used by leapfrog-style extensions. pub const Leaper = extend.Leaper; -/// Extend relation by key. + +/// Extends a tuple with every value that shares its key in a relation (semi-join). pub const ExtendWith = extend.ExtendWith; -/// Anti filter using a relation. + +/// Drops tuples whose (key, val) pair is present in a relation (anti-join predicate). pub const FilterAnti = extend.FilterAnti; -/// Anti extend using a relation. + +/// Extends a tuple with values for its key while excluding those present in another relation. pub const ExtendAnti = extend.ExtendAnti; -/// Extend into a variable. + +/// Runs a leapfrog extension over `source.recent` and inserts results into `output`. pub const extendInto = extend.extendInto; -/// Aggregate helper. -pub const aggregateFn = aggregate.aggregate; -/// Execution context type. -pub const ExecutionContext = context.ExecutionContext; + +/// B-tree secondary index keyed by an extracted attribute. +pub const SecondaryIndex = index_mod.SecondaryIndex; + +/// Group-by aggregation with a user-supplied folder. +pub const aggregate = aggregate_mod.aggregate; test { @import("std").testing.refAllDecls(@This()); + // Pull the modules themselves into test scope so their inline `test` + // blocks compile; the module identifiers themselves stay private here. + _ = relation; + _ = variable; + _ = iteration; + _ = join; + _ = extend; + _ = index_mod; + _ = aggregate_mod; } diff --git a/src/zodd/aggregate.zig b/src/zodd/aggregate.zig index 098ef7b..adee657 100644 --- a/src/zodd/aggregate.zig +++ b/src/zodd/aggregate.zig @@ -2,22 +2,20 @@ //! //! The module provides primitives for grouping and aggregating tuples. //! -//! It supports standard operations like sum, count, min, max via a generic folder interface. -//! The algorithm sorts tuples by the grouping key, then folds values. -//! The pre-processing step supports parallel execution. +//! It supports standard operations like sum, count, min, max via a generic +//! folder interface. The algorithm sorts tuples by the grouping key, then +//! folds values per group. const std = @import("std"); const Allocator = std.mem.Allocator; const Relation = @import("relation.zig").Relation; -const ExecutionContext = @import("context.zig").ExecutionContext; -const WaitGroup = @import("context.zig").WaitGroup; /// Aggregate tuples by key using a folder. pub fn aggregate( comptime Tuple: type, comptime Key: type, comptime AggVal: type, - ctx: *ExecutionContext, + allocator: Allocator, input: *const Relation(Tuple), key_func: fn (*const Tuple) Key, init_val: AggVal, @@ -26,56 +24,15 @@ pub fn aggregate( const ResultTuple = struct { Key, AggVal }; if (input.len() == 0) { - return Relation(ResultTuple).empty(ctx); + return Relation(ResultTuple).empty(allocator); } const Intermediate = struct { Key, *const Tuple }; - var intermediates = try ctx.allocator.alloc(Intermediate, input.len()); - defer ctx.allocator.free(intermediates); - - if (ctx.pool) |*pool| { - const chunk: usize = 256; - const count = input.len(); - const task_count = (count + chunk - 1) / chunk; - - const Task = struct { - start: usize, - end: usize, - input: []const Tuple, - output: []Intermediate, - key_func: *const fn (*const Tuple) Key, - - fn run(task: *@This()) void { - var i = task.start; - while (i < task.end) : (i += 1) { - task.output[i] = .{ task.key_func(&task.input[i]), &task.input[i] }; - } - } - }; - - const tasks = try ctx.allocator.alloc(Task, task_count); - defer ctx.allocator.free(tasks); - - var wg: WaitGroup = .{}; - var t: usize = 0; - while (t < task_count) : (t += 1) { - const start = t * chunk; - const end = @min(start + chunk, count); - tasks[t] = .{ - .start = start, - .end = end, - .input = input.elements, - .output = intermediates, - .key_func = &key_func, - }; - pool.*.spawnWg(&wg, Task.run, .{&tasks[t]}); - } + var intermediates = try allocator.alloc(Intermediate, input.len()); + defer allocator.free(intermediates); - wg.wait(); - } else { - for (input.elements, 0..) |*t, i| { - intermediates[i] = .{ key_func(t), t }; - } + for (input.elements, 0..) |*t, i| { + intermediates[i] = .{ key_func(t), t }; } const sortContext = struct { @@ -86,7 +43,7 @@ pub fn aggregate( std.sort.pdq(Intermediate, intermediates, {}, sortContext.lessThan); var results = std.ArrayListUnmanaged(ResultTuple).empty; - defer results.deinit(ctx.allocator); + defer results.deinit(allocator); if (intermediates.len > 0) { var current_key = intermediates[0][0]; @@ -94,24 +51,23 @@ pub fn aggregate( for (intermediates) |item| { if (std.math.order(item[0], current_key) != .eq) { - try results.append(ctx.allocator, .{ current_key, current_acc }); + try results.append(allocator, .{ current_key, current_acc }); current_key = item[0]; current_acc = init_val; } current_acc = folder(current_acc, item[1]); } - try results.append(ctx.allocator, .{ current_key, current_acc }); + try results.append(allocator, .{ current_key, current_acc }); } - return Relation(ResultTuple).fromSlice(ctx, results.items); + return Relation(ResultTuple).fromSlice(allocator, results.items); } test "aggregate: sum by key" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var data = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var data = try Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ 1, 10 }, .{ 1, 20 }, .{ 2, 5 }, @@ -131,7 +87,7 @@ test "aggregate: sum by key" { } }; - var result = try aggregate(Tuple, u32, u32, &ctx, &data, key_func.key, 0, sum_folder.fold); + var result = try aggregate(Tuple, u32, u32, allocator, &data, key_func.key, 0, sum_folder.fold); defer result.deinit(); try std.testing.expectEqual(@as(usize, 3), result.len()); @@ -149,10 +105,9 @@ test "aggregate: sum by key" { test "aggregate: count" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var data = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var data = try Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ 1, 10 }, .{ 1, 20 }, .{ 2, 5 }, @@ -170,7 +125,7 @@ test "aggregate: count" { } }; - var result = try aggregate(Tuple, u32, usize, &ctx, &data, key_func.key, 0, count_folder.fold); + var result = try aggregate(Tuple, u32, usize, allocator, &data, key_func.key, 0, count_folder.fold); defer result.deinit(); try std.testing.expectEqual(@as(usize, 2), result.len()); @@ -178,60 +133,17 @@ test "aggregate: count" { try std.testing.expectEqual(result.elements[1].@"1", 1); } -test "aggregate: parallel preprocess" { - const allocator = std.testing.allocator; - var ctx = try ExecutionContext.initWithThreads(allocator, 2); - defer ctx.deinit(); - const Tuple = struct { u32, u32 }; - - var data = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ - .{ 1, 10 }, - .{ 1, 20 }, - .{ 2, 5 }, - .{ 2, 6 }, - .{ 3, 100 }, - }); - defer data.deinit(); - - const sum_folder = struct { - fn fold(acc: u32, t: *const Tuple) u32 { - return acc + t[1]; - } - }; - const key_func = struct { - fn key(t: *const Tuple) u32 { - return t[0]; - } - }; - - var result = try aggregate(Tuple, u32, u32, &ctx, &data, key_func.key, 0, sum_folder.fold); - defer result.deinit(); - - try std.testing.expectEqual(@as(usize, 3), result.len()); - const res = result.elements; - - try std.testing.expectEqual(res[0].@"0", 1); - try std.testing.expectEqual(res[0].@"1", 30); - - try std.testing.expectEqual(res[1].@"0", 2); - try std.testing.expectEqual(res[1].@"1", 11); - - try std.testing.expectEqual(res[2].@"0", 3); - try std.testing.expectEqual(res[2].@"1", 100); -} - test "aggregate: min per key" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var data = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var data = try Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ 1, 30 }, .{ 1, 10 }, .{ 1, 20 }, .{ 2, 5 }, .{ 2, 500 }, }); defer data.deinit(); - var result = try aggregate(Tuple, u32, u32, &ctx, &data, struct { + var result = try aggregate(Tuple, u32, u32, allocator, &data, struct { fn key(t: *const Tuple) u32 { return t[0]; } @@ -249,16 +161,15 @@ test "aggregate: min per key" { test "aggregate: max per key" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var data = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var data = try Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ 1, 30 }, .{ 1, 10 }, .{ 1, 20 }, .{ 2, 5 }, .{ 2, 500 }, }); defer data.deinit(); - var result = try aggregate(Tuple, u32, u32, &ctx, &data, struct { + var result = try aggregate(Tuple, u32, u32, allocator, &data, struct { fn key(t: *const Tuple) u32 { return t[0]; } @@ -276,13 +187,12 @@ test "aggregate: max per key" { test "aggregate: empty input produces empty relation" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var empty_rel = Relation(Tuple).empty(&ctx); + var empty_rel = Relation(Tuple).empty(allocator); defer empty_rel.deinit(); - var result = try aggregate(Tuple, u32, u32, &ctx, &empty_rel, struct { + var result = try aggregate(Tuple, u32, u32, allocator, &empty_rel, struct { fn key(t: *const Tuple) u32 { return t[0]; } @@ -298,15 +208,14 @@ test "aggregate: empty input produces empty relation" { test "aggregate: single group produces one row" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var data = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var data = try Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ 7, 1 }, .{ 7, 2 }, .{ 7, 3 }, }); defer data.deinit(); - var result = try aggregate(Tuple, u32, u32, &ctx, &data, struct { + var result = try aggregate(Tuple, u32, u32, allocator, &data, struct { fn key(t: *const Tuple) u32 { return t[0]; } diff --git a/src/zodd/context.zig b/src/zodd/context.zig deleted file mode 100644 index 0be0718..0000000 --- a/src/zodd/context.zig +++ /dev/null @@ -1,149 +0,0 @@ -//! # Execution Context -//! -//! The context manages shared resources for query execution, primarily the memory allocator -//! and optional thread pool. -//! -//! Users pass it to Zodd operations to access resources. - -const std = @import("std"); -const Allocator = std.mem.Allocator; - -/// WaitGroup is a no-op synchronization marker. Since `Pool.spawnWg` currently -/// runs work inline on the calling thread, no real waiting is needed. The type -/// exists so that call sites can continue to declare `var wg: WaitGroup = .{}` -/// and call `wg.wait()` without churn. -pub const WaitGroup = struct { - pub fn wait(self: *WaitGroup) void { - _ = self; - } -}; - -/// Pool replaces the `std.Thread.Pool` removed in Zig 0.16. It currently -/// executes submitted work synchronously on the calling thread. This preserves -/// the call-site API so that the parallel code paths still compile and behave -/// correctly; real parallel execution can be layered back on once a stable -/// 0.16 concurrency story lands in user-facing std (today it lives behind -/// `std.Io` backends). -pub const Pool = struct { - allocator: Allocator, - - pub const Options = struct { - allocator: Allocator, - n_jobs: ?usize = null, - }; - - pub fn init(self: *Pool, options: Options) !void { - self.* = .{ .allocator = options.allocator }; - } - - pub fn deinit(self: *Pool) void { - self.* = undefined; - } - - pub fn spawnWg(self: *Pool, wg: *WaitGroup, comptime func: anytype, args: anytype) void { - _ = self; - _ = wg; - @call(.auto, func, args); - } -}; - -pub const ExecutionContext = struct { - /// Allocator for the context. - allocator: Allocator, - /// Thread pool for parallel execution. - pool: ?*Pool = null, - - /// Initializes a new execution context. - pub fn init(allocator: Allocator) ExecutionContext { - return .{ .allocator = allocator, .pool = null }; - } - - /// Initializes a new execution context with a thread pool. - pub fn initWithThreads(allocator: Allocator, worker_count: usize) !ExecutionContext { - const pool = try allocator.create(Pool); - errdefer allocator.destroy(pool); - try Pool.init(pool, .{ .allocator = allocator, .n_jobs = worker_count }); - return .{ .allocator = allocator, .pool = pool }; - } - - /// Deinitializes the execution context. - pub fn deinit(self: *ExecutionContext) void { - if (self.pool) |pool| { - pool.deinit(); - self.allocator.destroy(pool); - } - self.pool = null; - } - - /// Returns true if the context has a thread pool. - pub fn hasParallel(self: *const ExecutionContext) bool { - return self.pool != null; - } -}; - -test "ExecutionContext: init has no pool" { - var ctx = ExecutionContext.init(std.testing.allocator); - defer ctx.deinit(); - - try std.testing.expect(!ctx.hasParallel()); - try std.testing.expectEqual(@as(?*Pool, null), ctx.pool); -} - -test "ExecutionContext: initWithThreads attaches a pool and deinit releases it" { - var ctx = try ExecutionContext.initWithThreads(std.testing.allocator, 2); - defer ctx.deinit(); - - try std.testing.expect(ctx.hasParallel()); - try std.testing.expect(ctx.pool != null); -} - -test "ExecutionContext: deinit is idempotent" { - var ctx = try ExecutionContext.initWithThreads(std.testing.allocator, 1); - ctx.deinit(); - // Second deinit should be a no-op; pool was nulled out. - ctx.deinit(); - try std.testing.expect(!ctx.hasParallel()); -} - -test "Pool: spawnWg runs the submitted function" { - var pool: Pool = undefined; - try Pool.init(&pool, .{ .allocator = std.testing.allocator, .n_jobs = 2 }); - defer pool.deinit(); - - var ran: bool = false; - var wg: WaitGroup = .{}; - pool.spawnWg(&wg, struct { - fn run(flag: *bool) void { - flag.* = true; - } - }.run, .{&ran}); - wg.wait(); - - try std.testing.expect(ran); -} - -test "Pool: spawnWg across many tasks sees every one execute" { - var pool: Pool = undefined; - try Pool.init(&pool, .{ .allocator = std.testing.allocator, .n_jobs = 4 }); - defer pool.deinit(); - - var counters = [_]u32{0} ** 16; - var wg: WaitGroup = .{}; - for (&counters) |*c| { - pool.spawnWg(&wg, struct { - fn run(slot: *u32) void { - slot.* += 1; - } - }.run, .{c}); - } - wg.wait(); - - for (counters) |c| { - try std.testing.expectEqual(@as(u32, 1), c); - } -} - -test "WaitGroup: wait on unspawned group returns immediately" { - var wg: WaitGroup = .{}; - wg.wait(); -} diff --git a/src/zodd/extend.zig b/src/zodd/extend.zig index 67e6d45..e50f4a4 100644 --- a/src/zodd/extend.zig +++ b/src/zodd/extend.zig @@ -16,8 +16,6 @@ const Allocator = std.mem.Allocator; const Relation = @import("relation.zig").Relation; const Variable = @import("variable.zig").Variable; const gallop = @import("variable.zig").gallop; -const ExecutionContext = @import("context.zig").ExecutionContext; -const WaitGroup = @import("context.zig").WaitGroup; /// Creates a Leaper interface type for a Tuple and Value type. /// @@ -96,11 +94,11 @@ pub fn ExtendWith( cached_start: usize = 0, /// Initializes a new extend-with leaper. - pub fn init(ctx: *ExecutionContext, relation: *const Rel, key_func: *const fn (*const Tuple) Key) Self { + pub fn init(allocator: Allocator, relation: *const Rel, key_func: *const fn (*const Tuple) Key) Self { return Self{ .relation = relation, .key_func = key_func, - .allocator = ctx.allocator, + .allocator = allocator, }; } @@ -198,14 +196,14 @@ pub fn FilterAnti( /// Initializes a new filter-anti leaper. pub fn init( - ctx: *ExecutionContext, + allocator: Allocator, relation: *const Rel, key_func: *const fn (*const Tuple) struct { Key, Val }, ) Self { return Self{ .relation = relation, .key_func = key_func, - .allocator = ctx.allocator, + .allocator = allocator, }; } @@ -272,11 +270,11 @@ pub fn ExtendAnti( allocator: Allocator, /// Initializes a new extend-anti leaper. - pub fn init(ctx: *ExecutionContext, relation: *const Rel, key_func: *const fn (*const Tuple) Key) Self { + pub fn init(allocator: Allocator, relation: *const Rel, key_func: *const fn (*const Tuple) Key) Self { return Self{ .relation = relation, .key_func = key_func, - .allocator = ctx.allocator, + .allocator = allocator, }; } @@ -342,11 +340,15 @@ pub fn ExtendAnti( } /// Extends a variable into another variable using leapers. +/// +/// For each tuple in `source.recent`, picks the leaper with the smallest +/// candidate count, proposes values, intersects against the others, and +/// feeds each surviving value through `logic` to produce a `Result`. The +/// batch of results is appended to `output` via `output.insert`. pub fn extendInto( comptime Tuple: type, comptime Val: type, comptime Result: type, - ctx: *ExecutionContext, source: *Variable(Tuple), leapers: []Leaper(Tuple, Val), output: *Variable(Result), @@ -363,175 +365,45 @@ pub fn extendInto( var had_error = false; - if (ctx.pool != null and source.recent.elements.len > 0 and leapers.len > 0) { - const chunk: usize = 128; - const task_count = (source.recent.elements.len + chunk - 1) / chunk; - - const Task = struct { - slice: []const Tuple, - base_leapers: []Leaper(Tuple, Val), - leapers: []Leaper(Tuple, Val) = &[_]Leaper(Tuple, Val){}, - results: std.ArrayListUnmanaged(Result) = .empty, - had_error: bool = false, - logic_fn: *const fn (*const Tuple, *const Val) Result, - - fn run(task: *@This()) void { - var local_values = std.ArrayListUnmanaged(*const Val).empty; - defer local_values.deinit(task.base_leapers[0].allocator); - - for (task.slice) |*tuple| { - const sentinel = std.math.maxInt(usize); - var min_index: usize = sentinel; - var min_count: usize = sentinel; - - for (task.leapers, 0..) |leaper, i| { - const cnt = leaper.count(tuple); - if (cnt < min_count) { - min_count = cnt; - min_index = i; - } - } - - if (min_index == sentinel or min_count == 0 or min_count == sentinel) continue; - - local_values.clearRetainingCapacity(); - var min_leaper = &task.leapers[min_index]; - min_leaper.had_error = false; - min_leaper.propose(tuple, &local_values); - - if (min_leaper.had_error) { - task.had_error = true; - break; - } - - for (task.leapers, 0..) |leaper, i| { - if (i != min_index) { - leaper.intersect(tuple, &local_values); - } - } - - for (local_values.items) |val| { - task.results.append(min_leaper.allocator, task.logic_fn(tuple, val)) catch { - task.had_error = true; - break; - }; - } - - if (task.had_error) break; - } - } - }; - - const tasks = try ctx.allocator.alloc(Task, task_count); - defer ctx.allocator.free(tasks); - - var t: usize = 0; - while (t < task_count) : (t += 1) { - const start = t * chunk; - const end = @min(start + chunk, source.recent.elements.len); - tasks[t] = .{ - .slice = source.recent.elements[start..end], - .base_leapers = leapers, - .logic_fn = logic, - }; - } + for (source.recent.elements) |*tuple| { + const sentinel = std.math.maxInt(usize); + var min_index: usize = sentinel; + var min_count: usize = sentinel; - // Tracks how many entries of `tasks` have `leapers` populated. On an - // error partway through the clone loop, the `errdefer` below walks - // back over already-populated tasks and frees their leapers. - var populated: usize = 0; - errdefer { - for (tasks[0..populated]) |*task| { - for (task.leapers) |*leaper| { - leaper.deinit(); - } - ctx.allocator.free(task.leapers); + for (leapers, 0..) |leaper, i| { + const cnt = leaper.count(tuple); + if (cnt < min_count) { + min_count = cnt; + min_index = i; } } - var t_idx: usize = 0; - while (t_idx < task_count) : (t_idx += 1) { - const task = &tasks[t_idx]; - const clones = try ctx.allocator.alloc(Leaper(Tuple, Val), leapers.len); - var cloned: usize = 0; - errdefer { - for (clones[0..cloned]) |*leaper| { - leaper.deinit(); - } - ctx.allocator.free(clones); - } - var i: usize = 0; - while (i < leapers.len) : (i += 1) { - clones[i] = try leapers[i].clone(ctx.allocator); - cloned += 1; - } - task.leapers = clones; - populated = t_idx + 1; - } + if (min_index == sentinel or min_count == 0 or min_count == sentinel) continue; - if (ctx.pool) |*pool| { - var wg: WaitGroup = .{}; - for (tasks) |*task| { - pool.*.spawnWg(&wg, Task.run, .{task}); - } - wg.wait(); - } + values.clearRetainingCapacity(); + var min_leaper = &leapers[min_index]; + min_leaper.had_error = false; + min_leaper.propose(tuple, &values); - for (tasks) |*task| { - for (task.leapers) |*leaper| { - leaper.deinit(); - } - ctx.allocator.free(task.leapers); + if (min_leaper.had_error) { + had_error = true; + break; + } - defer task.results.deinit(output.allocator); - if (task.had_error) { - return error.OutOfMemory; - } - if (task.results.items.len > 0) { - try results.appendSlice(output.allocator, task.results.items); + for (leapers, 0..) |leaper, i| { + if (i != min_index) { + leaper.intersect(tuple, &values); } } - } else { - for (source.recent.elements) |*tuple| { - const sentinel = std.math.maxInt(usize); - var min_index: usize = sentinel; - var min_count: usize = sentinel; - - for (leapers, 0..) |leaper, i| { - const cnt = leaper.count(tuple); - if (cnt < min_count) { - min_count = cnt; - min_index = i; - } - } - if (min_index == sentinel or min_count == 0 or min_count == sentinel) continue; - - values.clearRetainingCapacity(); - var min_leaper = &leapers[min_index]; - min_leaper.had_error = false; - min_leaper.propose(tuple, &values); - - if (min_leaper.had_error) { + for (values.items) |val| { + results.append(output.allocator, logic(tuple, val)) catch { had_error = true; break; - } - - for (leapers, 0..) |leaper, i| { - if (i != min_index) { - leaper.intersect(tuple, &values); - } - } - - for (values.items) |val| { - results.append(output.allocator, logic(tuple, val)) catch { - had_error = true; - break; - }; - } - - if (had_error) break; + }; } + + if (had_error) break; } if (had_error) { @@ -539,7 +411,7 @@ pub fn extendInto( } if (results.items.len > 0) { - const rel = try Relation(Result).fromSlice(ctx, results.items); + const rel = try Relation(Result).fromSlice(output.allocator, results.items); try output.insert(rel); } } @@ -631,17 +503,16 @@ fn gallopValHelper(comptime Key: type, comptime Val: type, slice: []const struct test "ExtendWith: basic" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const KV = struct { u32, u32 }; - var rel = try Relation(KV).fromSlice(&ctx, &[_]KV{ + var rel = try Relation(KV).fromSlice(allocator, &[_]KV{ .{ 1, 10 }, .{ 1, 11 }, .{ 2, 20 }, }); defer rel.deinit(); - var ext = ExtendWith(u32, u32, u32).init(&ctx, &rel, struct { + var ext = ExtendWith(u32, u32, u32).init(allocator, &rel, struct { fn f(t: *const u32) u32 { return t.*; } @@ -654,17 +525,16 @@ test "ExtendWith: basic" { test "FilterAnti: filters matching tuples" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const KV = struct { u32, u32 }; const Tuple = struct { u32, u32 }; - var rel = try Relation(KV).fromSlice(&ctx, &[_]KV{ + var rel = try Relation(KV).fromSlice(allocator, &[_]KV{ .{ 1, 10 }, .{ 2, 20 }, }); defer rel.deinit(); - var filter = FilterAnti(Tuple, u32, u32).init(&ctx, &rel, struct { + var filter = FilterAnti(Tuple, u32, u32).init(allocator, &rel, struct { fn f(t: *const Tuple) KV { return .{ t[0], t[1] }; } @@ -679,17 +549,16 @@ test "FilterAnti: filters matching tuples" { test "ExtendAnti: proposes absent values" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const KV = struct { u32, u32 }; // Key, Val // Relation contains {(1, 10), (1, 20)} - var rel = try Relation(KV).fromSlice(&ctx, &[_]KV{ + var rel = try Relation(KV).fromSlice(allocator, &[_]KV{ .{ 1, 10 }, .{ 1, 20 }, }); defer rel.deinit(); - var ext = ExtendAnti(u32, u32, u32).init(&ctx, &rel, struct { + var ext = ExtendAnti(u32, u32, u32).init(allocator, &rel, struct { fn f(t: *const u32) u32 { return t.*; } @@ -720,20 +589,19 @@ test "ExtendAnti: proposes absent values" { test "extendInto: leapfrog join" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32 }; const Val = u32; // We are extending Tuple(u32) with a new u32 value // Pattern: R(x, y) :- A(x), B(x, y), C(x, y) // A provides x. B and C constrain y. - var A = Variable(Tuple).init(&ctx); + var A = Variable(Tuple).init(allocator); defer A.deinit(); - try A.insertSlice(&ctx, &[_]Tuple{.{1}}); // x=1 + try A.insertSlice(&[_]Tuple{.{1}}); // x=1 _ = try A.changed(); // B = {(1, 10), (1, 20), (1, 30)} - var R_B = try Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var R_B = try Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 1, 10 }, .{ 1, 20 }, .{ 1, 30 }, @@ -741,24 +609,24 @@ test "extendInto: leapfrog join" { defer R_B.deinit(); // C = {(1, 20), (1, 30), (1, 40)} - var R_C = try Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var R_C = try Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 1, 20 }, .{ 1, 30 }, .{ 1, 40 }, }); defer R_C.deinit(); - var output = Variable(struct { u32, u32 }).init(&ctx); + var output = Variable(struct { u32, u32 }).init(allocator); defer output.deinit(); // Leapers for B and C - var extB = ExtendWith(Tuple, u32, Val).init(&ctx, &R_B, struct { + var extB = ExtendWith(Tuple, u32, Val).init(allocator, &R_B, struct { fn f(t: *const Tuple) u32 { return t[0]; } }.f); - var extC = ExtendWith(Tuple, u32, Val).init(&ctx, &R_C, struct { + var extC = ExtendWith(Tuple, u32, Val).init(allocator, &R_C, struct { fn f(t: *const Tuple) u32 { return t[0]; } @@ -766,7 +634,7 @@ test "extendInto: leapfrog join" { var leapers = [_]Leaper(Tuple, Val){ extB.leaper(), extC.leaper() }; - try extendInto(Tuple, Val, struct { u32, u32 }, &ctx, &A, &leapers, &output, struct { + try extendInto(Tuple, Val, struct { u32, u32 }, &A, &leapers, &output, struct { fn logic(t: *const Tuple, v: *const Val) struct { u32, u32 } { return .{ t[0], v.* }; } @@ -782,24 +650,23 @@ test "extendInto: leapfrog join" { test "extendInto: only anti leapers is harmless" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32 }; const Val = u32; - var source = Variable(Tuple).init(&ctx); + var source = Variable(Tuple).init(allocator); defer source.deinit(); - try source.insertSlice(&ctx, &[_]Tuple{.{1}}); + try source.insertSlice(&[_]Tuple{.{1}}); _ = try source.changed(); const KV = struct { u32, u32 }; - var rel = try Relation(KV).fromSlice(&ctx, &[_]KV{}); + var rel = try Relation(KV).fromSlice(allocator, &[_]KV{}); defer rel.deinit(); - var output = Variable(struct { u32, u32 }).init(&ctx); + var output = Variable(struct { u32, u32 }).init(allocator); defer output.deinit(); - var ext = ExtendAnti(Tuple, u32, Val).init(&ctx, &rel, struct { + var ext = ExtendAnti(Tuple, u32, Val).init(allocator, &rel, struct { fn f(t: *const Tuple) u32 { return t[0]; } @@ -807,7 +674,7 @@ test "extendInto: only anti leapers is harmless" { var leapers = [_]Leaper(Tuple, Val){ext.leaper()}; - try extendInto(Tuple, Val, struct { u32, u32 }, &ctx, &source, leapers[0..], &output, struct { + try extendInto(Tuple, Val, struct { u32, u32 }, &source, leapers[0..], &output, struct { fn logic(t: *const Tuple, v: *const Val) struct { u32, u32 } { return .{ t[0], v.* }; } @@ -820,16 +687,15 @@ test "extendInto: only anti leapers is harmless" { test "ExtendWith: count zero does not propose values" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const KV = struct { u32, u32 }; const Tuple = u32; - var rel = try Relation(KV).fromSlice(&ctx, &[_]KV{ + var rel = try Relation(KV).fromSlice(allocator, &[_]KV{ .{ 2, 20 }, }); defer rel.deinit(); - var ext = ExtendWith(Tuple, u32, u32).init(&ctx, &rel, struct { + var ext = ExtendWith(Tuple, u32, u32).init(allocator, &rel, struct { fn f(t: *const Tuple) u32 { return t.*; } @@ -848,20 +714,19 @@ test "ExtendWith: count zero does not propose values" { test "FilterAnti and ExtendAnti: empty relation" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32 }; const KV = struct { u32, u32 }; - var rel = try Relation(KV).fromSlice(&ctx, &[_]KV{}); + var rel = try Relation(KV).fromSlice(allocator, &[_]KV{}); defer rel.deinit(); - var filter = FilterAnti(Tuple, u32, u32).init(&ctx, &rel, struct { + var filter = FilterAnti(Tuple, u32, u32).init(allocator, &rel, struct { fn f(t: *const Tuple) KV { return .{ t[0], 0 }; } }.f); - var ext = ExtendAnti(Tuple, u32, u32).init(&ctx, &rel, struct { + var ext = ExtendAnti(Tuple, u32, u32).init(allocator, &rel, struct { fn f(t: *const Tuple) u32 { return t[0]; } @@ -881,42 +746,40 @@ test "FilterAnti and ExtendAnti: empty relation" { try std.testing.expectEqual(@as(usize, 2), values.items.len); } -test "extendInto: parallel" { +test "extendInto: two leapers intersect to the common values" { const allocator = std.testing.allocator; - var ctx = try ExecutionContext.initWithThreads(allocator, 2); - defer ctx.deinit(); const Tuple = struct { u32 }; const Val = u32; - var A = Variable(Tuple).init(&ctx); + var A = Variable(Tuple).init(allocator); defer A.deinit(); - try A.insertSlice(&ctx, &[_]Tuple{.{1}}); + try A.insertSlice(&[_]Tuple{.{1}}); _ = try A.changed(); - var R_B = try Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var R_B = try Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 1, 10 }, .{ 1, 20 }, .{ 1, 30 }, }); defer R_B.deinit(); - var R_C = try Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var R_C = try Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 1, 20 }, .{ 1, 30 }, .{ 1, 40 }, }); defer R_C.deinit(); - var output = Variable(struct { u32, u32 }).init(&ctx); + var output = Variable(struct { u32, u32 }).init(allocator); defer output.deinit(); - var extB = ExtendWith(Tuple, u32, Val).init(&ctx, &R_B, struct { + var extB = ExtendWith(Tuple, u32, Val).init(allocator, &R_B, struct { fn f(t: *const Tuple) u32 { return t[0]; } }.f); - var extC = ExtendWith(Tuple, u32, Val).init(&ctx, &R_C, struct { + var extC = ExtendWith(Tuple, u32, Val).init(allocator, &R_C, struct { fn f(t: *const Tuple) u32 { return t[0]; } @@ -924,7 +787,7 @@ test "extendInto: parallel" { var leapers = [_]Leaper(Tuple, Val){ extB.leaper(), extC.leaper() }; - try extendInto(Tuple, Val, struct { u32, u32 }, &ctx, &A, &leapers, &output, struct { + try extendInto(Tuple, Val, struct { u32, u32 }, &A, &leapers, &output, struct { fn logic(t: *const Tuple, v: *const Val) struct { u32, u32 } { return .{ t[0], v.* }; } @@ -936,104 +799,6 @@ test "extendInto: parallel" { try std.testing.expectEqual(output.recent.elements[1][1], 30); } -test "extendInto: clone failure cleans up" { - const allocator = std.testing.allocator; - var ctx = try ExecutionContext.initWithThreads(allocator, 2); - defer ctx.deinit(); - - const Tuple = struct { u32 }; - const Val = u32; - - var source = Variable(Tuple).init(&ctx); - defer source.deinit(); - - try source.insertSlice(&ctx, &[_]Tuple{.{1}}); - _ = try source.changed(); - - const Counter = struct { - clones: usize = 0, - destroys: usize = 0, - fail_after: usize = 0, - }; - - const State = struct { - counter: *Counter, - value: Val, - }; - - const VTable = Leaper(Tuple, Val).VTable; - - const Impl = struct { - fn count(ptr: *anyopaque, _: *const Tuple) usize { - _ = ptr; - return 1; - } - - fn propose(ptr: *anyopaque, _: *const Tuple, alloc: Allocator, values: *std.ArrayListUnmanaged(*const Val), had_error: *bool) void { - const state: *State = @ptrCast(@alignCast(ptr)); - values.append(alloc, &state.value) catch { - had_error.* = true; - return; - }; - } - - fn intersect(_: *anyopaque, _: *const Tuple, _: *std.ArrayListUnmanaged(*const Val)) void {} - - fn clone(ptr: *anyopaque, alloc: Allocator) Allocator.Error!*anyopaque { - const state: *State = @ptrCast(@alignCast(ptr)); - if (state.counter.clones >= state.counter.fail_after) return error.OutOfMemory; - const new_state = try alloc.create(State); - new_state.* = .{ .counter = state.counter, .value = state.value }; - state.counter.clones += 1; - return @ptrCast(new_state); - } - - fn destroy(ptr: *anyopaque, alloc: Allocator) void { - const state: *State = @ptrCast(@alignCast(ptr)); - state.counter.destroys += 1; - alloc.destroy(state); - } - }; - - var counter = Counter{ .fail_after = 1 }; - - const makeLeaper = struct { - fn make(alloc: Allocator, counter_ptr: *Counter, value: Val) !Leaper(Tuple, Val) { - const state = try alloc.create(State); - state.* = .{ .counter = counter_ptr, .value = value }; - return .{ - .ptr = @ptrCast(state), - .allocator = alloc, - .vtable = &VTable{ - .count = Impl.count, - .propose = Impl.propose, - .intersect = Impl.intersect, - .clone = Impl.clone, - .destroy = Impl.destroy, - }, - }; - } - }; - - var leaper1 = try makeLeaper.make(allocator, &counter, 10); - defer leaper1.deinit(); - var leaper2 = try makeLeaper.make(allocator, &counter, 20); - defer leaper2.deinit(); - - var leapers = [_]Leaper(Tuple, Val){ leaper1, leaper2 }; - - var output = Variable(struct { u32, u32 }).init(&ctx); - defer output.deinit(); - - try std.testing.expectError(error.OutOfMemory, extendInto(Tuple, Val, struct { u32, u32 }, &ctx, &source, leapers[0..], &output, struct { - fn logic(t: *const Tuple, v: *const Val) struct { u32, u32 } { - return .{ t[0], v.* }; - } - }.logic)); - - try std.testing.expectEqual(counter.clones, counter.destroys); -} - test "Leaper clone uses clone allocator" { const allocator = std.testing.allocator; @@ -1083,13 +848,13 @@ test "Leaper clone uses clone allocator" { var base_count = CountingAlloc{ .backing = allocator }; var clone_count = CountingAlloc{ .backing = allocator }; - var ctx = ExecutionContext.init(base_count.wrap()); + const base_alloc = base_count.wrap(); const KV = struct { u32, u32 }; - var rel = try Relation(KV).fromSlice(&ctx, &[_]KV{.{ 1, 10 }}); + var rel = try Relation(KV).fromSlice(base_alloc, &[_]KV{.{ 1, 10 }}); defer rel.deinit(); - var ext = ExtendWith(u32, u32, u32).init(&ctx, &rel, struct { + var ext = ExtendWith(u32, u32, u32).init(base_alloc, &rel, struct { fn f(t: *const u32) u32 { return t.*; } diff --git a/src/zodd/index.zig b/src/zodd/index.zig index 476c435..e79bd08 100644 --- a/src/zodd/index.zig +++ b/src/zodd/index.zig @@ -9,7 +9,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const ordered = @import("ordered"); const Relation = @import("relation.zig").Relation; -const ExecutionContext = @import("context.zig").ExecutionContext; pub fn SecondaryIndex( comptime Tuple: type, @@ -26,15 +25,12 @@ pub fn SecondaryIndex( map: Map, /// Allocator for the index. allocator: Allocator, - /// Execution context. - ctx: *ExecutionContext, /// Initializes a new secondary index. - pub fn init(ctx: *ExecutionContext) Self { + pub fn init(allocator: Allocator) Self { return Self{ - .map = Map.init(ctx.allocator), - .allocator = ctx.allocator, - .ctx = ctx, + .map = Map.init(allocator), + .allocator = allocator, }; } @@ -59,14 +55,14 @@ pub fn SecondaryIndex( pub fn insert(self: *Self, tuple: Tuple) !void { const key = key_extractor(tuple); if (self.map.getPtr(key)) |rel_ptr| { - const single = try Relation(Tuple).fromSlice(self.ctx, &[_]Tuple{tuple}); + const single = try Relation(Tuple).fromSlice(self.allocator, &[_]Tuple{tuple}); var mutable_single = single; errdefer mutable_single.deinit(); var old_rel = rel_ptr.*; const new_rel = try old_rel.merge(&mutable_single); rel_ptr.* = new_rel; } else { - const rel = try Relation(Tuple).fromSlice(self.ctx, &[_]Tuple{tuple}); + const rel = try Relation(Tuple).fromSlice(self.allocator, &[_]Tuple{tuple}); try self.map.put(key, rel); } } @@ -104,7 +100,7 @@ pub fn SecondaryIndex( try result_tuples.appendSlice(self.allocator, entry.value.elements); } - return Relation(Tuple).fromSlice(self.ctx, result_tuples.items); + return Relation(Tuple).fromSlice(self.allocator, result_tuples.items); } }; } @@ -115,7 +111,6 @@ fn u32Compare(a: u32, b: u32) std.math.Order { test "SecondaryIndex: basic usage" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; const Index = SecondaryIndex(Tuple, u32, struct { @@ -124,7 +119,7 @@ test "SecondaryIndex: basic usage" { } }.extract, u32Compare, 4); - var idx = Index.init(&ctx); + var idx = Index.init(allocator); defer idx.deinit(); try idx.insert(.{ 1, 10 }); @@ -146,7 +141,6 @@ test "SecondaryIndex: basic usage" { test "SecondaryIndex: getRange empty and inverted" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; const Index = SecondaryIndex(Tuple, u32, struct { @@ -155,7 +149,7 @@ test "SecondaryIndex: getRange empty and inverted" { } }.extract, u32Compare, 4); - var idx = Index.init(&ctx); + var idx = Index.init(allocator); defer idx.deinit(); try idx.insert(.{ 1, 10 }); @@ -174,7 +168,6 @@ test "SecondaryIndex: getRange end is inclusive" { // Locks in the closed-interval [start, end] contract documented on // getRange. A regression here would be a silent behavior change. const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; const Index = SecondaryIndex(Tuple, u32, struct { @@ -183,7 +176,7 @@ test "SecondaryIndex: getRange end is inclusive" { } }.extract, u32Compare, 4); - var idx = Index.init(&ctx); + var idx = Index.init(allocator); defer idx.deinit(); try idx.insert(.{ 1, 10 }); diff --git a/src/zodd/iteration.zig b/src/zodd/iteration.zig index b94c149..a08da96 100644 --- a/src/zodd/iteration.zig +++ b/src/zodd/iteration.zig @@ -1,17 +1,17 @@ //! # Iteration Manager //! -//! The manager handles the fixpoint iteration loop for semi-naive evaluation. +//! The manager handles the fixed-point iteration loop for semi-naive evaluation. //! //! It orchestrates the evolution of multiple `Variable` instances, checking for convergence -//! (when no new facts are added). It supports parallel "changed" checks. +//! (when no new facts are added). const std = @import("std"); const Allocator = std.mem.Allocator; const Variable = @import("variable.zig").Variable; const Relation = @import("relation.zig").Relation; -const ExecutionContext = @import("context.zig").ExecutionContext; -const Pool = @import("context.zig").Pool; -const WaitGroup = @import("context.zig").WaitGroup; + +/// Errors returned by `Iteration.changed`. +pub const IterateError = Allocator.Error || error{MaxIterationsExceeded}; pub fn Iteration(comptime Tuple: type) type { return struct { @@ -23,25 +23,24 @@ pub fn Iteration(comptime Tuple: type) type { variables: VarList, /// Allocator for the iteration. allocator: Allocator, - /// Execution context. - ctx: *ExecutionContext, /// Maximum number of iterations allowed. max_iterations: usize, /// Current iteration count. current_iteration: usize, - /// Initializes a new iteration. - pub fn init(ctx: *ExecutionContext, max_iterations: ?usize) Self { + /// Initializes a new iteration. `max_iterations` of `null` means no + /// limit; otherwise `changed` returns `error.MaxIterationsExceeded` + /// after that many steps. + pub fn init(allocator: Allocator, max_iterations: ?usize) Self { return Self{ .variables = VarList.empty, - .allocator = ctx.allocator, - .ctx = ctx, + .allocator = allocator, .max_iterations = max_iterations orelse std.math.maxInt(usize), .current_iteration = 0, }; } - /// Deinitializes the iteration. + /// Deinitializes the iteration and every `Variable` it owns. pub fn deinit(self: *Self) void { for (self.variables.items) |v| { v.deinit(); @@ -50,27 +49,22 @@ pub fn Iteration(comptime Tuple: type) type { self.variables.deinit(self.allocator); } - /// Creates a new variable associated with this iteration. + /// Creates a new variable owned by this iteration. pub fn variable(self: *Self) Allocator.Error!*Var { const v = try self.allocator.create(Var); - v.* = Var.init(self.ctx); + v.* = Var.init(self.allocator); try self.variables.append(self.allocator, v); return v; } - /// Runs one step of the iteration and returns true if any variable changed. - pub fn changed(self: *Self) !bool { + /// Runs one step of the iteration and returns true if any variable + /// changed. + pub fn changed(self: *Self) IterateError!bool { if (self.current_iteration >= self.max_iterations) { return error.MaxIterationsExceeded; } self.current_iteration += 1; - if (self.ctx.pool) |pool| { - if (self.variables.items.len > 1) { - return self.changedParallel(pool); - } - } - var any_changed = false; for (self.variables.items) |v| { if (try v.changed()) { @@ -80,41 +74,7 @@ pub fn Iteration(comptime Tuple: type) type { return any_changed; } - fn changedParallel(self: *Self, pool: *Pool) !bool { - const count = self.variables.items.len; - const Task = struct { - var_ptr: *Var, - changed: bool = false, - err: ?anyerror = null, - - fn run(task: *@This()) void { - task.changed = task.var_ptr.changed() catch |err| { - task.err = err; - return; - }; - } - }; - - const tasks = try self.allocator.alloc(Task, count); - defer self.allocator.free(tasks); - - var wg: WaitGroup = .{}; - for (self.variables.items, 0..) |v, i| { - tasks[i] = .{ .var_ptr = v }; - pool.spawnWg(&wg, Task.run, .{&tasks[i]}); - } - - wg.wait(); - - var any_changed = false; - for (tasks) |task| { - if (task.err) |err| return err; - if (task.changed) any_changed = true; - } - return any_changed; - } - - /// Resets the iteration state. + /// Resets the iteration step counter. Does not touch the variables. pub fn reset(self: *Self) void { self.current_iteration = 0; } @@ -123,16 +83,15 @@ pub fn Iteration(comptime Tuple: type) type { test "Iteration: basic usage" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var iter = Iteration(u32).init(&ctx, null); + var iter = Iteration(u32).init(allocator, null); defer iter.deinit(); const v1 = try iter.variable(); const v2 = try iter.variable(); - try v1.insertSlice(&ctx, &[_]u32{ 1, 2, 3 }); - try v2.insertSlice(&ctx, &[_]u32{ 4, 5 }); + try v1.insertSlice(&[_]u32{ 1, 2, 3 }); + try v2.insertSlice(&[_]u32{ 4, 5 }); const changed1 = try iter.changed(); try std.testing.expect(changed1); @@ -143,13 +102,12 @@ test "Iteration: basic usage" { test "Iteration: recursion limit" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var iter = Iteration(u32).init(&ctx, 1); + var iter = Iteration(u32).init(allocator, 1); defer iter.deinit(); const v = try iter.variable(); - try v.insertSlice(&ctx, &[_]u32{1}); + try v.insertSlice(&[_]u32{1}); _ = try iter.changed(); @@ -158,34 +116,30 @@ test "Iteration: recursion limit" { test "Iteration: reset" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var iter = Iteration(u32).init(&ctx, 10); + var iter = Iteration(u32).init(allocator, 10); defer iter.deinit(); const v = try iter.variable(); - try v.insertSlice(&ctx, &[_]u32{1}); + try v.insertSlice(&[_]u32{1}); - // Run some iterations _ = try iter.changed(); try std.testing.expectEqual(@as(usize, 1), iter.current_iteration); iter.reset(); try std.testing.expectEqual(@as(usize, 0), iter.current_iteration); - // Can run again _ = try iter.changed(); try std.testing.expectEqual(@as(usize, 1), iter.current_iteration); } test "Iteration: reset without new data" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var iter = Iteration(u32).init(&ctx, 10); + var iter = Iteration(u32).init(allocator, 10); defer iter.deinit(); const v = try iter.variable(); - try v.insertSlice(&ctx, &[_]u32{1}); + try v.insertSlice(&[_]u32{1}); _ = try iter.changed(); const changed2 = try iter.changed(); @@ -196,24 +150,3 @@ test "Iteration: reset without new data" { const changed3 = try iter.changed(); try std.testing.expect(!changed3); } - -test "Iteration: parallel changed" { - const allocator = std.testing.allocator; - var ctx = try ExecutionContext.initWithThreads(allocator, 2); - defer ctx.deinit(); - - var iter = Iteration(u32).init(&ctx, null); - defer iter.deinit(); - - const v1 = try iter.variable(); - const v2 = try iter.variable(); - - try v1.insertSlice(&ctx, &[_]u32{ 1, 2, 3 }); - try v2.insertSlice(&ctx, &[_]u32{ 4, 5 }); - - const changed1 = try iter.changed(); - try std.testing.expect(changed1); - - const changed2 = try iter.changed(); - try std.testing.expect(!changed2); -} diff --git a/src/zodd/join.zig b/src/zodd/join.zig index 7d55615..a19f946 100644 --- a/src/zodd/join.zig +++ b/src/zodd/join.zig @@ -14,8 +14,6 @@ const Allocator = std.mem.Allocator; const Relation = @import("relation.zig").Relation; const Variable = @import("variable.zig").Variable; const gallop = @import("variable.zig").gallop; -const ExecutionContext = @import("context.zig").ExecutionContext; -const WaitGroup = @import("context.zig").WaitGroup; /// Performs a merge-join between two sorted relations on a common key. /// @@ -115,12 +113,15 @@ fn gallopKey(comptime Key: type, comptime Val: type, slice: []const struct { Key } /// Joins two variables and inserts the result into an output variable. +/// +/// Runs the semi-naive pieces `recent × stable`, `stable × recent`, and +/// `recent × recent` against `input1` and `input2`, collects the projected +/// results via `logic`, and appends them as a single batch on `output`. pub fn joinInto( comptime Key: type, comptime Val1: type, comptime Val2: type, comptime Result: type, - ctx: *ExecutionContext, input1: *Variable(struct { Key, Val1 }), input2: *Variable(struct { Key, Val2 }), output: *Variable(Result), @@ -146,80 +147,12 @@ pub fn joinInto( var had_error = false; const cb_ctx = Context{ .results = &results, .alloc = output.allocator, .logic = &logic, .had_error = &had_error }; - if (ctx.pool != null and (input1.stable.items.len + input2.stable.items.len) > 0) { - const Task = struct { - left: *const Relation(struct { Key, Val1 }), - right: *const Relation(struct { Key, Val2 }), - results: std.ArrayListUnmanaged(Result) = .empty, - alloc: Allocator, - had_error: bool = false, - - fn run(task: *@This()) void { - const TaskContext = struct { - results: *std.ArrayListUnmanaged(Result), - alloc: Allocator, - logic: *const fn (*const Key, *const Val1, *const Val2) Result, - had_error: *bool, - - fn callback(self: @This(), key: *const Key, v1: *const Val1, v2: *const Val2) void { - self.results.append(self.alloc, self.logic(key, v1, v2)) catch { - self.had_error.* = true; - }; - } - }; - - const ctx_local = TaskContext{ - .results = &task.results, - .alloc = task.alloc, - .logic = &logic, - .had_error = &task.had_error, - }; - - joinHelper(Key, Val1, Val2, task.left, task.right, ctx_local, TaskContext.callback); - } - }; - - const task_count = input1.stable.items.len + input2.stable.items.len; - const tasks = try ctx.allocator.alloc(Task, task_count); - defer ctx.allocator.free(tasks); - - var idx: usize = 0; - for (input2.stable.items) |*batch2| { - tasks[idx] = .{ .left = &input1.recent, .right = batch2, .alloc = output.allocator }; - idx += 1; - } - for (input1.stable.items) |*batch1| { - tasks[idx] = .{ .left = batch1, .right = &input2.recent, .alloc = output.allocator }; - idx += 1; - } - - if (ctx.pool) |*pool| { - var wg: WaitGroup = .{}; - for (tasks) |*task| { - pool.*.spawnWg(&wg, Task.run, .{task}); - } - wg.wait(); - } - - for (tasks) |*task| { - defer task.results.deinit(output.allocator); - if (task.had_error) { - return error.OutOfMemory; - } - if (task.results.items.len > 0) { - try results.appendSlice(output.allocator, task.results.items); - } - } - } else { - for (input2.stable.items) |*batch2| { - joinHelper(Key, Val1, Val2, &input1.recent, batch2, cb_ctx, Context.callback); - } - - for (input1.stable.items) |*batch1| { - joinHelper(Key, Val1, Val2, batch1, &input2.recent, cb_ctx, Context.callback); - } + for (input2.stable.items) |*batch2| { + joinHelper(Key, Val1, Val2, &input1.recent, batch2, cb_ctx, Context.callback); + } + for (input1.stable.items) |*batch1| { + joinHelper(Key, Val1, Val2, batch1, &input2.recent, cb_ctx, Context.callback); } - joinHelper(Key, Val1, Val2, &input1.recent, &input2.recent, cb_ctx, Context.callback); if (had_error) { @@ -227,7 +160,7 @@ pub fn joinInto( } if (results.items.len > 0) { - const rel = try Relation(Result).fromSlice(ctx, results.items); + const rel = try Relation(Result).fromSlice(output.allocator, results.items); try output.insert(rel); } } @@ -238,7 +171,6 @@ pub fn joinAnti( comptime Val: type, comptime FilterVal: type, comptime Result: type, - ctx: *ExecutionContext, input: *Variable(struct { Key, Val }), filter: *Variable(struct { Key, FilterVal }), output: *Variable(Result), @@ -248,111 +180,34 @@ pub fn joinAnti( var results = ResultList.empty; defer results.deinit(output.allocator); - if (ctx.pool != null and input.recent.elements.len > 0) { - const Task = struct { - slice: []const struct { Key, Val }, - filter: *const Variable(struct { Key, FilterVal }), - results: std.ArrayListUnmanaged(Result) = .empty, - alloc: Allocator, - logic: *const fn (*const Key, *const Val) Result, - had_error: bool = false, - - fn run(task: *@This()) void { - for (task.slice) |tuple| { - const key = tuple[0]; - var found = false; - - { - const slice = gallopKey(Key, FilterVal, task.filter.recent.elements, key); - if (slice.len > 0 and countMatchingKeys(Key, FilterVal, slice, key) > 0) { - found = true; - } - } - - if (!found) { - for (task.filter.stable.items) |*batch| { - const slice = gallopKey(Key, FilterVal, batch.elements, key); - if (slice.len > 0 and countMatchingKeys(Key, FilterVal, slice, key) > 0) { - found = true; - break; - } - } - } - - if (!found) { - task.results.append(task.alloc, task.logic(&key, &tuple[1])) catch { - task.had_error = true; - return; - }; - } - } - } - }; - - const chunk: usize = 128; - const task_count = (input.recent.elements.len + chunk - 1) / chunk; - const tasks = try ctx.allocator.alloc(Task, task_count); - defer ctx.allocator.free(tasks); - - var i: usize = 0; - while (i < task_count) : (i += 1) { - const start = i * chunk; - const end = @min(start + chunk, input.recent.elements.len); - tasks[i] = .{ - .slice = input.recent.elements[start..end], - .filter = filter, - .alloc = output.allocator, - .logic = &logic, - }; - } - - if (ctx.pool) |*pool| { - var wg: WaitGroup = .{}; - for (tasks) |*task| { - pool.*.spawnWg(&wg, Task.run, .{task}); - } - wg.wait(); - } + for (input.recent.elements) |tuple| { + const key = tuple[0]; + var found = false; - for (tasks) |*task| { - defer task.results.deinit(output.allocator); - if (task.had_error) { - return error.OutOfMemory; - } - if (task.results.items.len > 0) { - try results.appendSlice(output.allocator, task.results.items); + { + const slice = gallopKey(Key, FilterVal, filter.recent.elements, key); + if (slice.len > 0 and countMatchingKeys(Key, FilterVal, slice, key) > 0) { + found = true; } } - } else { - for (input.recent.elements) |tuple| { - const key = tuple[0]; - var found = false; - { - const slice = gallopKey(Key, FilterVal, filter.recent.elements, key); + if (!found) { + for (filter.stable.items) |*batch| { + const slice = gallopKey(Key, FilterVal, batch.elements, key); if (slice.len > 0 and countMatchingKeys(Key, FilterVal, slice, key) > 0) { found = true; + break; } } + } - if (!found) { - for (filter.stable.items) |*batch| { - const slice = gallopKey(Key, FilterVal, batch.elements, key); - if (slice.len > 0 and countMatchingKeys(Key, FilterVal, slice, key) > 0) { - found = true; - break; - } - } - } - - if (!found) { - try results.append(output.allocator, logic(&key, &tuple[1])); - } + if (!found) { + try results.append(output.allocator, logic(&key, &tuple[1])); } } if (results.items.len > 0) { - const rel = try Relation(Result).fromSlice(ctx, results.items); + const rel = try Relation(Result).fromSlice(output.allocator, results.items); try output.insert(rel); } } @@ -362,16 +217,15 @@ test "joinHelper: basic" { const Tuple2 = struct { u32, u32 }; const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var input1 = try Relation(Tuple1).fromSlice(&ctx, &[_]Tuple1{ + var input1 = try Relation(Tuple1).fromSlice(allocator, &[_]Tuple1{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 }, }); defer input1.deinit(); - var input2 = try Relation(Tuple2).fromSlice(&ctx, &[_]Tuple2{ + var input2 = try Relation(Tuple2).fromSlice(allocator, &[_]Tuple2{ .{ 2, 200 }, .{ 3, 300 }, .{ 3, 301 }, @@ -399,25 +253,24 @@ test "joinHelper: basic" { test "joinInto: variable join" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var v1 = Variable(Tuple).init(&ctx); + var v1 = Variable(Tuple).init(allocator); defer v1.deinit(); - var v2 = Variable(Tuple).init(&ctx); + var v2 = Variable(Tuple).init(allocator); defer v2.deinit(); - var output = Variable(struct { u32, u32, u32 }).init(&ctx); + var output = Variable(struct { u32, u32, u32 }).init(allocator); defer output.deinit(); - try v1.insertSlice(&ctx, &[_]Tuple{ .{ 1, 10 }, .{ 2, 20 } }); - try v2.insertSlice(&ctx, &[_]Tuple{ .{ 2, 200 }, .{ 3, 300 } }); + try v1.insertSlice(&[_]Tuple{ .{ 1, 10 }, .{ 2, 20 } }); + try v2.insertSlice(&[_]Tuple{ .{ 2, 200 }, .{ 3, 300 } }); _ = try v1.changed(); _ = try v2.changed(); - try joinInto(u32, u32, u32, struct { u32, u32, u32 }, &ctx, &v1, &v2, &output, struct { + try joinInto(u32, u32, u32, struct { u32, u32, u32 }, &v1, &v2, &output, struct { fn logic(key: *const u32, v1_val: *const u32, v2_val: *const u32) struct { u32, u32, u32 } { return .{ key.*, v1_val.*, v2_val.* }; } @@ -429,25 +282,24 @@ test "joinInto: variable join" { test "joinAnti: simple negation" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var input = Variable(Tuple).init(&ctx); + var input = Variable(Tuple).init(allocator); defer input.deinit(); - var filter = Variable(Tuple).init(&ctx); + var filter = Variable(Tuple).init(allocator); defer filter.deinit(); - var output = Variable(Tuple).init(&ctx); + var output = Variable(Tuple).init(allocator); defer output.deinit(); - try input.insertSlice(&ctx, &[_]Tuple{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 } }); - try filter.insertSlice(&ctx, &[_]Tuple{.{ 2, 200 }}); + try input.insertSlice(&[_]Tuple{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 } }); + try filter.insertSlice(&[_]Tuple{.{ 2, 200 }}); _ = try input.changed(); _ = try filter.changed(); - try joinAnti(u32, u32, u32, Tuple, &ctx, &input, &filter, &output, struct { + try joinAnti(u32, u32, u32, Tuple, &input, &filter, &output, struct { fn logic(key: *const u32, val: *const u32) Tuple { return .{ key.*, val.* }; } @@ -462,18 +314,17 @@ test "joinAnti: simple negation" { test "joinHelper: multiplicative matches" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const T1 = struct { u32, u32 }; const T2 = struct { u32, u32 }; - var input1 = try Relation(T1).fromSlice(&ctx, &[_]T1{ + var input1 = try Relation(T1).fromSlice(allocator, &[_]T1{ .{ 1, 10 }, .{ 1, 11 }, .{ 2, 20 }, }); defer input1.deinit(); - var input2 = try Relation(T2).fromSlice(&ctx, &[_]T2{ + var input2 = try Relation(T2).fromSlice(allocator, &[_]T2{ .{ 1, 100 }, .{ 1, 101 }, .{ 2, 200 }, @@ -500,27 +351,26 @@ test "joinHelper: multiplicative matches" { test "joinInto: stable batches only" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var v1 = Variable(Tuple).init(&ctx); + var v1 = Variable(Tuple).init(allocator); defer v1.deinit(); - var v2 = Variable(Tuple).init(&ctx); + var v2 = Variable(Tuple).init(allocator); defer v2.deinit(); - var output = Variable(struct { u32, u32, u32 }).init(&ctx); + var output = Variable(struct { u32, u32, u32 }).init(allocator); defer output.deinit(); - try v1.insertSlice(&ctx, &[_]Tuple{.{ 1, 10 }}); + try v1.insertSlice(&[_]Tuple{.{ 1, 10 }}); _ = try v1.changed(); _ = try v1.changed(); - try v2.insertSlice(&ctx, &[_]Tuple{ .{ 1, 100 }, .{ 2, 200 } }); + try v2.insertSlice(&[_]Tuple{ .{ 1, 100 }, .{ 2, 200 } }); _ = try v2.changed(); _ = try v2.changed(); - try joinInto(u32, u32, u32, struct { u32, u32, u32 }, &ctx, &v1, &v2, &output, struct { + try joinInto(u32, u32, u32, struct { u32, u32, u32 }, &v1, &v2, &output, struct { fn logic(key: *const u32, v1_val: *const u32, v2_val: *const u32) struct { u32, u32, u32 } { return .{ key.*, v1_val.*, v2_val.* }; } @@ -531,28 +381,26 @@ test "joinInto: stable batches only" { try std.testing.expectEqual(@as(usize, 0), output.recent.len()); } -test "joinAnti: parallel" { +test "joinAnti: four tuples filtered by two" { const allocator = std.testing.allocator; - var ctx = try ExecutionContext.initWithThreads(allocator, 2); - defer ctx.deinit(); const Tuple = struct { u32, u32 }; - var input = Variable(Tuple).init(&ctx); + var input = Variable(Tuple).init(allocator); defer input.deinit(); - var filter = Variable(Tuple).init(&ctx); + var filter = Variable(Tuple).init(allocator); defer filter.deinit(); - var output = Variable(Tuple).init(&ctx); + var output = Variable(Tuple).init(allocator); defer output.deinit(); - try input.insertSlice(&ctx, &[_]Tuple{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 }, .{ 4, 40 } }); - try filter.insertSlice(&ctx, &[_]Tuple{ .{ 2, 200 }, .{ 4, 400 } }); + try input.insertSlice(&[_]Tuple{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 }, .{ 4, 40 } }); + try filter.insertSlice(&[_]Tuple{ .{ 2, 200 }, .{ 4, 400 } }); _ = try input.changed(); _ = try filter.changed(); - try joinAnti(u32, u32, u32, Tuple, &ctx, &input, &filter, &output, struct { + try joinAnti(u32, u32, u32, Tuple, &input, &filter, &output, struct { fn logic(key: *const u32, val: *const u32) Tuple { return .{ key.*, val.* }; } diff --git a/src/zodd/relation.zig b/src/zodd/relation.zig index f668c47..bca0bf5 100644 --- a/src/zodd/relation.zig +++ b/src/zodd/relation.zig @@ -8,16 +8,14 @@ //! ```zig //! const zodd = @import("zodd"); //! -//! // Define tuple type //! const Edge = struct { u32, u32 }; //! -//! // Create from slice -//! var rel = try zodd.Relation(Edge).fromSlice(ctx, &[_]Edge{ +//! var rel = try zodd.Relation(Edge).fromSlice(allocator, &[_]Edge{ //! .{ 1, 2 }, .{ 1, 2 }, .{ 2, 3 } //! }); //! defer rel.deinit(); //! -//! // Elements are sorted and deduplicated +//! // Elements are sorted and deduplicated. //! std.debug.assert(rel.elements.len == 2); //! ``` @@ -25,8 +23,6 @@ const std = @import("std"); const mem = std.mem; const sort = std.sort; const Allocator = mem.Allocator; -const ExecutionContext = @import("context.zig").ExecutionContext; -const WaitGroup = @import("context.zig").WaitGroup; /// Shrinks `slice` in place to `new_len`, or allocates a fresh smaller buffer /// and copies into it. On success the returned slice's length equals its @@ -52,123 +48,46 @@ pub fn Relation(comptime Tuple: type) type { /// The underlying sorted, deduplicated slice of tuples. /// The slice is owned by the Relation. elements: []Tuple, - /// The allocator for the relation. + /// The allocator that owns the elements buffer. allocator: Allocator, - /// The execution context. - ctx: *ExecutionContext, - /// Creates a `Relation` from a slice of tuples. - /// - /// The function copies, sorts, and deduplicates the input slice. - /// It uses a thread pool for sorting if one is available. - /// - /// Arguments: - /// - `ctx`: The execution context. - /// - `input`: The slice of tuples. - /// - /// Returns: A new `Relation`. - pub fn fromSlice(ctx: *ExecutionContext, input: []const Tuple) Allocator.Error!Self { + /// Creates a `Relation` from a slice of tuples. The function copies, + /// sorts, and deduplicates the input slice. + pub fn fromSlice(allocator: Allocator, input: []const Tuple) Allocator.Error!Self { if (input.len == 0) { return Self{ .elements = &[_]Tuple{}, - .allocator = ctx.allocator, - .ctx = ctx, + .allocator = allocator, }; } - const elements = try ctx.allocator.alloc(Tuple, input.len); - errdefer ctx.allocator.free(elements); - if (ctx.pool) |pool| { - const chunk: usize = 1024; - const task_count = (input.len + chunk - 1) / chunk; - const Task = struct { - start: usize, - end: usize, - input: []const Tuple, - output: []Tuple, - - fn run(task: *@This()) void { - const size = task.end - task.start; - if (size == 0) return; - @memcpy(task.output[task.start..task.end], task.input[task.start..task.end]); - } - }; - - const tasks = try ctx.allocator.alloc(Task, task_count); - defer ctx.allocator.free(tasks); - - var wg: WaitGroup = .{}; - var t: usize = 0; - while (t < task_count) : (t += 1) { - const start = t * chunk; - const end = @min(start + chunk, input.len); - tasks[t] = .{ .start = start, .end = end, .input = input, .output = elements }; - pool.spawnWg(&wg, Task.run, .{&tasks[t]}); - } - - if (task_count > 0) { - wg.wait(); - } - } else { - @memcpy(elements, input); - } - - if (ctx.pool) |pool| { - const chunk: usize = 2048; - const task_count = (input.len + chunk - 1) / chunk; - if (task_count > 1) { - const Task = struct { - start: usize, - end: usize, - data: []Tuple, - - fn run(task: *@This()) void { - std.sort.pdq(Tuple, task.data[task.start..task.end], {}, lessThan); - } - }; - - const tasks = try ctx.allocator.alloc(Task, task_count); - defer ctx.allocator.free(tasks); - - var wg: WaitGroup = .{}; - var t2: usize = 0; - while (t2 < task_count) : (t2 += 1) { - const start = t2 * chunk; - const end = @min(start + chunk, input.len); - tasks[t2] = .{ .start = start, .end = end, .data = elements }; - pool.spawnWg(&wg, Task.run, .{&tasks[t2]}); - } - - wg.wait(); - } - } + const elements = try allocator.alloc(Tuple, input.len); + errdefer allocator.free(elements); + @memcpy(elements, input); sort.pdq(Tuple, elements, {}, lessThan); const unique_len = deduplicate(elements); if (unique_len < elements.len) { - const shrunk = try shrinkOrCopy(Tuple, ctx.allocator, elements, unique_len); + const shrunk = try shrinkOrCopy(Tuple, allocator, elements, unique_len); return Self{ .elements = shrunk, - .allocator = ctx.allocator, - .ctx = ctx, + .allocator = allocator, }; } return Self{ .elements = elements, - .allocator = ctx.allocator, - .ctx = ctx, + .allocator = allocator, }; } /// Creates an empty relation. - pub fn empty(ctx: *ExecutionContext) Self { + pub fn empty(allocator: Allocator) Self { return Self{ .elements = &[_]Tuple{}, - .allocator = ctx.allocator, - .ctx = ctx, + .allocator = allocator, }; } @@ -190,7 +109,12 @@ pub fn Relation(comptime Tuple: type) type { return self.elements.len == 0; } - /// Merges this relation with another relation. + /// Merges this relation with another. + /// + /// **Destructive:** `merge` consumes both `self` and `other`. After + /// this call both input relations are left in an empty state; their + /// storage is either reused or freed. Only the returned `Relation` is + /// valid to keep using. pub fn merge(self: *Self, other: *Self) Allocator.Error!Self { if (self.elements.len == 0) { const result = other.*; @@ -258,14 +182,12 @@ pub fn Relation(comptime Tuple: type) type { return Self{ .elements = shrunk, .allocator = self.allocator, - .ctx = self.ctx, }; } return Self{ .elements = merged, .allocator = self.allocator, - .ctx = self.ctx, }; } @@ -408,12 +330,12 @@ pub fn Relation(comptime Tuple: type) type { } /// Loads a relation from a reader. - pub fn load(ctx: *ExecutionContext, reader: anytype) !Self { - return loadWithLimit(ctx, reader, std.math.maxInt(usize)); + pub fn load(allocator: Allocator, reader: anytype) !Self { + return loadWithLimit(allocator, reader, std.math.maxInt(usize)); } /// Loads a relation from a reader with a limit on the number of elements. - pub fn loadWithLimit(ctx: *ExecutionContext, reader: anytype, max_len: usize) !Self { + pub fn loadWithLimit(allocator: Allocator, reader: anytype, max_len: usize) !Self { if (!isSerializableType(Tuple)) return error.UnsupportedType; const magic = try reader.takeArray(7); if (!std.mem.eql(u8, magic, "ZODDREL")) { @@ -427,14 +349,14 @@ pub fn Relation(comptime Tuple: type) type { const length_u64 = try reader.takeInt(u64, .little); const length = std.math.cast(usize, length_u64) orelse return error.InvalidFormat; if (length == 0) { - return Self.empty(ctx); + return Self.empty(allocator); } if (length > max_len) { return error.TooLarge; } - const elements = try ctx.allocator.alloc(Tuple, length); - errdefer ctx.allocator.free(elements); + const elements = try allocator.alloc(Tuple, length); + errdefer allocator.free(elements); var i: usize = 0; while (i < length) : (i += 1) { @@ -445,18 +367,16 @@ pub fn Relation(comptime Tuple: type) type { const unique_len = deduplicate(elements); if (unique_len < elements.len) { - const shrunk = try shrinkOrCopy(Tuple, ctx.allocator, elements, unique_len); + const shrunk = try shrinkOrCopy(Tuple, allocator, elements, unique_len); return Self{ .elements = shrunk, - .allocator = ctx.allocator, - .ctx = ctx, + .allocator = allocator, }; } return Self{ .elements = elements, - .allocator = ctx.allocator, - .ctx = ctx, + .allocator = allocator, }; } }; @@ -464,8 +384,7 @@ pub fn Relation(comptime Tuple: type) type { test "Relation: empty" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var rel = Relation(u32).empty(&ctx); + var rel = Relation(u32).empty(allocator); defer rel.deinit(); try std.testing.expect(rel.isEmpty()); @@ -474,10 +393,9 @@ test "Relation: empty" { test "Relation: persistence" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var original = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var original = try Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 }, @@ -490,7 +408,7 @@ test "Relation: persistence" { try original.save(&aw.writer); var reader = std.Io.Reader.fixed(aw.writer.buffered()); - var loaded = try Relation(Tuple).load(&ctx, &reader); + var loaded = try Relation(Tuple).load(allocator, &reader); defer loaded.deinit(); try std.testing.expectEqual(original.len(), loaded.len()); @@ -499,10 +417,9 @@ test "Relation: persistence" { test "Relation: fromSlice sorts and deduplicates" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const input = [_]u32{ 5, 3, 3, 1, 5, 2, 1 }; - var rel = try Relation(u32).fromSlice(&ctx, &input); + var rel = try Relation(u32).fromSlice(allocator, &input); defer rel.deinit(); try std.testing.expectEqual(@as(usize, 4), rel.len()); @@ -511,7 +428,6 @@ test "Relation: fromSlice sorts and deduplicates" { test "Relation: tuple type" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; const input = [_]Tuple{ .{ 2, 1 }, @@ -520,7 +436,7 @@ test "Relation: tuple type" { .{ 1, 1 }, }; - var rel = try Relation(Tuple).fromSlice(&ctx, &input); + var rel = try Relation(Tuple).fromSlice(allocator, &input); defer rel.deinit(); try std.testing.expectEqual(@as(usize, 3), rel.len()); @@ -531,10 +447,9 @@ test "Relation: tuple type" { test "Relation: merge" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var rel1 = try Relation(u32).fromSlice(&ctx, &[_]u32{ 1, 3, 5 }); - var rel2 = try Relation(u32).fromSlice(&ctx, &[_]u32{ 2, 3, 4 }); + var rel1 = try Relation(u32).fromSlice(allocator, &[_]u32{ 1, 3, 5 }); + var rel2 = try Relation(u32).fromSlice(allocator, &[_]u32{ 2, 3, 4 }); var merged = try rel1.merge(&rel2); defer merged.deinit(); @@ -545,7 +460,6 @@ test "Relation: merge" { test "Relation: load normalizes order" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; var aw: std.Io.Writer.Allocating = .init(allocator); @@ -566,7 +480,7 @@ test "Relation: load normalizes order" { } var reader = std.Io.Reader.fixed(aw.writer.buffered()); - var rel = try Relation(Tuple).load(&ctx, &reader); + var rel = try Relation(Tuple).load(allocator, &reader); defer rel.deinit(); try std.testing.expectEqual(@as(usize, 2), rel.len()); @@ -576,7 +490,6 @@ test "Relation: load normalizes order" { test "Relation: loadWithLimit zero length with zero limit" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); var aw: std.Io.Writer.Allocating = .init(allocator); defer aw.deinit(); @@ -587,7 +500,7 @@ test "Relation: loadWithLimit zero length with zero limit" { try writer.writeInt(u64, 0, .little); var reader = std.Io.Reader.fixed(aw.writer.buffered()); - var rel = try Relation(u32).loadWithLimit(&ctx, &reader, 0); + var rel = try Relation(u32).loadWithLimit(allocator, &reader, 0); defer rel.deinit(); try std.testing.expectEqual(@as(usize, 0), rel.len()); @@ -595,9 +508,8 @@ test "Relation: loadWithLimit zero length with zero limit" { test "Relation: scalar save and load" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var original = try Relation(u32).fromSlice(&ctx, &[_]u32{ 3, 1, 2, 2 }); + var original = try Relation(u32).fromSlice(allocator, &[_]u32{ 3, 1, 2, 2 }); defer original.deinit(); var aw: std.Io.Writer.Allocating = .init(allocator); @@ -606,48 +518,18 @@ test "Relation: scalar save and load" { try original.save(&aw.writer); var reader = std.Io.Reader.fixed(aw.writer.buffered()); - var loaded = try Relation(u32).load(&ctx, &reader); + var loaded = try Relation(u32).load(allocator, &reader); defer loaded.deinit(); try std.testing.expectEqual(original.len(), loaded.len()); try std.testing.expectEqualSlices(u32, original.elements, loaded.elements); } -test "Relation: fromSlice parallel copy" { - const allocator = std.testing.allocator; - var ctx = try ExecutionContext.initWithThreads(allocator, 2); - defer ctx.deinit(); - - const input = [_]u32{ 5, 3, 3, 1, 5, 2, 1 }; - - var rel = try Relation(u32).fromSlice(&ctx, &input); - defer rel.deinit(); - - try std.testing.expectEqual(@as(usize, 4), rel.len()); - try std.testing.expectEqualSlices(u32, &[_]u32{ 1, 2, 3, 5 }, rel.elements); -} - -test "Relation: merge parallel copy" { - const allocator = std.testing.allocator; - var ctx = try ExecutionContext.initWithThreads(allocator, 2); - defer ctx.deinit(); - - var rel1 = try Relation(u32).fromSlice(&ctx, &[_]u32{ 1, 3, 5 }); - var rel2 = try Relation(u32).fromSlice(&ctx, &[_]u32{ 2, 3, 4 }); - - var merged = try rel1.merge(&rel2); - defer merged.deinit(); - - try std.testing.expectEqual(@as(usize, 5), merged.len()); - try std.testing.expectEqualSlices(u32, &[_]u32{ 1, 2, 3, 4, 5 }, merged.elements); -} - test "Relation: save/load unsupported type" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Bad = struct { *u8 }; - var rel = Relation(Bad).empty(&ctx); + var rel = Relation(Bad).empty(allocator); defer rel.deinit(); var aw: std.Io.Writer.Allocating = .init(allocator); @@ -663,7 +545,7 @@ test "Relation: save/load unsupported type" { const used = header_writer.end; var reader_fbs = std.Io.Reader.fixed(header[0..used]); - try std.testing.expectError(error.UnsupportedType, Relation(Bad).load(&ctx, &reader_fbs)); + try std.testing.expectError(error.UnsupportedType, Relation(Bad).load(allocator, &reader_fbs)); } test "shrinkOrCopy: no-op when new_len equals current length" { @@ -737,10 +619,9 @@ test "shrinkOrCopy: copy fallback when remap is rejected" { test "Relation: save/load round-trip for signed ints" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { i32, i32 }; - var original = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var original = try Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ -7, 42 }, .{ -1, 0 }, .{ 100, -100 }, @@ -752,7 +633,7 @@ test "Relation: save/load round-trip for signed ints" { try original.save(&aw.writer); var reader = std.Io.Reader.fixed(aw.writer.buffered()); - var loaded = try Relation(Tuple).load(&ctx, &reader); + var loaded = try Relation(Tuple).load(allocator, &reader); defer loaded.deinit(); try std.testing.expectEqualSlices(Tuple, original.elements, loaded.elements); @@ -760,10 +641,9 @@ test "Relation: save/load round-trip for signed ints" { test "Relation: save/load round-trip for floats" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u32, f64 }; - var original = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var original = try Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ 1, 3.14 }, .{ 2, -0.5 }, .{ 3, 1.0e20 }, @@ -775,7 +655,7 @@ test "Relation: save/load round-trip for floats" { try original.save(&aw.writer); var reader = std.Io.Reader.fixed(aw.writer.buffered()); - var loaded = try Relation(Tuple).load(&ctx, &reader); + var loaded = try Relation(Tuple).load(allocator, &reader); defer loaded.deinit(); try std.testing.expectEqualSlices(Tuple, original.elements, loaded.elements); @@ -783,10 +663,9 @@ test "Relation: save/load round-trip for floats" { test "Relation: save/load round-trip for differently-sized ints" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); const Tuple = struct { u8, u64 }; - var original = try Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var original = try Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ 0, std.math.maxInt(u64) }, .{ 255, 0 }, .{ 127, 12345 }, @@ -798,7 +677,7 @@ test "Relation: save/load round-trip for differently-sized ints" { try original.save(&aw.writer); var reader = std.Io.Reader.fixed(aw.writer.buffered()); - var loaded = try Relation(Tuple).load(&ctx, &reader); + var loaded = try Relation(Tuple).load(allocator, &reader); defer loaded.deinit(); try std.testing.expectEqualSlices(Tuple, original.elements, loaded.elements); diff --git a/src/zodd/variable.zig b/src/zodd/variable.zig index 6507709..9530185 100644 --- a/src/zodd/variable.zig +++ b/src/zodd/variable.zig @@ -13,12 +13,12 @@ //! ## Usage //! //! ```zig -//! var v = try Variable(Edge).init(ctx, &initial_edges); +//! var v = Variable(Edge).init(allocator); //! defer v.deinit(); //! +//! try v.insertSlice(initial_edges); //! while (try v.changed()) { -//! // Join logic here, populating v.next -//! try v.insert(new_facts); +//! // Join logic here, populating v via `insert` / `insertSlice`. //! } //! ``` @@ -26,7 +26,6 @@ const std = @import("std"); const Allocator = std.mem.Allocator; const Relation = @import("relation.zig").Relation; const shrinkOrCopy = @import("relation.zig").shrinkOrCopy; -const ExecutionContext = @import("context.zig").ExecutionContext; pub fn Variable(comptime Tuple: type) type { return struct { @@ -42,21 +41,14 @@ pub fn Variable(comptime Tuple: type) type { to_add: RelList, /// The allocator for internal structures. allocator: Allocator, - /// The execution context. - ctx: *ExecutionContext, /// Initializes a new variable. - /// - /// Arguments: - /// - `ctx`: The execution context. - /// - `initial_data`: (Optional) The initial relation. - pub fn init(ctx: *ExecutionContext) Self { + pub fn init(allocator: Allocator) Self { return Self{ .stable = RelList.empty, - .recent = Rel.empty(ctx), + .recent = Rel.empty(allocator), .to_add = RelList.empty, - .allocator = ctx.allocator, - .ctx = ctx, + .allocator = allocator, }; } @@ -75,14 +67,16 @@ pub fn Variable(comptime Tuple: type) type { self.to_add.deinit(self.allocator); } - /// Inserts a relation into the variable. + /// Inserts a relation into the variable. The variable takes ownership + /// of the relation's storage; do not `deinit` it after calling this. pub fn insert(self: *Self, relation: Rel) Allocator.Error!void { try self.to_add.append(self.allocator, relation); } - /// Inserts a slice of tuples into the variable. - pub fn insertSlice(self: *Self, ctx: *ExecutionContext, tuples: []const Tuple) Allocator.Error!void { - const rel = try Rel.fromSlice(ctx, tuples); + /// Inserts a slice of tuples into the variable. The tuples are copied; + /// the caller retains ownership of `tuples`. + pub fn insertSlice(self: *Self, tuples: []const Tuple) Allocator.Error!void { + const rel = try Rel.fromSlice(self.allocator, tuples); try self.insert(rel); } @@ -90,7 +84,7 @@ pub fn Variable(comptime Tuple: type) type { pub fn changed(self: *Self) Allocator.Error!bool { if (!self.recent.isEmpty()) { var recent = self.recent; - self.recent = Rel.empty(self.ctx); + self.recent = Rel.empty(self.allocator); // `recent` now owns the tuples. If anything below fails we // must free them; on success we transfer ownership to // `self.stable` and null out `recent`. @@ -108,7 +102,7 @@ pub fn Variable(comptime Tuple: type) type { } try self.stable.append(self.allocator, recent); - recent = Rel.empty(self.ctx); + recent = Rel.empty(self.allocator); } if (self.to_add.items.len > 0) { @@ -126,7 +120,7 @@ pub fn Variable(comptime Tuple: type) type { } self.recent = to_add; - to_add = Rel.empty(self.ctx); + to_add = Rel.empty(self.allocator); } return !self.recent.isEmpty(); @@ -153,7 +147,7 @@ pub fn Variable(comptime Tuple: type) type { if (write_idx < target.elements.len) { if (write_idx == 0) { target.deinit(); - target.* = Rel.empty(self.ctx); + target.* = Rel.empty(self.allocator); } else { target.elements = try shrinkOrCopy(Tuple, self.allocator, target.elements, write_idx); } @@ -173,10 +167,15 @@ pub fn Variable(comptime Tuple: type) type { } /// Completes the variable and returns the final relation. + /// + /// **Destructive:** `complete` consumes the variable's internal + /// batches and returns a single merged `Relation`. After this call + /// the variable is left empty and should not be used again, other + /// than to `deinit` (which remains safe). pub fn complete(self: *Self) Allocator.Error!Rel { if (!self.recent.isEmpty()) { try self.stable.append(self.allocator, self.recent); - self.recent = Rel.empty(self.ctx); + self.recent = Rel.empty(self.allocator); } if (self.to_add.items.len > 0) { @@ -189,11 +188,11 @@ pub fn Variable(comptime Tuple: type) type { to_add = try to_add.merge(&more); } try self.stable.append(self.allocator, to_add); - to_add = Rel.empty(self.ctx); + to_add = Rel.empty(self.allocator); } if (self.stable.items.len == 0) { - return Rel.empty(self.ctx); + return Rel.empty(self.allocator); } var result = self.stable.pop().?; @@ -251,12 +250,11 @@ pub fn gallop(comptime T: type, slice: []const T, target: T) []const T { test "Variable: basic lifecycle" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var v = Variable(u32).init(&ctx); + var v = Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3 }); + try v.insertSlice(&[_]u32{ 1, 2, 3 }); const changed1 = try v.changed(); try std.testing.expect(changed1); @@ -270,15 +268,14 @@ test "Variable: basic lifecycle" { test "Variable: deduplication across rounds" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var v = Variable(u32).init(&ctx); + var v = Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3 }); + try v.insertSlice(&[_]u32{ 1, 2, 3 }); _ = try v.changed(); - try v.insertSlice(&ctx, &[_]u32{ 2, 3, 4, 5 }); + try v.insertSlice(&[_]u32{ 2, 3, 4, 5 }); const changed = try v.changed(); try std.testing.expect(changed); @@ -287,15 +284,14 @@ test "Variable: deduplication across rounds" { test "Variable: complete" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var v = Variable(u32).init(&ctx); + var v = Variable(u32).init(allocator); - try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3 }); + try v.insertSlice(&[_]u32{ 1, 2, 3 }); _ = try v.changed(); _ = try v.changed(); - try v.insertSlice(&ctx, &[_]u32{ 4, 5 }); + try v.insertSlice(&[_]u32{ 4, 5 }); _ = try v.changed(); _ = try v.changed(); @@ -308,15 +304,14 @@ test "Variable: complete" { test "Variable: totalLen" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var v = Variable(u32).init(&ctx); + var v = Variable(u32).init(allocator); defer v.deinit(); // Init: 0 try std.testing.expectEqual(@as(usize, 0), v.totalLen()); // Insert to_add: 3 items - try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3 }); + try v.insertSlice(&[_]u32{ 1, 2, 3 }); try std.testing.expectEqual(@as(usize, 3), v.totalLen()); // Changed: recent=3, stable=0, to_add=0 (moved to recent) @@ -329,7 +324,7 @@ test "Variable: totalLen" { try std.testing.expectEqual(@as(usize, 3), v.totalLen()); // Add more - try v.insertSlice(&ctx, &[_]u32{4}); + try v.insertSlice(&[_]u32{4}); try std.testing.expectEqual(@as(usize, 4), v.totalLen()); } @@ -370,16 +365,15 @@ test "gallop: target beyond saturated step" { test "Variable: changed filters against stable batches" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var v = Variable(u32).init(&ctx); + var v = Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3, 4, 5, 6, 7, 8 }); + try v.insertSlice(&[_]u32{ 1, 2, 3, 4, 5, 6, 7, 8 }); _ = try v.changed(); _ = try v.changed(); - try v.insertSlice(&ctx, &[_]u32{ 2, 4, 6, 8, 9 }); + try v.insertSlice(&[_]u32{ 2, 4, 6, 8, 9 }); const changed = try v.changed(); try std.testing.expect(changed); @@ -389,15 +383,14 @@ test "Variable: changed filters against stable batches" { test "Variable: changed with recent and to_add" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var v = Variable(u32).init(&ctx); + var v = Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); + try v.insertSlice(&[_]u32{ 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }); _ = try v.changed(); - try v.insertSlice(&ctx, &[_]u32{ 3, 5, 11, 12 }); + try v.insertSlice(&[_]u32{ 3, 5, 11, 12 }); const changed = try v.changed(); try std.testing.expect(changed); @@ -408,12 +401,11 @@ test "Variable: changed with recent and to_add" { test "Variable: insertSlice with empty slice is a no-op" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var v = Variable(u32).init(&ctx); + var v = Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, &[_]u32{}); + try v.insertSlice(&[_]u32{}); // The empty insert lands as an empty Relation on the to_add queue; it // should merely vanish through changed() without error or leaks. @@ -424,12 +416,11 @@ test "Variable: insertSlice with empty slice is a no-op" { test "Variable: changed returns false once no new facts arrive" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var v = Variable(u32).init(&ctx); + var v = Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3 }); + try v.insertSlice(&[_]u32{ 1, 2, 3 }); try std.testing.expect(try v.changed()); try std.testing.expect(!try v.changed()); try std.testing.expect(!try v.changed()); @@ -437,14 +428,13 @@ test "Variable: changed returns false once no new facts arrive" { test "Variable: changed merges multiple to_add batches into recent" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var v = Variable(u32).init(&ctx); + var v = Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, &[_]u32{ 1, 3, 5 }); - try v.insertSlice(&ctx, &[_]u32{ 2, 4, 6 }); - try v.insertSlice(&ctx, &[_]u32{ 5, 7 }); + try v.insertSlice(&[_]u32{ 1, 3, 5 }); + try v.insertSlice(&[_]u32{ 2, 4, 6 }); + try v.insertSlice(&[_]u32{ 5, 7 }); try std.testing.expect(try v.changed()); try std.testing.expectEqual(@as(usize, 7), v.recent.len()); @@ -453,13 +443,12 @@ test "Variable: changed merges multiple to_add batches into recent" { test "Variable: complete folds to_add when nothing has been processed yet" { const allocator = std.testing.allocator; - var ctx = ExecutionContext.init(allocator); - var v = Variable(u32).init(&ctx); + var v = Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, &[_]u32{ 2, 1 }); - try v.insertSlice(&ctx, &[_]u32{ 3, 1 }); + try v.insertSlice(&[_]u32{ 2, 1 }); + try v.insertSlice(&[_]u32{ 3, 1 }); var result = try v.complete(); defer result.deinit(); diff --git a/tests/incremental_tests.zig b/tests/incremental_tests.zig index 9623c38..f0eb728 100644 --- a/tests/incremental_tests.zig +++ b/tests/incremental_tests.zig @@ -4,33 +4,32 @@ const zodd = @import("zodd"); test "incremental maintenance: monotonic updates" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var iter = zodd.Iteration(Tuple).init(&ctx, 100); + var iter = zodd.Iteration(Tuple).init(allocator, 100); defer iter.deinit(); const B = try iter.variable(); const A = try iter.variable(); - try B.insertSlice(&ctx, &[_]Tuple{.{ 1, 2 }}); + try B.insertSlice(&[_]Tuple{.{ 1, 2 }}); while (try iter.changed()) { if (B.recent.len() > 0) { - const rel = try zodd.Relation(Tuple).fromSlice(&ctx, B.recent.elements); + const rel = try zodd.Relation(Tuple).fromSlice(allocator, B.recent.elements); try A.insert(rel); } } try testing.expectEqual(@as(usize, 1), A.totalLen()); - try B.insertSlice(&ctx, &[_]Tuple{.{ 2, 3 }}); + try B.insertSlice(&[_]Tuple{.{ 2, 3 }}); iter.reset(); while (try iter.changed()) { if (B.recent.len() > 0) { - const rel = try zodd.Relation(Tuple).fromSlice(&ctx, B.recent.elements); + const rel = try zodd.Relation(Tuple).fromSlice(allocator, B.recent.elements); try A.insert(rel); } } @@ -47,24 +46,23 @@ test "incremental maintenance: monotonic updates" { test "incremental maintenance: join with new data after reset" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const KV = struct { u32, u32 }; const Out = struct { u32, u32, u32 }; - var iter = zodd.Iteration(KV).init(&ctx, 100); + var iter = zodd.Iteration(KV).init(allocator, 100); defer iter.deinit(); const edges = try iter.variable(); const labels = try iter.variable(); - var joined = zodd.Variable(Out).init(&ctx); + var joined = zodd.Variable(Out).init(allocator); defer joined.deinit(); // Round 1: edges={1->2}, labels={1->100} - try edges.insertSlice(&ctx, &[_]KV{.{ 1, 2 }}); - try labels.insertSlice(&ctx, &[_]KV{.{ 1, 100 }}); + try edges.insertSlice(&[_]KV{.{ 1, 2 }}); + try labels.insertSlice(&[_]KV{.{ 1, 100 }}); while (try iter.changed()) { - try zodd.joinInto(u32, u32, u32, Out, &ctx, edges, labels, &joined, struct { + try zodd.joinInto(u32, u32, u32, Out, edges, labels, &joined, struct { fn logic(key: *const u32, edge_val: *const u32, label_val: *const u32) Out { return .{ key.*, edge_val.*, label_val.* }; } @@ -74,12 +72,12 @@ test "incremental maintenance: join with new data after reset" { try testing.expectEqual(@as(usize, 1), joined.totalLen()); // Round 2: add edge 2->3 and label 2->200 - try edges.insertSlice(&ctx, &[_]KV{.{ 2, 3 }}); - try labels.insertSlice(&ctx, &[_]KV{.{ 2, 200 }}); + try edges.insertSlice(&[_]KV{.{ 2, 3 }}); + try labels.insertSlice(&[_]KV{.{ 2, 200 }}); iter.reset(); while (try iter.changed()) { - try zodd.joinInto(u32, u32, u32, Out, &ctx, edges, labels, &joined, struct { + try zodd.joinInto(u32, u32, u32, Out, edges, labels, &joined, struct { fn logic(key: *const u32, edge_val: *const u32, label_val: *const u32) Out { return .{ key.*, edge_val.*, label_val.* }; } @@ -92,21 +90,20 @@ test "incremental maintenance: join with new data after reset" { test "incremental maintenance: transitive closure re-convergence" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Edge = struct { u32, u32 }; const EdgeList = std.ArrayListUnmanaged(Edge); // Phase 1: edges 1->2, 2->3 - var edges = try zodd.Relation(Edge).fromSlice(&ctx, &[_]Edge{ + var edges = try zodd.Relation(Edge).fromSlice(allocator, &[_]Edge{ .{ 1, 2 }, .{ 2, 3 }, }); defer edges.deinit(); - var reachable = zodd.Variable(Edge).init(&ctx); + var reachable = zodd.Variable(Edge).init(allocator); defer reachable.deinit(); - try reachable.insertSlice(&ctx, edges.elements); + try reachable.insertSlice(edges.elements); var iters: usize = 0; while (try reachable.changed()) { @@ -119,7 +116,7 @@ test "incremental maintenance: transitive closure re-convergence" { } } if (new.items.len > 0) { - try reachable.insert(try zodd.Relation(Edge).fromSlice(&ctx, new.items)); + try reachable.insert(try zodd.Relation(Edge).fromSlice(allocator, new.items)); } iters += 1; if (iters > 10) break; @@ -129,14 +126,14 @@ test "incremental maintenance: transitive closure re-convergence" { try testing.expectEqual(@as(usize, 3), reachable.totalLen()); // Phase 2: add edge 3->4 - var edges2 = try zodd.Relation(Edge).fromSlice(&ctx, &[_]Edge{ + var edges2 = try zodd.Relation(Edge).fromSlice(allocator, &[_]Edge{ .{ 1, 2 }, .{ 2, 3 }, .{ 3, 4 }, }); defer edges2.deinit(); - try reachable.insertSlice(&ctx, &[_]Edge{.{ 3, 4 }}); + try reachable.insertSlice(&[_]Edge{.{ 3, 4 }}); iters = 0; while (try reachable.changed()) { @@ -156,7 +153,7 @@ test "incremental maintenance: transitive closure re-convergence" { } } if (new.items.len > 0) { - try reachable.insert(try zodd.Relation(Edge).fromSlice(&ctx, new.items)); + try reachable.insert(try zodd.Relation(Edge).fromSlice(allocator, new.items)); } iters += 1; if (iters > 10) break; @@ -168,16 +165,15 @@ test "incremental maintenance: transitive closure re-convergence" { test "incremental maintenance: iteration reset with multiple variables" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); - var iter = zodd.Iteration(u32).init(&ctx, 50); + var iter = zodd.Iteration(u32).init(allocator, 50); defer iter.deinit(); const v1 = try iter.variable(); const v2 = try iter.variable(); - try v1.insertSlice(&ctx, &[_]u32{ 10, 20 }); - try v2.insertSlice(&ctx, &[_]u32{ 30, 40 }); + try v1.insertSlice(&[_]u32{ 10, 20 }); + try v2.insertSlice(&[_]u32{ 30, 40 }); // Converge while (try iter.changed()) {} @@ -187,7 +183,7 @@ test "incremental maintenance: iteration reset with multiple variables" { // Reset and add more data iter.reset(); - try v1.insertSlice(&ctx, &[_]u32{ 50, 60 }); + try v1.insertSlice(&[_]u32{ 50, 60 }); const changed = try iter.changed(); try testing.expect(changed); diff --git a/tests/integration_tests.zig b/tests/integration_tests.zig index 47727f8..9770037 100644 --- a/tests/integration_tests.zig +++ b/tests/integration_tests.zig @@ -4,21 +4,20 @@ const zodd = @import("zodd"); test "transitive closure: linear chain" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Edge = struct { u32, u32 }; const EdgeList = std.ArrayListUnmanaged(Edge); - var edges = try zodd.Relation(Edge).fromSlice(&ctx, &[_]Edge{ + var edges = try zodd.Relation(Edge).fromSlice(allocator, &[_]Edge{ .{ 1, 2 }, .{ 2, 3 }, .{ 3, 4 }, }); defer edges.deinit(); - var reachable = zodd.Variable(Edge).init(&ctx); + var reachable = zodd.Variable(Edge).init(allocator); defer reachable.deinit(); - try reachable.insertSlice(&ctx, edges.elements); + try reachable.insertSlice(edges.elements); var iters: usize = 0; while (try reachable.changed()) : (iters += 1) { @@ -34,7 +33,7 @@ test "transitive closure: linear chain" { } if (results.items.len > 0) { - try reachable.insert(try zodd.Relation(Edge).fromSlice(&ctx, results.items)); + try reachable.insert(try zodd.Relation(Edge).fromSlice(allocator, results.items)); } if (iters > 10) break; @@ -48,11 +47,10 @@ test "transitive closure: linear chain" { test "transitive closure: diamond graph" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Edge = struct { u32, u32 }; const EdgeList = std.ArrayListUnmanaged(Edge); - var edges = try zodd.Relation(Edge).fromSlice(&ctx, &[_]Edge{ + var edges = try zodd.Relation(Edge).fromSlice(allocator, &[_]Edge{ .{ 1, 2 }, .{ 1, 3 }, .{ 2, 4 }, @@ -60,10 +58,10 @@ test "transitive closure: diamond graph" { }); defer edges.deinit(); - var reachable = zodd.Variable(Edge).init(&ctx); + var reachable = zodd.Variable(Edge).init(allocator); defer reachable.deinit(); - try reachable.insertSlice(&ctx, edges.elements); + try reachable.insertSlice(edges.elements); var iters: usize = 0; while (try reachable.changed()) : (iters += 1) { @@ -79,7 +77,7 @@ test "transitive closure: diamond graph" { } if (results.items.len > 0) { - try reachable.insert(try zodd.Relation(Edge).fromSlice(&ctx, results.items)); + try reachable.insert(try zodd.Relation(Edge).fromSlice(allocator, results.items)); } if (iters > 10) break; @@ -93,21 +91,20 @@ test "transitive closure: diamond graph" { test "transitive closure: cycle detection" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Edge = struct { u32, u32 }; const EdgeList = std.ArrayListUnmanaged(Edge); - var edges = try zodd.Relation(Edge).fromSlice(&ctx, &[_]Edge{ + var edges = try zodd.Relation(Edge).fromSlice(allocator, &[_]Edge{ .{ 1, 2 }, .{ 2, 3 }, .{ 3, 1 }, }); defer edges.deinit(); - var reachable = zodd.Variable(Edge).init(&ctx); + var reachable = zodd.Variable(Edge).init(allocator); defer reachable.deinit(); - try reachable.insertSlice(&ctx, edges.elements); + try reachable.insertSlice(edges.elements); var iters: usize = 0; while (try reachable.changed()) : (iters += 1) { @@ -123,7 +120,7 @@ test "transitive closure: cycle detection" { } if (results.items.len > 0) { - try reachable.insert(try zodd.Relation(Edge).fromSlice(&ctx, results.items)); + try reachable.insert(try zodd.Relation(Edge).fromSlice(allocator, results.items)); } if (iters > 20) break; @@ -137,11 +134,10 @@ test "transitive closure: cycle detection" { test "same generation: parent-child hierarchy" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Pair = struct { u32, u32 }; const PairList = std.ArrayListUnmanaged(Pair); - var parent_child = try zodd.Relation(Pair).fromSlice(&ctx, &[_]Pair{ + var parent_child = try zodd.Relation(Pair).fromSlice(allocator, &[_]Pair{ .{ 1, 2 }, .{ 1, 3 }, .{ 2, 4 }, @@ -149,10 +145,10 @@ test "same generation: parent-child hierarchy" { }); defer parent_child.deinit(); - var same_gen = zodd.Variable(Pair).init(&ctx); + var same_gen = zodd.Variable(Pair).init(allocator); defer same_gen.deinit(); - try same_gen.insertSlice(&ctx, &[_]Pair{ .{ 1, 1 }, .{ 2, 2 }, .{ 3, 3 }, .{ 4, 4 }, .{ 5, 5 } }); + try same_gen.insertSlice(&[_]Pair{ .{ 1, 1 }, .{ 2, 2 }, .{ 3, 3 }, .{ 4, 4 }, .{ 5, 5 } }); var iters: usize = 0; while (try same_gen.changed()) : (iters += 1) { @@ -175,7 +171,7 @@ test "same generation: parent-child hierarchy" { } if (results.items.len > 0) { - try same_gen.insert(try zodd.Relation(Pair).fromSlice(&ctx, results.items)); + try same_gen.insert(try zodd.Relation(Pair).fromSlice(allocator, results.items)); } if (iters > 10) break; @@ -189,10 +185,9 @@ test "same generation: parent-child hierarchy" { test "aggregate: group sum integration" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var rel = try zodd.Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var rel = try zodd.Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ 1, 10 }, .{ 1, 20 }, .{ 2, 5 }, @@ -210,7 +205,7 @@ test "aggregate: group sum integration" { } }; - var result = try zodd.aggregate.aggregate(Tuple, u32, u32, &ctx, &rel, key_func.key, 0, folder.fold); + var result = try zodd.aggregate(Tuple, u32, u32, allocator, &rel, key_func.key, 0, folder.fold); defer result.deinit(); try testing.expectEqual(@as(usize, 2), result.len()); @@ -222,26 +217,25 @@ test "aggregate: group sum integration" { test "joinInto: incremental updates integration" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; const Out = struct { u32, u32, u32 }; - var v1 = zodd.Variable(Tuple).init(&ctx); + var v1 = zodd.Variable(Tuple).init(allocator); defer v1.deinit(); - var v2 = zodd.Variable(Tuple).init(&ctx); + var v2 = zodd.Variable(Tuple).init(allocator); defer v2.deinit(); - var out = zodd.Variable(Out).init(&ctx); + var out = zodd.Variable(Out).init(allocator); defer out.deinit(); - try v1.insertSlice(&ctx, &[_]Tuple{.{ 1, 10 }}); - try v2.insertSlice(&ctx, &[_]Tuple{ .{ 1, 100 }, .{ 2, 200 } }); + try v1.insertSlice(&[_]Tuple{.{ 1, 10 }}); + try v2.insertSlice(&[_]Tuple{ .{ 1, 100 }, .{ 2, 200 } }); _ = try v1.changed(); _ = try v2.changed(); - try zodd.joinInto(u32, u32, u32, Out, &ctx, &v1, &v2, &out, struct { + try zodd.joinInto(u32, u32, u32, Out, &v1, &v2, &out, struct { fn logic(key: *const u32, v1_val: *const u32, v2_val: *const u32) Out { return .{ key.*, v1_val.*, v2_val.* }; } @@ -254,10 +248,10 @@ test "joinInto: incremental updates integration" { _ = try v2.changed(); _ = try out.changed(); - try v2.insertSlice(&ctx, &[_]Tuple{.{ 1, 101 }}); + try v2.insertSlice(&[_]Tuple{.{ 1, 101 }}); _ = try v2.changed(); - try zodd.joinInto(u32, u32, u32, Out, &ctx, &v1, &v2, &out, struct { + try zodd.joinInto(u32, u32, u32, Out, &v1, &v2, &out, struct { fn logic(key: *const u32, v1_val: *const u32, v2_val: *const u32) Out { return .{ key.*, v1_val.*, v2_val.* }; } @@ -270,39 +264,38 @@ test "joinInto: incremental updates integration" { test "extendInto: extend and anti integration" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32 }; const Val = u32; const Out = struct { u32, u32 }; - var source = zodd.Variable(Tuple).init(&ctx); + var source = zodd.Variable(Tuple).init(allocator); defer source.deinit(); - try source.insertSlice(&ctx, &[_]Tuple{ .{1}, .{2} }); + try source.insertSlice(&[_]Tuple{ .{1}, .{2} }); _ = try source.changed(); - var allow = try zodd.Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var allow = try zodd.Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 1, 10 }, .{ 1, 20 }, .{ 2, 30 }, }); defer allow.deinit(); - var block = try zodd.Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var block = try zodd.Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 1, 10 }, }); defer block.deinit(); - var output = zodd.Variable(Out).init(&ctx); + var output = zodd.Variable(Out).init(allocator); defer output.deinit(); - var ext_allow = zodd.ExtendWith(Tuple, u32, Val).init(&ctx, &allow, struct { + var ext_allow = zodd.ExtendWith(Tuple, u32, Val).init(allocator, &allow, struct { fn f(t: *const Tuple) u32 { return t[0]; } }.f); - var ext_block = zodd.ExtendAnti(Tuple, u32, Val).init(&ctx, &block, struct { + var ext_block = zodd.ExtendAnti(Tuple, u32, Val).init(allocator, &block, struct { fn f(t: *const Tuple) u32 { return t[0]; } @@ -310,7 +303,7 @@ test "extendInto: extend and anti integration" { var leapers = [_]zodd.Leaper(Tuple, Val){ ext_allow.leaper(), ext_block.leaper() }; - try zodd.extendInto(Tuple, Val, Out, &ctx, &source, &leapers, &output, struct { + try zodd.extendInto(Tuple, Val, Out, &source, &leapers, &output, struct { fn logic(t: *const Tuple, v: *const Val) Out { return .{ t[0], v.* }; } @@ -324,10 +317,9 @@ test "extendInto: extend and anti integration" { test "SecondaryIndex: getRange randomized integration" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - const Index = zodd.index.SecondaryIndex(Tuple, u32, struct { + const Index = zodd.SecondaryIndex(Tuple, u32, struct { fn extract(t: Tuple) u32 { return t[0]; } @@ -337,7 +329,7 @@ test "SecondaryIndex: getRange randomized integration" { } }.cmp, 4); - var idx = Index.init(&ctx); + var idx = Index.init(allocator); defer idx.deinit(); var all = std.ArrayListUnmanaged(Tuple).empty; @@ -371,7 +363,7 @@ test "SecondaryIndex: getRange randomized integration" { } } - var expected = try zodd.Relation(Tuple).fromSlice(&ctx, expected_list.items); + var expected = try zodd.Relation(Tuple).fromSlice(allocator, expected_list.items); defer expected.deinit(); var got = try idx.getRange(start, end); @@ -383,39 +375,38 @@ test "SecondaryIndex: getRange randomized integration" { test "integration: FilterAnti in extendInto" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32 }; const Val = u32; const Out = struct { u32, u32 }; - var source = zodd.Variable(Tuple).init(&ctx); + var source = zodd.Variable(Tuple).init(allocator); defer source.deinit(); - try source.insertSlice(&ctx, &[_]Tuple{ .{1}, .{2}, .{3} }); + try source.insertSlice(&[_]Tuple{ .{1}, .{2}, .{3} }); _ = try source.changed(); - var rel = try zodd.Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var rel = try zodd.Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 }, }); defer rel.deinit(); - var filter_rel = try zodd.Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var filter_rel = try zodd.Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 2, 999 }, }); defer filter_rel.deinit(); - var output = zodd.Variable(Out).init(&ctx); + var output = zodd.Variable(Out).init(allocator); defer output.deinit(); - var ext = zodd.ExtendWith(Tuple, u32, Val).init(&ctx, &rel, struct { + var ext = zodd.ExtendWith(Tuple, u32, Val).init(allocator, &rel, struct { fn f(t: *const Tuple) u32 { return t[0]; } }.f); - var anti = zodd.FilterAnti(Tuple, u32, u32).init(&ctx, &filter_rel, struct { + var anti = zodd.FilterAnti(Tuple, u32, u32).init(allocator, &filter_rel, struct { fn f(t: *const Tuple) struct { u32, u32 } { return .{ t[0], 999 }; } @@ -423,7 +414,7 @@ test "integration: FilterAnti in extendInto" { var leapers = [_]zodd.Leaper(Tuple, Val){ ext.leaper(), anti.leaper() }; - try zodd.extendInto(Tuple, Val, Out, &ctx, &source, &leapers, &output, struct { + try zodd.extendInto(Tuple, Val, Out, &source, &leapers, &output, struct { fn logic(t: *const Tuple, v: *const Val) Out { return .{ t[0], v.* }; } @@ -438,33 +429,32 @@ test "integration: FilterAnti in extendInto" { test "integration: multi-way intersection" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32 }; const Val = u32; const Out = struct { u32, u32 }; - var source = zodd.Variable(Tuple).init(&ctx); + var source = zodd.Variable(Tuple).init(allocator); defer source.deinit(); - try source.insertSlice(&ctx, &[_]Tuple{ .{1}, .{2}, .{3}, .{4} }); + try source.insertSlice(&[_]Tuple{ .{1}, .{2}, .{3}, .{4} }); _ = try source.changed(); - var r1 = try zodd.Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var r1 = try zodd.Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 1, 100 }, .{ 2, 200 }, .{ 3, 300 }, .{ 4, 400 }, }); defer r1.deinit(); - var r2 = try zodd.Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var r2 = try zodd.Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 1, 100 }, .{ 2, 200 }, .{ 4, 999 }, }); defer r2.deinit(); - var r3 = try zodd.Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var r3 = try zodd.Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 2, 200 }, .{ 3, 300 }, }); defer r3.deinit(); - var output = zodd.Variable(Out).init(&ctx); + var output = zodd.Variable(Out).init(allocator); defer output.deinit(); const KeyFunc = struct { @@ -473,13 +463,13 @@ test "integration: multi-way intersection" { } }; - var ext1 = zodd.ExtendWith(Tuple, u32, Val).init(&ctx, &r1, KeyFunc.f); - var ext2 = zodd.ExtendWith(Tuple, u32, Val).init(&ctx, &r2, KeyFunc.f); - var ext3 = zodd.ExtendWith(Tuple, u32, Val).init(&ctx, &r3, KeyFunc.f); + var ext1 = zodd.ExtendWith(Tuple, u32, Val).init(allocator, &r1, KeyFunc.f); + var ext2 = zodd.ExtendWith(Tuple, u32, Val).init(allocator, &r2, KeyFunc.f); + var ext3 = zodd.ExtendWith(Tuple, u32, Val).init(allocator, &r3, KeyFunc.f); var leapers = [_]zodd.Leaper(Tuple, Val){ ext1.leaper(), ext2.leaper(), ext3.leaper() }; - try zodd.extendInto(Tuple, Val, Out, &ctx, &source, &leapers, &output, struct { + try zodd.extendInto(Tuple, Val, Out, &source, &leapers, &output, struct { fn logic(t: *const Tuple, v: *const Val) Out { return .{ t[0], v.* }; } @@ -493,10 +483,9 @@ test "integration: multi-way intersection" { test "integration: persistence round-trip" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var original = try zodd.Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var original = try zodd.Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 }, }); defer original.deinit(); @@ -507,7 +496,7 @@ test "integration: persistence round-trip" { try original.save(&aw.writer); var reader = std.Io.Reader.fixed(aw.writer.buffered()); - var loaded = try zodd.Relation(Tuple).load(&ctx, &reader); + var loaded = try zodd.Relation(Tuple).load(allocator, &reader); defer loaded.deinit(); try testing.expectEqual(original.len(), loaded.len()); @@ -516,18 +505,17 @@ test "integration: persistence round-trip" { test "integration: empty-input transitive closure" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Edge = struct { u32, u32 }; - var edges = zodd.Relation(Edge).empty(&ctx); + var edges = zodd.Relation(Edge).empty(allocator); defer edges.deinit(); - var reachable = zodd.Variable(Edge).init(&ctx); + var reachable = zodd.Variable(Edge).init(allocator); defer reachable.deinit(); try reachable.insert(edges); - try reachable.insertSlice(&ctx, &[_]Edge{}); + try reachable.insertSlice(&[_]Edge{}); while (try reachable.changed()) { _ = struct { diff --git a/tests/property_tests.zig b/tests/property_tests.zig index a82f9f3..c71a25d 100644 --- a/tests/property_tests.zig +++ b/tests/property_tests.zig @@ -10,8 +10,8 @@ test "property: relation always sorted after fromSlice" { gen.list(u32, gen.intRange(u32, 0, 1000), 0, 50), struct { fn prop(data: []const u32) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var rel = try zodd.Relation(u32).fromSlice(&ctx, data); + const allocator = testing.allocator; + var rel = try zodd.Relation(u32).fromSlice(allocator, data); defer rel.deinit(); if (rel.elements.len > 1) { @@ -31,8 +31,8 @@ test "property: relation always deduplicated after fromSlice" { gen.list(u32, gen.intRange(u32, 0, 50), 0, 30), struct { fn prop(data: []const u32) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var rel = try zodd.Relation(u32).fromSlice(&ctx, data); + const allocator = testing.allocator; + var rel = try zodd.Relation(u32).fromSlice(allocator, data); defer rel.deinit(); if (rel.elements.len > 1) { @@ -60,14 +60,14 @@ test "property: relation merge is commutative" { two_lists_gen, struct { fn prop(lists: TwoLists) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var rel1a = try zodd.Relation(u32).fromSlice(&ctx, lists[0]); - var rel2a = try zodd.Relation(u32).fromSlice(&ctx, lists[1]); + const allocator = testing.allocator; + var rel1a = try zodd.Relation(u32).fromSlice(allocator, lists[0]); + var rel2a = try zodd.Relation(u32).fromSlice(allocator, lists[1]); var merged_ab = try rel1a.merge(&rel2a); defer merged_ab.deinit(); - var rel1b = try zodd.Relation(u32).fromSlice(&ctx, lists[0]); - var rel2b = try zodd.Relation(u32).fromSlice(&ctx, lists[1]); + var rel1b = try zodd.Relation(u32).fromSlice(allocator, lists[0]); + var rel2b = try zodd.Relation(u32).fromSlice(allocator, lists[1]); var merged_ba = try rel2b.merge(&rel1b); defer merged_ba.deinit(); @@ -84,11 +84,11 @@ test "property: variable deduplicates across rounds" { gen.list(u32, gen.intRange(u32, 0, 50), 1, 30), struct { fn prop(data: []const u32) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var v = zodd.Variable(u32).init(&ctx); + const allocator = testing.allocator; + var v = zodd.Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, data); + try v.insertSlice(data); while (try v.changed()) {} var result = try v.complete(); @@ -111,10 +111,10 @@ test "property: variable totalLen matches complete().len" { gen.list(u32, gen.intRange(u32, 0, 100), 1, 30), struct { fn prop(data: []const u32) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var v = zodd.Variable(u32).init(&ctx); + const allocator = testing.allocator; + var v = zodd.Variable(u32).init(allocator); - try v.insertSlice(&ctx, data); + try v.insertSlice(data); while (try v.changed()) {} const total_before = v.totalLen(); @@ -143,14 +143,14 @@ test "property: transitive closure reaches expected nodes" { edges_gen, struct { fn prop(edges: []const Edge) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var edges_rel = try zodd.Relation(Edge).fromSlice(&ctx, edges); + const allocator = testing.allocator; + var edges_rel = try zodd.Relation(Edge).fromSlice(allocator, edges); defer edges_rel.deinit(); - var reachable = zodd.Variable(Edge).init(&ctx); + var reachable = zodd.Variable(Edge).init(allocator); defer reachable.deinit(); - try reachable.insertSlice(&ctx, edges_rel.elements); + try reachable.insertSlice(edges_rel.elements); var iters: usize = 0; const EdgeList = std.ArrayListUnmanaged(Edge); @@ -167,7 +167,7 @@ test "property: transitive closure reaches expected nodes" { } if (results.items.len > 0) { - try reachable.insert(try zodd.Relation(Edge).fromSlice(&ctx, results.items)); + try reachable.insert(try zodd.Relation(Edge).fromSlice(allocator, results.items)); } if (iters > 20) break; @@ -189,15 +189,15 @@ test "property: relation merge is idempotent" { gen.list(u32, gen.intRange(u32, 0, 100), 0, 30), struct { fn prop(data: []const u32) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var rel1 = try zodd.Relation(u32).fromSlice(&ctx, data); + const allocator = testing.allocator; + var rel1 = try zodd.Relation(u32).fromSlice(allocator, data); defer rel1.deinit(); - var rel2 = try zodd.Relation(u32).fromSlice(&ctx, data); + var rel2 = try zodd.Relation(u32).fromSlice(allocator, data); var merged = try rel2.merge(&rel1); defer merged.deinit(); - var expected = try zodd.Relation(u32).fromSlice(&ctx, data); + var expected = try zodd.Relation(u32).fromSlice(allocator, data); defer expected.deinit(); try testing.expectEqualSlices(u32, expected.elements, merged.elements); @@ -221,8 +221,8 @@ test "property: gallop returns suffix at target" { gen_pair, struct { fn prop(input: Pair) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var rel = try zodd.Relation(u32).fromSlice(&ctx, input[0]); + const allocator = testing.allocator; + var rel = try zodd.Relation(u32).fromSlice(allocator, input[0]); defer rel.deinit(); const target = input[1]; @@ -262,18 +262,18 @@ test "property: relation merge is associative" { lists_gen, struct { fn prop(lists: ThreeLists) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var a1 = try zodd.Relation(u32).fromSlice(&ctx, lists[0]); - var b1 = try zodd.Relation(u32).fromSlice(&ctx, lists[1]); - var c1 = try zodd.Relation(u32).fromSlice(&ctx, lists[2]); + const allocator = testing.allocator; + var a1 = try zodd.Relation(u32).fromSlice(allocator, lists[0]); + var b1 = try zodd.Relation(u32).fromSlice(allocator, lists[1]); + var c1 = try zodd.Relation(u32).fromSlice(allocator, lists[2]); var ab = try a1.merge(&b1); var ab_c = try ab.merge(&c1); defer ab_c.deinit(); - var a2 = try zodd.Relation(u32).fromSlice(&ctx, lists[0]); - var b2 = try zodd.Relation(u32).fromSlice(&ctx, lists[1]); - var c2 = try zodd.Relation(u32).fromSlice(&ctx, lists[2]); + var a2 = try zodd.Relation(u32).fromSlice(allocator, lists[0]); + var b2 = try zodd.Relation(u32).fromSlice(allocator, lists[1]); + var c2 = try zodd.Relation(u32).fromSlice(allocator, lists[2]); var bc = try b2.merge(&c2); var a_bc = try a2.merge(&bc); @@ -301,11 +301,11 @@ test "property: joinHelper matches naive join" { pair_gen, struct { fn prop(p: Pair) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var rel1 = try zodd.Relation(Tuple).fromSlice(&ctx, p[0]); + const allocator = testing.allocator; + var rel1 = try zodd.Relation(Tuple).fromSlice(allocator, p[0]); defer rel1.deinit(); - var rel2 = try zodd.Relation(Tuple).fromSlice(&ctx, p[1]); + var rel2 = try zodd.Relation(Tuple).fromSlice(allocator, p[1]); defer rel2.deinit(); const Result = struct { u32, u32, u32 }; @@ -320,7 +320,7 @@ test "property: joinHelper matches naive join" { } } - var expected = try zodd.Relation(Result).fromSlice(&ctx, expected_list.items); + var expected = try zodd.Relation(Result).fromSlice(allocator, expected_list.items); defer expected.deinit(); const ResultList = std.ArrayListUnmanaged(Result); @@ -338,7 +338,7 @@ test "property: joinHelper matches naive join" { zodd.joinHelper(u32, u32, u32, &rel1, &rel2, Context{ .results = &got_list, .alloc = testing.allocator }, Context.callback); - var got = try zodd.Relation(Result).fromSlice(&ctx, got_list.items); + var got = try zodd.Relation(Result).fromSlice(allocator, got_list.items); defer got.deinit(); try testing.expectEqualSlices(Result, expected.elements, got.elements); @@ -363,23 +363,23 @@ test "property: joinAnti matches naive filter" { pair_gen, struct { fn prop(p: Pair) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var input = zodd.Variable(Tuple).init(&ctx); + const allocator = testing.allocator; + var input = zodd.Variable(Tuple).init(allocator); defer input.deinit(); - var filter = zodd.Variable(Tuple).init(&ctx); + var filter = zodd.Variable(Tuple).init(allocator); defer filter.deinit(); - var output = zodd.Variable(Tuple).init(&ctx); + var output = zodd.Variable(Tuple).init(allocator); defer output.deinit(); - try input.insertSlice(&ctx, p[0]); - try filter.insertSlice(&ctx, p[1]); + try input.insertSlice(p[0]); + try filter.insertSlice(p[1]); _ = try input.changed(); _ = try filter.changed(); - try zodd.joinAnti(u32, u32, u32, Tuple, &ctx, &input, &filter, &output, struct { + try zodd.joinAnti(u32, u32, u32, Tuple, &input, &filter, &output, struct { fn logic(key: *const u32, val: *const u32) Tuple { return .{ key.*, val.* }; } @@ -403,10 +403,10 @@ test "property: joinAnti matches naive filter" { } } - var expected = try zodd.Relation(Tuple).fromSlice(&ctx, expected_list.items); + var expected = try zodd.Relation(Tuple).fromSlice(allocator, expected_list.items); defer expected.deinit(); - var got = try zodd.Relation(Tuple).fromSlice(&ctx, output.recent.elements); + var got = try zodd.Relation(Tuple).fromSlice(allocator, output.recent.elements); defer got.deinit(); try testing.expectEqualSlices(Tuple, expected.elements, got.elements); @@ -432,20 +432,20 @@ test "property: extendInto matches naive extend" { pair_gen, struct { fn prop(p: Pair) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var source = zodd.Variable(Tuple).init(&ctx); + const allocator = testing.allocator; + var source = zodd.Variable(Tuple).init(allocator); defer source.deinit(); - var rel = try zodd.Relation(KV).fromSlice(&ctx, p[1]); + var rel = try zodd.Relation(KV).fromSlice(allocator, p[1]); defer rel.deinit(); - var output = zodd.Variable(KV).init(&ctx); + var output = zodd.Variable(KV).init(allocator); defer output.deinit(); - try source.insertSlice(&ctx, p[0]); + try source.insertSlice(p[0]); _ = try source.changed(); - var ext = zodd.ExtendWith(Tuple, u32, u32).init(&ctx, &rel, struct { + var ext = zodd.ExtendWith(Tuple, u32, u32).init(allocator, &rel, struct { fn f(t: *const Tuple) u32 { return t.*; } @@ -453,7 +453,7 @@ test "property: extendInto matches naive extend" { var leapers = [_]zodd.Leaper(Tuple, u32){ext.leaper()}; - try zodd.extendInto(Tuple, u32, KV, &ctx, &source, &leapers, &output, struct { + try zodd.extendInto(Tuple, u32, KV, &source, &leapers, &output, struct { fn logic(t: *const Tuple, v: *const u32) KV { return .{ t.*, v.* }; } @@ -472,10 +472,10 @@ test "property: extendInto matches naive extend" { } } - var expected = try zodd.Relation(KV).fromSlice(&ctx, expected_list.items); + var expected = try zodd.Relation(KV).fromSlice(allocator, expected_list.items); defer expected.deinit(); - var got = try zodd.Relation(KV).fromSlice(&ctx, output.recent.elements); + var got = try zodd.Relation(KV).fromSlice(allocator, output.recent.elements); defer got.deinit(); try testing.expectEqualSlices(KV, expected.elements, got.elements); @@ -501,8 +501,8 @@ test "property: SecondaryIndex get matches naive filter" { list_gen, struct { fn prop(data: List) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - const Index = zodd.index.SecondaryIndex(Tuple, u32, struct { + const allocator = testing.allocator; + const Index = zodd.SecondaryIndex(Tuple, u32, struct { fn extract(t: Tuple) u32 { return t[0]; } @@ -512,14 +512,14 @@ test "property: SecondaryIndex get matches naive filter" { } }.cmp, 4); - var idx = Index.init(&ctx); + var idx = Index.init(allocator); defer idx.deinit(); for (data) |t| { try idx.insert(t); } - var rel = try zodd.Relation(Tuple).fromSlice(&ctx, data); + var rel = try zodd.Relation(Tuple).fromSlice(allocator, data); defer rel.deinit(); var i: usize = 0; @@ -535,7 +535,7 @@ test "property: SecondaryIndex get matches naive filter" { } } - var expected = try zodd.Relation(Tuple).fromSlice(&ctx, expected_list.items); + var expected = try zodd.Relation(Tuple).fromSlice(allocator, expected_list.items); defer expected.deinit(); const got_ptr = idx.get(key).?; @@ -563,8 +563,8 @@ test "property: aggregate matches naive sum" { list_gen, struct { fn prop(data: List) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var rel = try zodd.Relation(Tuple).fromSlice(&ctx, data); + const allocator = testing.allocator; + var rel = try zodd.Relation(Tuple).fromSlice(allocator, data); defer rel.deinit(); const key_func = struct { @@ -578,7 +578,7 @@ test "property: aggregate matches naive sum" { } }; - var result = try zodd.aggregate.aggregate(Tuple, u32, u32, &ctx, &rel, key_func.key, 0, folder.fold); + var result = try zodd.aggregate(Tuple, u32, u32, allocator, &rel, key_func.key, 0, folder.fold); defer result.deinit(); var map = std.AutoHashMap(u32, u32).init(testing.allocator); @@ -600,7 +600,7 @@ test "property: aggregate matches naive sum" { try expected_list.append(testing.allocator, .{ entry.key_ptr.*, entry.value_ptr.* }); } - var expected = try zodd.Relation(struct { u32, u32 }).fromSlice(&ctx, expected_list.items); + var expected = try zodd.Relation(struct { u32, u32 }).fromSlice(allocator, expected_list.items); defer expected.deinit(); try testing.expectEqualSlices(struct { u32, u32 }, expected.elements, result.elements); @@ -626,9 +626,9 @@ test "property: persistence round-trip" { list_gen, struct { fn prop(data: List) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); + const allocator = testing.allocator; - var original = try zodd.Relation(Tuple).fromSlice(&ctx, data); + var original = try zodd.Relation(Tuple).fromSlice(allocator, data); defer original.deinit(); var aw: std.Io.Writer.Allocating = .init(testing.allocator); @@ -637,7 +637,7 @@ test "property: persistence round-trip" { try original.save(&aw.writer); var reader = std.Io.Reader.fixed(aw.writer.buffered()); - var loaded = try zodd.Relation(Tuple).load(&ctx, &reader); + var loaded = try zodd.Relation(Tuple).load(allocator, &reader); defer loaded.deinit(); try testing.expectEqual(original.len(), loaded.len()); @@ -664,8 +664,8 @@ test "property: aggregate count matches naive count" { list_gen, struct { fn prop(data: List) !void { - var ctx = zodd.ExecutionContext.init(testing.allocator); - var rel = try zodd.Relation(Tuple).fromSlice(&ctx, data); + const allocator = testing.allocator; + var rel = try zodd.Relation(Tuple).fromSlice(allocator, data); defer rel.deinit(); const key_func = struct { @@ -674,11 +674,11 @@ test "property: aggregate count matches naive count" { } }.f; - var result = try zodd.aggregateFn( + var result = try zodd.aggregate( Tuple, u32, u32, - &ctx, + allocator, &rel, key_func, 0, @@ -714,7 +714,7 @@ test "property: aggregate count matches naive count" { }; std.sort.block(struct { u32, u32 }, expected_list.items, {}, sort.lessThan); - var expected = try zodd.Relation(struct { u32, u32 }).fromSlice(&ctx, expected_list.items); + var expected = try zodd.Relation(struct { u32, u32 }).fromSlice(allocator, expected_list.items); defer expected.deinit(); try testing.expectEqualSlices(struct { u32, u32 }, expected.elements, result.elements); diff --git a/tests/regression_tests.zig b/tests/regression_tests.zig index 066ca6c..982c1de 100644 --- a/tests/regression_tests.zig +++ b/tests/regression_tests.zig @@ -4,12 +4,11 @@ const zodd = @import("zodd"); test "regression: totalLen includes to_add batches" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); - var v = zodd.Variable(u32).init(&ctx); + var v = zodd.Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3 }); + try v.insertSlice(&[_]u32{ 1, 2, 3 }); try testing.expectEqual(@as(usize, 3), v.totalLen()); @@ -17,22 +16,21 @@ test "regression: totalLen includes to_add batches" { try testing.expectEqual(@as(usize, 3), v.totalLen()); - try v.insertSlice(&ctx, &[_]u32{ 4, 5 }); + try v.insertSlice(&[_]u32{ 4, 5 }); try testing.expectEqual(@as(usize, 5), v.totalLen()); } test "regression: Iteration cleanup handles variables" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); - var iter = zodd.Iteration(u32).init(&ctx, null); + var iter = zodd.Iteration(u32).init(allocator, null); const v1 = try iter.variable(); const v2 = try iter.variable(); - try v1.insertSlice(&ctx, &[_]u32{ 1, 2, 3 }); - try v2.insertSlice(&ctx, &[_]u32{ 4, 5 }); + try v1.insertSlice(&[_]u32{ 1, 2, 3 }); + try v2.insertSlice(&[_]u32{ 4, 5 }); _ = try iter.changed(); @@ -41,10 +39,9 @@ test "regression: Iteration cleanup handles variables" { test "regression: intersection correctness with sorted values" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const KV = struct { u32, u32 }; - var rel = try zodd.Relation(KV).fromSlice(&ctx, &[_]KV{ + var rel = try zodd.Relation(KV).fromSlice(allocator, &[_]KV{ .{ 1, 10 }, .{ 1, 20 }, .{ 1, 30 }, @@ -53,7 +50,7 @@ test "regression: intersection correctness with sorted values" { }); defer rel.deinit(); - var ext = zodd.ExtendWith(u32, u32, u32).init(&ctx, &rel, struct { + var ext = zodd.ExtendWith(u32, u32, u32).init(allocator, &rel, struct { fn f(t: *const u32) u32 { return t.*; } @@ -74,21 +71,20 @@ test "regression: intersection correctness with sorted values" { test "regression: variable deduplication across multiple rounds" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); - var v = zodd.Variable(u32).init(&ctx); + var v = zodd.Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3 }); + try v.insertSlice(&[_]u32{ 1, 2, 3 }); _ = try v.changed(); - try v.insertSlice(&ctx, &[_]u32{ 2, 3, 4, 5 }); + try v.insertSlice(&[_]u32{ 2, 3, 4, 5 }); const changed1 = try v.changed(); try testing.expect(changed1); try testing.expectEqual(@as(usize, 2), v.recent.len()); - try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3, 4, 5 }); + try v.insertSlice(&[_]u32{ 1, 2, 3, 4, 5 }); const changed2 = try v.changed(); try testing.expect(!changed2); @@ -102,26 +98,25 @@ test "regression: variable deduplication across multiple rounds" { test "regression: extendInto error detection with allocation failure simulation" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32 }; const Val = u32; - var source = zodd.Variable(Tuple).init(&ctx); + var source = zodd.Variable(Tuple).init(allocator); defer source.deinit(); - try source.insertSlice(&ctx, &[_]Tuple{.{1}}); + try source.insertSlice(&[_]Tuple{.{1}}); _ = try source.changed(); - var R_B = try zodd.Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var R_B = try zodd.Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 1, 10 }, .{ 1, 20 }, }); defer R_B.deinit(); - var output = zodd.Variable(struct { u32, u32 }).init(&ctx); + var output = zodd.Variable(struct { u32, u32 }).init(allocator); defer output.deinit(); - var extB = zodd.ExtendWith(Tuple, u32, Val).init(&ctx, &R_B, struct { + var extB = zodd.ExtendWith(Tuple, u32, Val).init(allocator, &R_B, struct { fn f(t: *const Tuple) u32 { return t[0]; } @@ -129,7 +124,7 @@ test "regression: extendInto error detection with allocation failure simulation" var leapers = [_]zodd.Leaper(Tuple, Val){extB.leaper()}; - try zodd.extendInto(Tuple, Val, struct { u32, u32 }, &ctx, &source, &leapers, &output, struct { + try zodd.extendInto(Tuple, Val, struct { u32, u32 }, &source, &leapers, &output, struct { fn logic(t: *const Tuple, v: *const Val) struct { u32, u32 } { return .{ t[0], v.* }; } @@ -141,10 +136,9 @@ test "regression: extendInto error detection with allocation failure simulation" test "regression: SecondaryIndex get returns pointer not copy" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - const Index = zodd.index.SecondaryIndex(Tuple, u32, struct { + const Index = zodd.SecondaryIndex(Tuple, u32, struct { fn extract(t: Tuple) u32 { return t[1]; } @@ -154,7 +148,7 @@ test "regression: SecondaryIndex get returns pointer not copy" { } }.cmp, 4); - var idx = Index.init(&ctx); + var idx = Index.init(allocator); defer idx.deinit(); try idx.insert(.{ 1, 10 }); @@ -168,12 +162,11 @@ test "regression: SecondaryIndex get returns pointer not copy" { test "regression: Variable complete includes recent and to_add data" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); - var v = zodd.Variable(u32).init(&ctx); + var v = zodd.Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3 }); + try v.insertSlice(&[_]u32{ 1, 2, 3 }); var result = try v.complete(); defer result.deinit(); @@ -186,15 +179,14 @@ test "regression: Variable complete includes recent and to_add data" { test "regression: Variable complete with recent data not yet stable" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); - var v = zodd.Variable(u32).init(&ctx); + var v = zodd.Variable(u32).init(allocator); defer v.deinit(); - try v.insertSlice(&ctx, &[_]u32{ 1, 2 }); + try v.insertSlice(&[_]u32{ 1, 2 }); _ = try v.changed(); - try v.insertSlice(&ctx, &[_]u32{ 3, 4 }); + try v.insertSlice(&[_]u32{ 3, 4 }); _ = try v.changed(); var result = try v.complete(); @@ -205,7 +197,6 @@ test "regression: Variable complete with recent data not yet stable" { test "regression: gallop with large step values" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const size = 1000; const data = try allocator.alloc(u32, size); @@ -215,7 +206,7 @@ test "regression: gallop with large step values" { elem.* = @intCast(i * 2); } - var rel = try zodd.Relation(u32).fromSlice(&ctx, data); + var rel = try zodd.Relation(u32).fromSlice(allocator, data); defer rel.deinit(); const target: u32 = 1500; @@ -229,10 +220,9 @@ test "regression: gallop with large step values" { test "regression: Relation save and load with tuples" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var original = try zodd.Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var original = try zodd.Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ 2, 20 }, .{ 1, 10 }, .{ 3, 30 }, @@ -245,7 +235,7 @@ test "regression: Relation save and load with tuples" { try original.save(&aw.writer); var reader = std.Io.Reader.fixed(aw.writer.buffered()); - var loaded = try zodd.Relation(Tuple).load(&ctx, &reader); + var loaded = try zodd.Relation(Tuple).load(allocator, &reader); defer loaded.deinit(); try testing.expectEqual(original.len(), loaded.len()); @@ -254,26 +244,25 @@ test "regression: Relation save and load with tuples" { test "regression: extendInto with only ExtendAnti should not call propose" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32 }; const Val = u32; - var source = zodd.Variable(Tuple).init(&ctx); + var source = zodd.Variable(Tuple).init(allocator); defer source.deinit(); - try source.insertSlice(&ctx, &[_]Tuple{.{1}}); + try source.insertSlice(&[_]Tuple{.{1}}); _ = try source.changed(); const KV = struct { u32, u32 }; - var rel = try zodd.Relation(KV).fromSlice(&ctx, &[_]KV{ + var rel = try zodd.Relation(KV).fromSlice(allocator, &[_]KV{ .{ 2, 100 }, }); defer rel.deinit(); - var output = zodd.Variable(struct { u32, u32 }).init(&ctx); + var output = zodd.Variable(struct { u32, u32 }).init(allocator); defer output.deinit(); - var ext = zodd.ExtendAnti(Tuple, u32, Val).init(&ctx, &rel, struct { + var ext = zodd.ExtendAnti(Tuple, u32, Val).init(allocator, &rel, struct { fn f(t: *const Tuple) u32 { return t[0]; } @@ -281,7 +270,7 @@ test "regression: extendInto with only ExtendAnti should not call propose" { var leapers = [_]zodd.Leaper(Tuple, Val){ext.leaper()}; - try zodd.extendInto(Tuple, Val, struct { u32, u32 }, &ctx, &source, leapers[0..], &output, struct { + try zodd.extendInto(Tuple, Val, struct { u32, u32 }, &source, leapers[0..], &output, struct { fn logic(t: *const Tuple, v: *const Val) struct { u32, u32 } { return .{ t[0], v.* }; } @@ -293,10 +282,9 @@ test "regression: extendInto with only ExtendAnti should not call propose" { test "regression: SecondaryIndex does not leak memory on repeated inserts" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - const Index = zodd.index.SecondaryIndex(Tuple, u32, struct { + const Index = zodd.SecondaryIndex(Tuple, u32, struct { fn extract(t: Tuple) u32 { return t[0]; } @@ -306,7 +294,7 @@ test "regression: SecondaryIndex does not leak memory on repeated inserts" { } }.cmp, 4); - var idx = Index.init(&ctx); + var idx = Index.init(allocator); defer idx.deinit(); try idx.insert(.{ 1, 100 }); @@ -319,25 +307,24 @@ test "regression: SecondaryIndex does not leak memory on repeated inserts" { test "regression: joinAnti searches full filter" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var input = zodd.Variable(Tuple).init(&ctx); + var input = zodd.Variable(Tuple).init(allocator); defer input.deinit(); - var filter = zodd.Variable(Tuple).init(&ctx); + var filter = zodd.Variable(Tuple).init(allocator); defer filter.deinit(); - var output = zodd.Variable(Tuple).init(&ctx); + var output = zodd.Variable(Tuple).init(allocator); defer output.deinit(); - try input.insertSlice(&ctx, &[_]Tuple{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 } }); - try filter.insertSlice(&ctx, &[_]Tuple{ .{ 1, 100 }, .{ 3, 300 } }); + try input.insertSlice(&[_]Tuple{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 } }); + try filter.insertSlice(&[_]Tuple{ .{ 1, 100 }, .{ 3, 300 } }); _ = try input.changed(); _ = try filter.changed(); - try zodd.joinAnti(u32, u32, u32, Tuple, &ctx, &input, &filter, &output, struct { + try zodd.joinAnti(u32, u32, u32, Tuple, &input, &filter, &output, struct { fn logic(key: *const u32, val: *const u32) Tuple { return .{ key.*, val.* }; } @@ -350,7 +337,6 @@ test "regression: joinAnti searches full filter" { test "regression: Relation loadWithLimit rejects large length" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; var aw: std.Io.Writer.Allocating = .init(allocator); @@ -369,31 +355,30 @@ test "regression: Relation loadWithLimit rejects large length" { try writer.writeAll(std.mem.sliceAsBytes(&arr2)); var reader = std.Io.Reader.fixed(aw.writer.buffered()); - try testing.expectError(error.TooLarge, zodd.Relation(Tuple).loadWithLimit(&ctx, &reader, 1)); + try testing.expectError(error.TooLarge, zodd.Relation(Tuple).loadWithLimit(allocator, &reader, 1)); } test "regression: extendInto resets leaper error" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32 }; const Val = u32; - var source = zodd.Variable(Tuple).init(&ctx); + var source = zodd.Variable(Tuple).init(allocator); defer source.deinit(); - try source.insertSlice(&ctx, &[_]Tuple{.{1}}); + try source.insertSlice(&[_]Tuple{.{1}}); _ = try source.changed(); - var rel = try zodd.Relation(struct { u32, u32 }).fromSlice(&ctx, &[_]struct { u32, u32 }{ + var rel = try zodd.Relation(struct { u32, u32 }).fromSlice(allocator, &[_]struct { u32, u32 }{ .{ 1, 10 }, .{ 1, 20 }, }); defer rel.deinit(); - var output = zodd.Variable(struct { u32, u32 }).init(&ctx); + var output = zodd.Variable(struct { u32, u32 }).init(allocator); defer output.deinit(); - var ext = zodd.ExtendWith(Tuple, u32, Val).init(&ctx, &rel, struct { + var ext = zodd.ExtendWith(Tuple, u32, Val).init(allocator, &rel, struct { fn f(t: *const Tuple) u32 { return t[0]; } @@ -402,7 +387,7 @@ test "regression: extendInto resets leaper error" { var leapers = [_]zodd.Leaper(Tuple, Val){ext.leaper()}; leapers[0].had_error = true; - try zodd.extendInto(Tuple, Val, struct { u32, u32 }, &ctx, &source, &leapers, &output, struct { + try zodd.extendInto(Tuple, Val, struct { u32, u32 }, &source, &leapers, &output, struct { fn logic(t: *const Tuple, v: *const Val) struct { u32, u32 } { return .{ t[0], v.* }; } @@ -414,7 +399,6 @@ test "regression: extendInto resets leaper error" { test "regression: loadWithLimit rejects invalid magic" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; var aw: std.Io.Writer.Allocating = .init(allocator); @@ -430,12 +414,11 @@ test "regression: loadWithLimit rejects invalid magic" { try writer.writeAll(std.mem.sliceAsBytes(&arr1)); var reader = std.Io.Reader.fixed(aw.writer.buffered()); - try testing.expectError(error.InvalidFormat, zodd.Relation(Tuple).loadWithLimit(&ctx, &reader, 10)); + try testing.expectError(error.InvalidFormat, zodd.Relation(Tuple).loadWithLimit(allocator, &reader, 10)); } test "regression: loadWithLimit rejects unsupported version" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; var aw: std.Io.Writer.Allocating = .init(allocator); @@ -451,34 +434,33 @@ test "regression: loadWithLimit rejects unsupported version" { try writer.writeAll(std.mem.sliceAsBytes(&arr1)); var reader = std.Io.Reader.fixed(aw.writer.buffered()); - try testing.expectError(error.UnsupportedVersion, zodd.Relation(Tuple).loadWithLimit(&ctx, &reader, 10)); + try testing.expectError(error.UnsupportedVersion, zodd.Relation(Tuple).loadWithLimit(allocator, &reader, 10)); } test "regression: joinAnti checks multiple stable batches" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var input = zodd.Variable(Tuple).init(&ctx); + var input = zodd.Variable(Tuple).init(allocator); defer input.deinit(); - var filter = zodd.Variable(Tuple).init(&ctx); + var filter = zodd.Variable(Tuple).init(allocator); defer filter.deinit(); - var output = zodd.Variable(Tuple).init(&ctx); + var output = zodd.Variable(Tuple).init(allocator); defer output.deinit(); - try input.insertSlice(&ctx, &[_]Tuple{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 } }); + try input.insertSlice(&[_]Tuple{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 } }); _ = try input.changed(); - try filter.insertSlice(&ctx, &[_]Tuple{.{ 1, 100 }}); + try filter.insertSlice(&[_]Tuple{.{ 1, 100 }}); _ = try filter.changed(); _ = try filter.changed(); - try filter.insertSlice(&ctx, &[_]Tuple{.{ 3, 300 }}); + try filter.insertSlice(&[_]Tuple{.{ 3, 300 }}); _ = try filter.changed(); - try zodd.joinAnti(u32, u32, u32, Tuple, &ctx, &input, &filter, &output, struct { + try zodd.joinAnti(u32, u32, u32, Tuple, &input, &filter, &output, struct { fn logic(key: *const u32, val: *const u32) Tuple { return .{ key.*, val.* }; } @@ -491,9 +473,8 @@ test "regression: joinAnti checks multiple stable batches" { test "regression: complete on empty Variable returns empty relation" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); - var v = zodd.Variable(u32).init(&ctx); + var v = zodd.Variable(u32).init(allocator); defer v.deinit(); var res = try v.complete(); @@ -504,25 +485,24 @@ test "regression: complete on empty Variable returns empty relation" { test "regression: joinInto with empty input produces empty output" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const KV = struct { u32, u32 }; const Out = struct { u32, u32, u32 }; - var v1 = zodd.Variable(KV).init(&ctx); + var v1 = zodd.Variable(KV).init(allocator); defer v1.deinit(); - var v2 = zodd.Variable(KV).init(&ctx); + var v2 = zodd.Variable(KV).init(allocator); defer v2.deinit(); - var out = zodd.Variable(Out).init(&ctx); + var out = zodd.Variable(Out).init(allocator); defer out.deinit(); - try v1.insertSlice(&ctx, &[_]KV{.{ 1, 10 }}); + try v1.insertSlice(&[_]KV{.{ 1, 10 }}); _ = try v1.changed(); _ = try v2.changed(); - try zodd.joinInto(u32, u32, u32, Out, &ctx, &v1, &v2, &out, struct { + try zodd.joinInto(u32, u32, u32, Out, &v1, &v2, &out, struct { fn logic(k: *const u32, v1_val: *const u32, v2_val: *const u32) Out { return .{ k.*, v1_val.*, v2_val.* }; } @@ -534,24 +514,23 @@ test "regression: joinInto with empty input produces empty output" { test "regression: joinAnti with empty filter keeps all inputs" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var input = zodd.Variable(Tuple).init(&ctx); + var input = zodd.Variable(Tuple).init(allocator); defer input.deinit(); - var filter = zodd.Variable(Tuple).init(&ctx); + var filter = zodd.Variable(Tuple).init(allocator); defer filter.deinit(); - var output = zodd.Variable(Tuple).init(&ctx); + var output = zodd.Variable(Tuple).init(allocator); defer output.deinit(); - try input.insertSlice(&ctx, &[_]Tuple{ .{ 1, 10 }, .{ 2, 20 } }); + try input.insertSlice(&[_]Tuple{ .{ 1, 10 }, .{ 2, 20 } }); _ = try input.changed(); _ = try filter.changed(); - try zodd.joinAnti(u32, u32, u32, Tuple, &ctx, &input, &filter, &output, struct { + try zodd.joinAnti(u32, u32, u32, Tuple, &input, &filter, &output, struct { fn logic(key: *const u32, val: *const u32) Tuple { return .{ key.*, val.* }; } @@ -563,21 +542,20 @@ test "regression: joinAnti with empty filter keeps all inputs" { test "regression: aggregate with unique keys" { const allocator = testing.allocator; - var ctx = zodd.ExecutionContext.init(allocator); const Tuple = struct { u32, u32 }; - var rel = try zodd.Relation(Tuple).fromSlice(&ctx, &[_]Tuple{ + var rel = try zodd.Relation(Tuple).fromSlice(allocator, &[_]Tuple{ .{ 1, 10 }, .{ 2, 20 }, .{ 3, 30 }, }); defer rel.deinit(); - var result = try zodd.aggregateFn( + var result = try zodd.aggregate( Tuple, u32, u32, - &ctx, + allocator, &rel, struct { fn key(t: *const Tuple) u32 { @@ -679,10 +657,10 @@ test "regression: Relation.fromSlice survives realloc-shrink failure" { // Allocation sequence: #0 = initial elements buffer, #1 = realloc-internal // alloc during shrink. We fail #1 so the shrinkOrCopy fallback runs. var fa = FlakeyAllocator{ .child = testing.allocator, .fail_on_alloc = 1 }; - var ctx = zodd.ExecutionContext.init(fa.allocator()); + const allocator = fa.allocator(); const input = [_]u32{ 1, 1, 2, 2, 3, 3 }; - var rel = try zodd.Relation(u32).fromSlice(&ctx, &input); + var rel = try zodd.Relation(u32).fromSlice(allocator, &input); defer rel.deinit(); try testing.expectEqual(@as(usize, 3), rel.len()); @@ -692,10 +670,10 @@ test "regression: Relation.fromSlice survives realloc-shrink failure" { test "regression: Relation.merge survives realloc-shrink failure" { var fa = FlakeyAllocator{ .child = testing.allocator }; - var ctx = zodd.ExecutionContext.init(fa.allocator()); + const allocator = fa.allocator(); - var a = try zodd.Relation(u32).fromSlice(&ctx, &[_]u32{ 1, 3, 5 }); - var b = try zodd.Relation(u32).fromSlice(&ctx, &[_]u32{ 3, 5, 7 }); + var a = try zodd.Relation(u32).fromSlice(allocator, &[_]u32{ 1, 3, 5 }); + var b = try zodd.Relation(u32).fromSlice(allocator, &[_]u32{ 3, 5, 7 }); // After `fromSlice` twice with already-sorted+unique input, alloc_count // should reflect one alloc per relation. merge will alloc the merge buffer @@ -713,7 +691,7 @@ test "regression: Relation.merge survives realloc-shrink failure" { test "regression: Relation.loadWithLimit survives realloc-shrink failure" { var fa = FlakeyAllocator{ .child = testing.allocator }; - var ctx = zodd.ExecutionContext.init(fa.allocator()); + const allocator = fa.allocator(); const Tuple = struct { u32, u32 }; // Build a valid serialized buffer with duplicates so load shrinks. @@ -733,7 +711,7 @@ test "regression: Relation.loadWithLimit survives realloc-shrink failure" { fa.fail_on_alloc = 1; var reader = std.Io.Reader.fixed(aw.writer.buffered()); - var loaded = try zodd.Relation(Tuple).load(&ctx, &reader); + var loaded = try zodd.Relation(Tuple).load(allocator, &reader); defer loaded.deinit(); try testing.expectEqual(@as(usize, 2), loaded.len()); @@ -751,7 +729,7 @@ test "regression: SecondaryIndex.deinit frees map even if iterator fails" { // byte counter on FlakeyAllocator itself: under the old bug, deinit // freed nothing; with the fix, the B-tree's internal nodes are freed. var fa = FlakeyAllocator{ .child = std.heap.page_allocator }; - var ctx = zodd.ExecutionContext.init(fa.allocator()); + const allocator = fa.allocator(); const Tuple = struct { u32, u32 }; const u32Cmp = struct { @@ -759,13 +737,13 @@ test "regression: SecondaryIndex.deinit frees map even if iterator fails" { return std.math.order(a, b); } }.f; - const Index = zodd.index.SecondaryIndex(Tuple, u32, struct { + const Index = zodd.SecondaryIndex(Tuple, u32, struct { fn extract(t: Tuple) u32 { return t[0]; } }.extract, u32Cmp, 4); - var idx = Index.init(&ctx); + var idx = Index.init(allocator); try idx.insert(.{ 1, 10 }); try idx.insert(.{ 2, 20 }); try idx.insert(.{ 3, 30 }); @@ -784,21 +762,21 @@ test "regression: Variable.changed frees local batches on merge OOM" { // and merges the two. If the merge's internal alloc fails, both locals // must be freed. Before the fix they leaked. var fa = FlakeyAllocator{ .child = testing.allocator }; - var ctx = zodd.ExecutionContext.init(fa.allocator()); + const allocator = fa.allocator(); - var v = zodd.Variable(u32).init(&ctx); + var v = zodd.Variable(u32).init(allocator); defer v.deinit(); // Round 1: { 1, 2, 3, 4 } → self.recent. - try v.insertSlice(&ctx, &[_]u32{ 1, 2, 3, 4 }); + try v.insertSlice(&[_]u32{ 1, 2, 3, 4 }); _ = try v.changed(); // Round 2: { 5, 6, 7, 8 } → self.recent; round 1 batch moves to stable. - try v.insertSlice(&ctx, &[_]u32{ 5, 6, 7, 8 }); + try v.insertSlice(&[_]u32{ 5, 6, 7, 8 }); _ = try v.changed(); // Queue round 3's new tuples so changed() has real work to do. - try v.insertSlice(&ctx, &[_]u32{ 9, 10 }); + try v.insertSlice(&[_]u32{ 9, 10 }); // Round 3: with len(stable.last) == 4 and len(recent) == 4, changed() // triggers the stable+recent merge. Fail the merge buffer alloc. @@ -813,17 +791,17 @@ test "regression: Variable.complete frees locals on merge OOM" { // complete() pops batches off self.stable and merges them into a single // result. A failing merge alloc must still free the in-flight locals. var fa = FlakeyAllocator{ .child = testing.allocator }; - var ctx = zodd.ExecutionContext.init(fa.allocator()); + const allocator = fa.allocator(); - var v = zodd.Variable(u32).init(&ctx); + var v = zodd.Variable(u32).init(allocator); defer v.deinit(); // Build up multiple stable batches. - try v.insertSlice(&ctx, &[_]u32{ 1, 2 }); + try v.insertSlice(&[_]u32{ 1, 2 }); _ = try v.changed(); - try v.insertSlice(&ctx, &[_]u32{ 3, 4 }); + try v.insertSlice(&[_]u32{ 3, 4 }); _ = try v.changed(); - try v.insertSlice(&ctx, &[_]u32{ 5, 6 }); + try v.insertSlice(&[_]u32{ 5, 6 }); _ = try v.changed(); fa.fail_on_alloc = fa.alloc_count; @@ -831,78 +809,3 @@ test "regression: Variable.complete frees locals on merge OOM" { try testing.expectError(error.OutOfMemory, v.complete()); try testing.expect(fa.alloc_failures > 0); } - -test "regression: extendInto cleans up all tasks on mid-loop clone failure" { - // The parallel extendInto path clones each leaper once per task in a - // tight loop. If cloning fails partway through, tasks whose leapers were - // already populated must be cleaned up. The old version did not track - // that, so testing.allocator's leak detector catches any regression. - const Tuple = struct { u32 }; - const Val = u32; - const KV = struct { u32, u32 }; - - var fa = FlakeyAllocator{ .child = testing.allocator }; - var ctx = try zodd.ExecutionContext.initWithThreads(fa.allocator(), 2); - defer ctx.deinit(); - - // > 128 tuples so extendInto splits into multiple tasks (chunk size 128). - var input: [200]Tuple = undefined; - for (&input, 0..) |*t, i| t.* = .{@intCast(i)}; - - var source = zodd.Variable(Tuple).init(&ctx); - defer source.deinit(); - try source.insertSlice(&ctx, &input); - _ = try source.changed(); - - var rel = try zodd.Relation(KV).fromSlice(&ctx, &[_]KV{ .{ 1, 10 }, .{ 2, 20 } }); - defer rel.deinit(); - - var ext = zodd.ExtendWith(Tuple, u32, Val).init(&ctx, &rel, struct { - fn f(t: *const Tuple) u32 { - return t[0]; - } - }.f); - var leapers = [_]zodd.Leaper(Tuple, Val){ext.leaper()}; - - var output = zodd.Variable(KV).init(&ctx); - defer output.deinit(); - - // Allocation sequence inside extendInto's parallel branch: - // [+0] tasks array - // [+1] task 0 clones array [+2] task 0 clone[0] - // [+3] task 1 clones array [+4] task 1 clone[0] - // Failing [+4] reproduces the leak scenario (task 0 fully populated). - fa.fail_on_alloc = fa.alloc_count + 4; - - const result = zodd.extendInto(Tuple, Val, KV, &ctx, &source, &leapers, &output, struct { - fn logic(t: *const Tuple, v: *const Val) KV { - return .{ t[0], v.* }; - } - }.logic); - - try testing.expectError(error.OutOfMemory, result); - try testing.expect(fa.alloc_failures > 0); - // testing.allocator's leak check at scope exit is the actual regression - // assertion: with the old code, task 0's cloned leapers would leak. -} - -test "regression: Variable.filterAgainst survives realloc-shrink failure" { - // filterAgainst compacts target in place, then shrinks. We drive it by - // inserting duplicates that get filtered out on the next `changed()`. - // Allocation counting across `Variable.changed()` and ArrayList growth is - // brittle, so instead of picking one exact index we fail every alloc of - // the exact shrink size by re-using shrinkOrCopy directly from our test. - // (Deterministic integration coverage for Variable is already provided by - // the fromSlice/merge/load tests above, which share the same helper.) - const allocator = testing.allocator; - const original = try allocator.alloc(u32, 8); - for (original, 0..) |*slot, i| slot.* = @intCast(i); - - var fa = FlakeyAllocator{ .child = allocator, .fail_on_alloc = 0 }; - const shrunk = try zodd.relation.shrinkOrCopy(u32, fa.allocator(), original, 5); - defer fa.allocator().free(shrunk); - - try testing.expectEqual(@as(usize, 5), shrunk.len); - try testing.expectEqualSlices(u32, &[_]u32{ 0, 1, 2, 3, 4 }, shrunk); - try testing.expect(fa.alloc_failures > 0); -}