Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 20 additions & 0 deletions .github/workflows/tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -61,3 +61,23 @@ jobs:
zig build wasm
cp zig-out/web/zodd.wasm web/zodd.wasm
node web/smoke_test.mjs

differential:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v4

- name: Install Zig 0.16.0
run: |
curl -sSfL https://ziglang.org/download/0.16.0/zig-x86_64-linux-0.16.0.tar.xz | tar -xJ
echo "$PWD/zig-x86_64-linux-0.16.0" >> "$GITHUB_PATH"

- name: Set up uv
uses: astral-sh/setup-uv@v5

- name: Build the Zodd CLI
run: zig build cli

- name: Run differential tests against Clingo
run: uv run tests/differential/difftest.py --runs 500
9 changes: 7 additions & 2 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,8 +44,11 @@ Priorities, in order:
- `src/zodd/frontend/`: Datalog frontend. `program.zig` is the public `Database` API; `token.zig` and `parser.zig` parse textual Datalog;
`ast.zig` and `builder.zig` hold the shared IR and the programmatic builder; `analyze.zig` checks safety and stratification;
`dyntuple.zig`, `plan.zig`, `join_runtime.zig`, and `evaluator.zig` compile and run rules on the engine core; `explain.zig` renders rule
plans and provenance proof trees.
plans and provenance proof trees; `magic.zig` builds the demand-transformed (magic sets) program behind `Database.queryDemand`.
- `src/cli/main.zig`: The `zodd` CLI executable (`run`, `query`, `plan`, `explain`, and `repl` subcommands), built via `zig build cli`.
- `tests/`: Non-unit tests (`integration_tests.zig`, `regression_tests.zig`, `property_tests.zig`, `incremental_tests.zig`, `frontend_tests.zig`).
- `tests/differential/difftest.py`: Differential testing against Clingo; random stratified programs evaluated by both engines must
agree. Run via `make diff-test` (needs `uv`; the Clingo dependency is declared in the root `pyproject.toml` and pinned by `uv.lock`).
- `web/`: Web frontend. `zodd_wasm.zig` is the Wasm wrapper built by `zig build wasm`; `index.html`, `main.js`, and `style.css` are the UI;
`smoke_test.mjs` is the Node.js smoke test run by `make web-test`.
- `examples/`: Self-contained example programs (`e1_network_reachability.zig` through `e8_comparison_filters.zig`) built as executables via
Expand Down Expand Up @@ -86,10 +89,11 @@ The rest of `src/zodd/` is internal and may be refactored freely as long as the

### Dependencies

Zodd depends on two sibling Zig packages declared in `build.zig.zon`:
Zodd depends on three 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`.
- `chilli`: CLI framework, used only by the `zodd` CLI executable (`src/cli/main.zig`).

Please do not add further dependencies without prior discussion.

Expand All @@ -112,6 +116,7 @@ Run the relevant targets for any change:
| 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` |
| Differential | `make diff-test` | Compares Zodd against Clingo on random programs (needs `uv`) |
| Everything | `make all` | Runs `build`, `test`, `lint`, and `docs` |

## First Contribution Flow
Expand Down
11 changes: 10 additions & 1 deletion Makefile
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ SHELL := /usr/bin/env bash
################################################################################

.PHONY: all build rebuild example test lint format docs docs-serve clean install-deps release help coverage \
setup-hooks test-hooks web web-serve web-test
setup-hooks test-hooks web web-serve web-test cli diff-test
.DEFAULT_GOAL := help

help: ## Show the help messages for all targets
Expand Down Expand Up @@ -68,6 +68,15 @@ lint: ## Check code style and formatting of Zig files
@echo "Running code style checks..."
$(ZIG) fmt --check $(SRC_DIR) $(TEST_DIR) web/zodd_wasm.zig

cli: ## Build the zodd CLI into zig-out/bin
@echo "Building the zodd CLI..."
$(ZIG) build cli $(BUILD_OPTS)

DIFF_RUNS ?= 200

diff-test: cli ## Differential-test zodd against clingo via uv (DIFF_RUNS=200)
uv run tests/differential/difftest.py --runs $(DIFF_RUNS)

web: ## Build the web frontend Wasm module and stage it under `web/`
@echo "Building the web frontend Wasm module..."
$(ZIG) build wasm
Expand Down
14 changes: 9 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,22 @@ This document lists the completed and planned features for Zodd.
- [x] Recursion limits
- [x] Persistence
- [x] Secondary indices
- [x] Incremental maintenance
- [ ] Parallel execution
- [ ] CLI
- [x] Incremental maintenance support
- [x] Parallel execution support
- [x] CLI (`zodd run`, `query` with demand-driven evaluation, `plan`, `explain`, and a `repl`)
- [ ] Streaming input
- [x] Rule DSL (textual Datalog frontend with a parser, a builder API, stratified negation, and aggregates)
- [x] Comparison operators (`<`, `<=`, `>`, `>=`, `=`, and `!=` as body filters)
- [x] Arithmetic in comparison filters (`+`, `-`, `*`, `/`, and parentheses on either side of a comparison)
- [x] Arithmetic assignments support
- [x] Query planner
- [x] Explain (rule plan rendering and tuple provenance proof trees)
- [ ] Magic sets
- [x] Explain support
- [x] Magic sets support
- [x] Fact retraction support

### Development and Testing

- [x] Unit tests in each module
- [x] Integration, regression, property-based tests, etc. in `tests` directory
- [x] Differential testing against Clingo
- [ ] Benchmarks
45 changes: 38 additions & 7 deletions build.zig
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,12 @@ pub fn build(b: *std.Build) void {
});
b.installArtifact(lib);

// Version and commit information, for the CLI and the Wasm module.
const build_options = b.addOptions();
build_options.addOption([]const u8, "version", getVersion(b));
build_options.addOption([]const u8, "commit", getGitInfo(b));
const build_options_mod = build_options.createModule();

// Unit tests (embedded in src/lib.zig)
const lib_tests = b.addTest(.{
.root_module = zodd_mod,
Expand All @@ -36,6 +42,37 @@ pub fn build(b: *std.Build) void {
const test_step = b.step("test", "Run all tests");
test_step.dependOn(&run_lib_tests.step);

// CLI executable (see src/cli/)
{
const chilli_dep = b.dependency("chilli", .{
.target = target,
.optimize = optimize,
});
const cli_mod = b.createModule(.{
.root_source_file = b.path("src/cli/main.zig"),
.target = target,
.optimize = optimize,
});
cli_mod.addImport("zodd", zodd_mod);
cli_mod.addImport("chilli", chilli_dep.module("chilli"));
cli_mod.addImport("build_options", build_options_mod);

const cli_exe = b.addExecutable(.{
.name = "zodd",
.root_module = cli_mod,
});
b.installArtifact(cli_exe);

const cli_step = b.step("cli", "Build the zodd CLI");
cli_step.dependOn(&b.addInstallArtifact(cli_exe, .{}).step);

const cli_tests = b.addTest(.{
.root_module = cli_mod,
.name = "cli-tests",
});
test_step.dependOn(&b.addRunArtifact(cli_tests).step);
}

const io = b.graph.io;

// Discover and add tests from tests/ directory
Expand Down Expand Up @@ -132,19 +169,13 @@ pub fn build(b: *std.Build) void {
});
zodd_wasm_mod.addImport("ordered", ordered_wasm_dep.module("ordered"));

const build_options = b.addOptions();
const version = getVersion(b);
build_options.addOption([]const u8, "version", version);
const commit = getGitInfo(b);
build_options.addOption([]const u8, "commit", commit);

const wasm_mod = b.createModule(.{
.root_source_file = b.path("web/zodd_wasm.zig"),
.target = wasm_target,
.optimize = wasm_optimize,
});
wasm_mod.addImport("zodd", zodd_wasm_mod);
wasm_mod.addImport("build_options", build_options.createModule());
wasm_mod.addImport("build_options", build_options_mod);

const wasm_exe = b.addExecutable(.{
.name = "zodd",
Expand Down
8 changes: 6 additions & 2 deletions build.zig.zon
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
.hash = "minish-0.3.0-SQtSTYI3AgCxWdWbKxS_lvmfbp0wJk29ZW5C9CozJaxm",
},
.ordered = .{
.url = "https://github.com/CogitatorTech/ordered/archive/refs/tags/v0.2.0.tar.gz",
.hash = "ordered-0.2.0-Gy41sAAkAgBO3TZAn9nYuspdeDcD_sHLoIGEZY4pCDDM",
.url = "https://github.com/CogitatorTech/ordered/archive/refs/tags/v0.2.1.tar.gz",
.hash = "ordered-0.2.1-Gy41sDCgAgA_QUoMf8Upew_Yx8sAPe7Fw_A2v5qP8GMK",
},
.chilli = .{
.url = "https://github.com/CogitatorTech/chilli/archive/refs/tags/v0.3.2.tar.gz",
.hash = "chilli-0.3.2-c19PrireAQAXEfA7xrmJW2lrSiNiuuG8wtJI-MJVZgFU",
},
},
.paths = .{ "build.zig", "build.zig.zon", "src", "LICENSE", "README.md" },
Expand Down
2 changes: 2 additions & 0 deletions examples/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@
| 6 | [e6_dependency_resolution.zig](e6_dependency_resolution.zig) | Package dependency resolution with size aggregation and indexes. |
| 7 | [e7_datalog_frontend.zig](e7_datalog_frontend.zig) | Datalog parser, evaluator, and query frontend. |
| 8 | [e8_comparison_filters.zig](e8_comparison_filters.zig) | Comparison filters to monitor latencies against SLA limits. |
| 9 | [e9_arithmetic_hops.zig](e9_arithmetic_hops.zig) | Arithmetic assignments counting network hops under an iteration limit. |

#### Running Examples

Expand All @@ -32,4 +33,5 @@ zig build run-e5_taint_analysis
zig build run-e6_dependency_resolution
zig build run-e7_datalog_frontend
zig build run-e8_comparison_filters
zig build run-e9_arithmetic_hops
```
4 changes: 2 additions & 2 deletions examples/e3_data_lineage.zig
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,12 @@ const zodd = @import("zodd");
// contains_pii(D2) :- contains_pii(D1), transform(D1, D2),
// NOT anonymizes(D1, D2).
// violation(D) :- contains_pii(D), public_dataset(D).

pub fn main() !void {
var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

std.debug.print("Zodd Datalog Engine - Data Lineage Tracking\n", .{});
std.debug.print("=================================================\n\n", .{});

Expand Down
4 changes: 2 additions & 2 deletions examples/e4_rbac_authorization.zig
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,12 @@ const zodd = @import("zodd");
// has_role(U, R2) :- has_role(U, R1), role_hier(R1, R2).
// can_access(U, P) :- has_role(U, R), role_perm(R, P).
// effective(U, P) :- can_access(U, P), NOT denied(U, P).

pub fn main() !void {
var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

std.debug.print("Zodd Datalog Engine - RBAC Authorization\n", .{});
std.debug.print("=================================================\n\n", .{});

Expand Down
4 changes: 2 additions & 2 deletions examples/e5_taint_analysis.zig
Original file line number Diff line number Diff line change
Expand Up @@ -13,12 +13,12 @@ const zodd = @import("zodd");
//
// Uses ExtendWith (leapfrog trie join) for taint propagation and
// FilterAnti for sanitizer filtering.

pub fn main() !void {
var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

std.debug.print("Zodd Datalog Engine - Taint Analysis\n", .{});
std.debug.print("=============================================\n\n", .{});

Expand Down
4 changes: 2 additions & 2 deletions examples/e6_dependency_resolution.zig
Original file line number Diff line number Diff line change
Expand Up @@ -14,12 +14,12 @@ const zodd = @import("zodd");
// - Variable + Relation for transitive closure
// - aggregate for computing total install size per package
// - SecondaryIndex for efficient reverse-dependency lookups

pub fn main() !void {
var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

std.debug.print("Zodd Datalog Engine - Package Dependency Resolution\n", .{});
std.debug.print("===================================================\n\n", .{});

Expand Down
60 changes: 60 additions & 0 deletions examples/e9_arithmetic_hops.zig
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
const std = @import("std");
const zodd = @import("zodd");

// Arithmetic Assignments
//
// Counts network hops from a gateway with the `is` operator, which binds a
// fresh variable to an arithmetic expression per tuple. Recursive rules
// that use an assignment can derive new values forever (here the topology
// has a cycle), so the database requires `max_iterations` to be set, and a
// comparison filter bounds the hop count itself.
//
// Datalog rules:
// reach("gw", 0).
// reach(Y, H2) :- reach(X, H), link(X, Y), H2 is H + 1, H2 < 5.
// best(N, min(H)) :- reach(N, H).

pub fn main() !void {
var gpa = std.heap.DebugAllocator(.{}){};
defer _ = gpa.deinit();
const allocator = gpa.allocator();

std.debug.print("Zodd Datalog Engine - Arithmetic Assignments\n", .{});
std.debug.print("============================================\n\n", .{});

var db = zodd.Database.init(allocator);
defer db.deinit();

try db.run(
\\% Directed links; d -> a closes a cycle.
\\link("gw", "a"). link("a", "b"). link("b", "c").
\\link("a", "c"). link("c", "d"). link("d", "a").
\\
\\% Hop counts from the gateway: H2 is H + 1 computes the next hop
\\% count, and H2 < 5 keeps the search within a hop budget.
\\reach("gw", 0).
\\reach(Y, H2) :- reach(X, H), link(X, Y), H2 is H + 1, H2 < 5.
\\
\\% Shortest observed hop count per node.
\\best(N, min(H)) :- reach(N, H).
);

// A recursive rule with an assignment must run under an iteration
// limit; without one, `solve` returns error.IterationLimitRequired.
db.max_iterations = 32;
try db.solve();

std.debug.print("Reachable within the hop budget:\n", .{});
var best = try db.query("best", &.{ null, null });
defer best.deinit();
while (best.next()) |row| {
std.debug.print(" {s}: {d} hop(s)\n", .{ row.get(0).str, row.get(1).int });
}

std.debug.print("\nAll hop counts observed for node c:\n", .{});
var c_hops = try db.query("reach", &.{ zodd.Value{ .str = "c" }, null });
defer c_hops.deinit();
while (c_hops.next()) |row| {
std.debug.print(" {d} hop(s)\n", .{row.get(1).int});
}
}
5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ description = "The Python environment for the Zodd project"

requires-python = ">=3.10,<4.0"
dependencies = [
"pre-commit (>=4.2.0,<5.0.0)",
"icecream (>=2.1.4,<3.0.0)",
"pre-commit >=4.2.0",
"icecream >=2.1.4",
"clingo>=5.7",
]
Loading
Loading