├── .github ├── FUNDING.yml └── workflows │ ├── lints.yml │ ├── tests.yml │ └── docs.yml ├── CODE_OF_CONDUCT.md ├── pyproject.toml ├── .editorconfig ├── src ├── lib.zig └── dbc.zig ├── LICENSE ├── examples ├── README.md ├── e1_bounded_queue.zig ├── e3_linked_list.zig ├── e2_file_parser.zig ├── e5_generic_data_structure.zig └── e4_banking_system.zig ├── .pre-commit-config.yaml ├── .gitignore ├── logo.svg ├── CONTRIBUTING.md ├── Makefile └── README.md /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: [ habedi ] 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Code of Conduct 2 | 3 | We adhere to the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) version 2.1. 4 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [project] 2 | name = "zig-dbc" 3 | version = "0.1.0" 4 | description = "Python environment for Zig-DbC" 5 | readme = "README.md" 6 | license = { text = "MIT" } 7 | authors = [ 8 | { name = "Hassan Abedi", email = "hassan.abedi.t@gmail.com" } 9 | ] 10 | 11 | requires-python = ">=3.10,<4.0" 12 | dependencies = [ 13 | "python-dotenv (>=1.1.0,<2.0.0)", 14 | "pre-commit (>=4.2.0,<5.0.0)" 15 | ] 16 | 17 | [project.optional-dependencies] 18 | dev = [ 19 | "pytest>=8.0.1", 20 | "pytest-cov>=6.0.0", 21 | "pytest-mock>=3.14.0", 22 | "pytest-asyncio (>=0.26.0,<0.27.0)", 23 | "mypy>=1.11.1", 24 | "ruff>=0.9.3", 25 | "icecream (>=2.1.4,<3.0.0)" 26 | ] 27 | -------------------------------------------------------------------------------- /.github/workflows/lints.yml: -------------------------------------------------------------------------------- 1 | name: Run Linter Checks 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | workflow_dispatch: 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | lints: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout Code 25 | uses: actions/checkout@v4 26 | 27 | - name: Install Zig 28 | uses: goto-bus-stop/setup-zig@v2 29 | with: 30 | version: '0.14.1' 31 | 32 | - name: Install Dependencies 33 | run: | 34 | sudo apt-get update 35 | sudo apt-get install -y make 36 | 37 | - name: Run Linters 38 | run: make lint 39 | -------------------------------------------------------------------------------- /.github/workflows/tests.yml: -------------------------------------------------------------------------------- 1 | name: Run Tests 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | tags: 8 | - 'v*' 9 | 10 | pull_request: 11 | branches: 12 | - main 13 | 14 | workflow_dispatch: 15 | 16 | permissions: 17 | contents: read 18 | 19 | jobs: 20 | tests: 21 | runs-on: ubuntu-latest 22 | 23 | steps: 24 | - name: Checkout Repository 25 | uses: actions/checkout@v4 26 | 27 | - name: Install Zig 28 | uses: goto-bus-stop/setup-zig@v2 29 | with: 30 | version: '0.14.1' 31 | 32 | - name: Install Dependencies 33 | run: | 34 | sudo apt-get update 35 | sudo apt-get install -y make 36 | 37 | - name: Run the Tests 38 | run: make test 39 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Top-most EditorConfig file 2 | root = true 3 | 4 | # Global settings (applicable to all files unless overridden) 5 | [*] 6 | charset = utf-8 # Default character encoding 7 | end_of_line = lf # Use LF for line endings (Unix-style) 8 | indent_style = space # Use spaces for indentation 9 | indent_size = 4 # Default indentation size 10 | insert_final_newline = true # Make sure files end with a newline 11 | trim_trailing_whitespace = true # Remove trailing whitespace 12 | 13 | # Zig files 14 | [*.zig] 15 | max_line_length = 100 16 | 17 | # Markdown files 18 | [*.md] 19 | max_line_length = 120 20 | trim_trailing_whitespace = false # Don't remove trailing whitespace in Markdown files 21 | 22 | # Bash scripts 23 | [*.sh] 24 | indent_size = 2 25 | 26 | # YAML files 27 | [*.{yml,yaml}] 28 | indent_size = 2 29 | 30 | # Python files 31 | [*.py] 32 | max_line_length = 100 33 | -------------------------------------------------------------------------------- /src/lib.zig: -------------------------------------------------------------------------------- 1 | //! Zig-DbC -- A Design by Contract Library for Zig 2 | //! 3 | //! This library provides a set of functions to use design by contract (DbC) principles in 4 | //! Zig programs. DbC is a software engineering methodology that allows developers to specify 5 | //! and verify program correctness through: 6 | //! 7 | //! - **Preconditions**: Conditions that must be true when a function is called 8 | //! - **Postconditions**: Conditions that must be true when a function returns 9 | //! - **Invariants**: Conditions that must hold for object instances all the time 10 | //! 11 | //! Zig-DbC is inspired by DbC concepts from languages like Eiffel and Ada, 12 | //! and aims to provide a simple and idiomatic way to implement these principles in Zig. 13 | 14 | const dbc = @import("dbc.zig"); 15 | 16 | pub const require = dbc.require; 17 | pub const requiref = dbc.requiref; 18 | pub const requireCtx = dbc.requireCtx; 19 | pub const ensure = dbc.ensure; 20 | pub const ensuref = dbc.ensuref; 21 | pub const ensureCtx = dbc.ensureCtx; 22 | pub const contract = dbc.contract; 23 | pub const contractWithErrorTolerance = dbc.contractWithErrorTolerance; 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 Hassan Abedi 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /.github/workflows/docs.yml: -------------------------------------------------------------------------------- 1 | name: Publish API Documentation 2 | 3 | on: 4 | push: 5 | tags: 6 | - 'v*' 7 | 8 | workflow_dispatch: 9 | 10 | permissions: 11 | contents: write 12 | 13 | jobs: 14 | deploy-docs: 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout Repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Install Zig 21 | uses: goto-bus-stop/setup-zig@v2 22 | with: 23 | version: '0.14.1' 24 | 25 | - name: Install System Dependencies 26 | run: | 27 | sudo apt-get update 28 | sudo apt-get install -y make 29 | 30 | - name: Generate Documentation 31 | run: make docs 32 | 33 | - name: Deploy to GitHub Pages 34 | uses: peaceiris/actions-gh-pages@v4 35 | with: 36 | github_token: ${{ secrets.GITHUB_TOKEN }} 37 | publish_dir: ./docs/api 38 | publish_branch: gh-pages 39 | user_name: 'github-actions[bot]' 40 | user_email: 'github-actions[bot]@users.noreply.github.com' 41 | commit_message: "docs: Deploy documentation from ${{ github.sha }}" 42 | -------------------------------------------------------------------------------- /examples/README.md: -------------------------------------------------------------------------------- 1 | ## Zig-DbC Examples 2 | 3 | | # | File | Description | 4 | |---|----------------------------------------------------------------|-------------------------------------------------------------------------------------| 5 | | 1 | [e1_bounded_queue.zig](e1_bounded_queue.zig) | An example that shows contracts on a `BoundedQueue` data structure | 6 | | 2 | [e2_file_parser.zig](e2_file_parser.zig) | An example of using contracts to build a robust file parser | 7 | | 3 | [e3_linked_list.zig](e3_linked_list.zig) | An example that uses contracts to guarantee the integrity of a linked list | 8 | | 4 | [e4_banking_system.zig](e4_banking_system.zig) | An example of a banking system with contracts for transactions and balances | 9 | | 5 | [e5_generic_data_structure.zig](e5_generic_data_structure.zig) | An example of a generic data structure with contracts for invariants and operations | 10 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | default_stages: [ pre-push ] 2 | fail_fast: false 3 | 4 | repos: 5 | - repo: https://github.com/pre-commit/pre-commit-hooks 6 | rev: v5.0.0 7 | hooks: 8 | - id: trailing-whitespace 9 | args: [ --markdown-linebreak-ext=md ] 10 | - id: end-of-file-fixer 11 | - id: mixed-line-ending 12 | - id: check-merge-conflict 13 | - id: check-added-large-files 14 | - id: detect-private-key 15 | - id: check-yaml 16 | - id: check-toml 17 | - id: check-json 18 | - id: check-docstring-first 19 | - id: pretty-format-json 20 | args: [ --autofix, --no-sort-keys ] 21 | 22 | - repo: local 23 | hooks: 24 | - id: format 25 | name: Format the code 26 | entry: make format 27 | language: system 28 | pass_filenames: false 29 | stages: [ pre-commit ] 30 | 31 | - id: lint 32 | name: Check code style 33 | entry: make lint 34 | language: system 35 | pass_filenames: false 36 | stages: [ pre-commit ] 37 | 38 | - id: test 39 | name: Run the tests 40 | entry: make test 41 | language: system 42 | pass_filenames: false 43 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | ######################## 2 | # Python Specific 3 | ######################## 4 | __pycache__/ 5 | *.py[cod] 6 | *$py.class 7 | 8 | # Virtual environments 9 | .env/ 10 | .venv/ 11 | env/ 12 | venv/ 13 | 14 | # Packaging 15 | .Python 16 | *.egg 17 | *.egg-info/ 18 | dist/ 19 | build/ 20 | MANIFEST 21 | 22 | # Python Dependency tools 23 | develop-eggs/ 24 | downloads/ 25 | eggs/ 26 | .eggs/ 27 | lib/ 28 | lib64/ 29 | parts/ 30 | sdist/ 31 | var/ 32 | wheels/ 33 | .installed.cfg 34 | 35 | # Python Test & Coverage 36 | .tox/ 37 | .coverage 38 | .coverage.* 39 | .cache 40 | nosetests.xml 41 | coverage.xml 42 | *.cover 43 | .hypothesis/ 44 | .pytest_cache/ 45 | htmlcov/ 46 | 47 | # Jupyter 48 | .ipynb_checkpoints 49 | 50 | ######################## 51 | # Zig Specific 52 | ######################## 53 | zig-cache/ 54 | .zig-cache/ 55 | zig-out/ 56 | 57 | ######################## 58 | # IDE / Editor Junk 59 | ######################## 60 | .idea/ 61 | *.iml 62 | .vscode/ 63 | *.swp 64 | *~ 65 | *.bak 66 | *.tmp 67 | *.log 68 | tags 69 | 70 | ######################## 71 | # Miscellaneous 72 | ######################## 73 | 74 | # Distribution 75 | bin/ 76 | obj/ 77 | tmp/ 78 | temp/ 79 | 80 | # System / misc 81 | .DS_Store 82 | *.patch 83 | *.orig 84 | *.dump 85 | 86 | # Local databases 87 | *.db 88 | *.sqlite 89 | *.wal 90 | *.duckdb 91 | 92 | # Dependency lock files (optional) 93 | poetry.lock 94 | uv.lock 95 | 96 | # Additional files and directories to ignore (put below) 97 | *_output.txt 98 | public/ 99 | docs/api/ 100 | *.o 101 | *.a 102 | *.so 103 | *.dylib 104 | *.dll 105 | *.exe 106 | latest 107 | -------------------------------------------------------------------------------- /logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 5 | 6 | 10 | 14 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contribution Guidelines 2 | 3 | Thank you for considering contributing to this project! 4 | Contributions are always welcome and appreciated. 5 | 6 | ## How to Contribute 7 | 8 | Please check the [issue tracker](https://github.com/habedi/zig-dbc/issues) to see if there is an issue you 9 | would like to work on or if it has already been resolved. 10 | 11 | ### Reporting Bugs 12 | 13 | 1. Open an issue on the [issue tracker](https://github.com/habedi/zig-dbc/issues). 14 | 2. Include information such as steps to reproduce the observed behavior and relevant logs or screenshots. 15 | 16 | ### Suggesting Features 17 | 18 | 1. Open an issue on the [issue tracker](https://github.com/habedi/zig-dbc/issues). 19 | 2. Provide details about the feature, its purpose, and potential implementation ideas. 20 | 21 | ## Submitting Pull Requests 22 | 23 | - Ensure all tests pass before submitting a pull request. 24 | - Write a clear description of the changes you made and the reasons behind them. 25 | 26 | > [!IMPORTANT] 27 | > It's assumed that by submitting a pull request, you agree to license your contributions under the project's license. 28 | 29 | ## Development Workflow 30 | 31 | ### Prerequisites 32 | 33 | Install GNU Make on your system if it's not already installed. 34 | 35 | ```shell 36 | # For Debian-based systems like Debian, Ubuntu, etc. 37 | sudo apt-get install make 38 | ``` 39 | 40 | - Use the `make install-deps` command to install the development dependencies. 41 | 42 | ### Code Style 43 | 44 | - Use the `make format` command to format the code. 45 | 46 | ### Running Tests 47 | 48 | - Use the `make test` command to run the tests. 49 | 50 | ### Running Linters 51 | 52 | - Use the `make lint` command to run the linters. 53 | 54 | ### See Available Commands 55 | 56 | - Run `make help` to see all available commands for managing different tasks. 57 | 58 | ## Code of Conduct 59 | 60 | We adhere to the [Contributor Covenant](https://www.contributor-covenant.org/version/2/1/code_of_conduct/) version 2.1. 61 | -------------------------------------------------------------------------------- /examples/e1_bounded_queue.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const dbc = @import("dbc"); 4 | 5 | pub fn BoundedQueue(comptime T: type, comptime capacity: usize) type { 6 | return struct { 7 | const Self = @This(); 8 | 9 | items: [capacity]T = undefined, 10 | head: usize = 0, 11 | tail: usize = 0, 12 | count: usize = 0, 13 | 14 | // An invariant is a condition that must hold true before and after every method call. 15 | // This invariant checks that the queue's state is valid. 16 | fn invariant(self: Self) void { 17 | dbc.require(.{ self.count <= capacity, "Queue count exceeds capacity" }); 18 | } 19 | 20 | pub fn enqueue(self: *Self, item: T) void { 21 | // The `old` struct captures the state of the object before the method runs. 22 | const old = .{ .count = self.count, .item = item }; 23 | return dbc.contract(self, old, struct { 24 | // Preconditions are checked at the start of a function. 25 | fn run(ctx: @TypeOf(old), s: *Self) void { 26 | dbc.require(.{ s.count < capacity, "Cannot enqueue to a full queue" }); 27 | 28 | // Core method logic 29 | s.items[s.tail] = ctx.item; 30 | s.tail = (s.tail + 1) % capacity; 31 | s.count += 1; 32 | 33 | // Postconditions are checked at the end of a function. 34 | dbc.ensure(.{ s.count == ctx.count + 1, "Enqueue failed to increment count" }); 35 | } 36 | }.run); 37 | } 38 | 39 | pub fn dequeue(self: *Self) T { 40 | const old = .{ .count = self.count }; 41 | return dbc.contract(self, old, struct { 42 | fn run(ctx: @TypeOf(old), s: *Self) T { 43 | dbc.require(.{ s.count > 0, "Cannot dequeue from an empty queue" }); 44 | 45 | const dequeued_item = s.items[s.head]; 46 | s.head = (s.head + 1) % capacity; 47 | s.count -= 1; 48 | 49 | dbc.ensure(.{ s.count == ctx.count - 1, "Dequeue failed to decrement count" }); 50 | return dequeued_item; 51 | } 52 | }.run); 53 | } 54 | }; 55 | } 56 | 57 | pub fn main() !void { 58 | const MyQueue = BoundedQueue(u32, 3); 59 | var q = MyQueue{}; 60 | 61 | std.debug.print("Contracts are active in this build mode: {}\n", .{builtin.mode != .ReleaseFast}); 62 | 63 | q.enqueue(10); 64 | q.enqueue(20); 65 | std.debug.print("Dequeued: {}\n", .{q.dequeue()}); 66 | std.debug.print("Dequeued: {}\n", .{q.dequeue()}); 67 | } 68 | -------------------------------------------------------------------------------- /examples/e3_linked_list.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const dbc = @import("dbc"); 4 | 5 | const Allocator = std.mem.Allocator; 6 | 7 | const Node = struct { 8 | data: u32, 9 | next: ?*Node, 10 | }; 11 | 12 | const SinglyLinkedList = struct { 13 | const Self = @This(); 14 | allocator: Allocator, 15 | head: ?*Node, 16 | count: usize, 17 | 18 | // The invariant checks that the manually maintained `count` 19 | // matches the actual number of nodes in the linked list. 20 | fn invariant(self: Self) void { 21 | var actual_count: usize = 0; 22 | var current = self.head; 23 | // This loop iterates through all nodes, counting them. 24 | while (current) |node| { 25 | actual_count += 1; 26 | current = node.next; 27 | } 28 | dbc.require(.{ self.count == actual_count, "List count is inconsistent with node count." }); 29 | } 30 | 31 | pub fn init(allocator: Allocator) Self { 32 | return Self{ 33 | .allocator = allocator, 34 | .head = null, 35 | .count = 0, 36 | }; 37 | } 38 | 39 | pub fn deinit(self: *Self) void { 40 | var current = self.head; 41 | while (current) |node| { 42 | const next = node.next; 43 | self.allocator.destroy(node); 44 | current = next; 45 | } 46 | self.head = null; 47 | self.count = 0; 48 | } 49 | 50 | pub fn push_front(self: *Self, value: u32) !void { 51 | const old = .{ .count = self.count, .value = value }; 52 | return dbc.contract(self, old, struct { 53 | fn run(ctx: @TypeOf(old), s: *Self) !void { 54 | const new_node = try s.allocator.create(Node); 55 | new_node.data = ctx.value; 56 | new_node.next = s.head; 57 | s.head = new_node; 58 | s.count += 1; 59 | 60 | // The postcondition verifies that the count was correctly incremented. 61 | dbc.ensure(.{ s.count == ctx.count + 1, "Push_front failed to decrement count." }); 62 | } 63 | }.run); 64 | } 65 | 66 | pub fn pop_front(self: *Self) ?u32 { 67 | if (self.head == null) return null; 68 | 69 | const old = .{ .count = self.count }; 70 | return dbc.contract(self, old, struct { 71 | fn run(ctx: @TypeOf(old), s: *Self) u32 { 72 | dbc.require(.{ s.head != null, "Cannot pop from an empty list." }); 73 | 74 | const head_node = s.head.?; 75 | const value = head_node.data; 76 | s.head = head_node.next; 77 | s.allocator.destroy(head_node); 78 | s.count -= 1; 79 | 80 | // The postcondition verifies that the count was correctly decremented. 81 | dbc.ensure(.{ s.count == ctx.count - 1, "Pop_front failed to decrement count." }); 82 | 83 | return value; 84 | } 85 | }.run); 86 | } 87 | }; 88 | 89 | pub fn main() !void { 90 | std.debug.print("Contracts are active in this build mode: {}\n", .{builtin.mode != .ReleaseFast}); 91 | 92 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 93 | defer _ = gpa.deinit(); 94 | 95 | var list = SinglyLinkedList.init(gpa.allocator()); 96 | defer list.deinit(); 97 | 98 | try list.push_front(10); 99 | try list.push_front(20); 100 | try list.push_front(30); 101 | 102 | std.debug.print("List size: {d}\n", .{list.count}); 103 | 104 | if (list.pop_front()) |value| { 105 | std.debug.print("Popped value: {d}\n", .{value}); 106 | } 107 | 108 | if (list.pop_front()) |value| { 109 | std.debug.print("Popped value: {d}\n", .{value}); 110 | } 111 | 112 | std.debug.print("List size: {d}\n", .{list.count}); 113 | } 114 | -------------------------------------------------------------------------------- /Makefile: -------------------------------------------------------------------------------- 1 | # ################################################################################ 2 | # # Configuration and Variables 3 | # ################################################################################ 4 | ZIG ?= $(shell which zig || echo ~/.local/share/zig/0.14.1/zig) 5 | BUILD_TYPE ?= Debug 6 | BUILD_OPTS = -Doptimize=$(BUILD_TYPE) 7 | JOBS ?= $(shell nproc || echo 2) 8 | SRC_DIR := src 9 | EXAMPLES_DIR := examples 10 | BUILD_DIR := zig-out 11 | CACHE_DIR := .zig-cache 12 | BINARY_NAME := example 13 | RELEASE_MODE := ReleaseSmall 14 | TEST_FLAGS := --summary all #--verbose 15 | JUNK_FILES := *.o *.obj *.dSYM *.dll *.so *.dylib *.a *.lib *.pdb temp/ 16 | 17 | # Automatically find all example names 18 | EXAMPLES := $(patsubst %.zig,%,$(notdir $(wildcard examples/*.zig))) 19 | EXAMPLE ?= all 20 | 21 | SHELL := /usr/bin/env bash 22 | .SHELLFLAGS := -eu -o pipefail -c 23 | 24 | ################################################################################ 25 | # Targets 26 | ################################################################################ 27 | 28 | .PHONY: all help build rebuild run test release clean lint format docs serve-docs install-deps setup-hooks test-hooks 29 | .DEFAULT_GOAL := help 30 | 31 | help: ## Show the help messages for all targets 32 | @echo "Usage: make " 33 | @echo "" 34 | @echo "Targets:" 35 | @grep -E '^[a-zA-Z_-]+:.*## .*$$' Makefile | \ 36 | awk 'BEGIN {FS = ":.*## "}; {printf " \033[36m%-15s\033[0m %s\n", $$1, $$2}' 37 | 38 | all: build test lint docs ## build, test, lint, and doc 39 | 40 | build: ## Build project (e.g. 'make build BUILD_TYPE=ReleaseSmall' or 'make build' for Debug mode) 41 | @echo "Building project in $(BUILD_TYPE) mode with $(JOBS) concurrent jobs..." 42 | @$(ZIG) build $(BUILD_OPTS) -j$(JOBS) 43 | 44 | rebuild: clean build ## clean and build 45 | 46 | run: ## Run an example (e.g. 'make run EXAMPLE=e1_bounded_queue' or 'make run' to run all examples) 47 | @if [ "$(EXAMPLE)" = "all" ]; then \ 48 | echo "--> Running all examples..."; \ 49 | for ex in $(EXAMPLES); do \ 50 | echo ""; \ 51 | echo "--> Running '$$ex'"; \ 52 | $(ZIG) build run-$$ex $(BUILD_OPTS); \ 53 | done; \ 54 | else \ 55 | echo "--> Running example: $(EXAMPLE)"; \ 56 | $(ZIG) build run-$(EXAMPLE) $(BUILD_OPTS); \ 57 | fi 58 | 59 | test: ## Run tests 60 | @echo "Running tests..." 61 | @$(ZIG) build test $(BUILD_OPTS) -j$(JOBS) $(TEST_FLAGS) 62 | 63 | release: ## Build in Release mode 64 | @echo "Building the project in Release mode..." 65 | @$(MAKE) BUILD_TYPE=$(RELEASE_MODE) build 66 | 67 | clean: ## Remove docs, build artifacts, and cache directories 68 | @echo "Removing build artifacts, cache, generated docs, and junk files..." 69 | @rm -rf $(BUILD_DIR) $(CACHE_DIR) $(JUNK_FILES) docs/api public 70 | 71 | lint: ## Check code style and formatting of Zig files 72 | @echo "Running code style checks..." 73 | @$(ZIG) fmt --check $(SRC_DIR) $(EXAMPLES_DIR) 74 | 75 | format: ## Format Zig files 76 | @echo "Formatting Zig files..." 77 | @$(ZIG) fmt . 78 | 79 | docs: ## Generate API documentation 80 | @echo "Generating API documentation..." 81 | @$(ZIG) build docs 82 | 83 | serve-docs: ## Serve the generated documentation on a local server 84 | @echo "Serving API documentation locally..." 85 | @cd docs/api && python3 -m http.server 8000 86 | 87 | install-deps: ## Install system dependencies (for Debian-based systems) 88 | @echo "Installing system dependencies..." 89 | @sudo apt-get update 90 | @sudo apt-get install -y make llvm snapd 91 | @sudo snap install zig --beta --classic 92 | 93 | setup-hooks: ## Install Git hooks (pre-commit and pre-push) 94 | @echo "Setting up Git hooks..." 95 | @if ! command -v pre-commit &> /dev/null; then \ 96 | echo "pre-commit not found. Please install it using 'pip install pre-commit'"; \ 97 | exit 1; \ 98 | fi 99 | @pre-commit install --hook-type pre-commit 100 | @pre-commit install --hook-type pre-push 101 | @pre-commit install-hooks 102 | 103 | test-hooks: ## Test Git hooks on all files 104 | @echo "Testing Git hooks..." 105 | @pre-commit run --all-files --show-diff-on-failure 106 | -------------------------------------------------------------------------------- /examples/e2_file_parser.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const dbc = @import("dbc"); 4 | 5 | const Allocator = std.mem.Allocator; 6 | 7 | const FileParser = struct { 8 | const Self = @This(); 9 | allocator: Allocator, 10 | lines: std.ArrayList([]const u8), 11 | file_path: ?[]const u8, 12 | is_parsed: bool, 13 | 14 | // The invariant checks that if the parser is in a 'parsed' state, 15 | // a file path must exist. This helps maintain data integrity. 16 | fn invariant(self: Self) void { 17 | if (self.is_parsed) { 18 | dbc.require(.{ self.file_path != null, "Parsed file must have a path" }); 19 | } 20 | } 21 | 22 | pub fn init(allocator: Allocator) Self { 23 | return Self{ 24 | .allocator = allocator, 25 | .lines = std.ArrayList([]const u8).init(allocator), 26 | .file_path = null, 27 | .is_parsed = false, 28 | }; 29 | } 30 | 31 | pub fn deinit(self: *Self) void { 32 | for (self.lines.items) |line| { 33 | self.allocator.free(line); 34 | } 35 | self.lines.deinit(); 36 | } 37 | 38 | pub fn reset(self: *Self) void { 39 | for (self.lines.items) |line| { 40 | self.allocator.free(line); 41 | } 42 | self.lines.clearAndFree(); 43 | if (self.file_path) |path| { 44 | self.allocator.free(path); 45 | self.file_path = null; 46 | } 47 | self.is_parsed = false; 48 | } 49 | 50 | pub fn parseFile(self: *Self, path: []const u8) !void { 51 | const old = .{ .old_lines_count = self.lines.items.len, .path = path }; 52 | 53 | return dbc.contract(self, old, struct { 54 | fn run(ctx: @TypeOf(old), s: *Self) !void { 55 | // A precondition to ensure the parser is not already in a parsed state. 56 | // This prevents re-parsing without calling `reset`. 57 | dbc.require(.{ !s.is_parsed, "Parser is already in a parsed state. Call reset first." }); 58 | 59 | const file = try std.fs.cwd().openFile(ctx.path, .{}); 60 | defer file.close(); 61 | 62 | var buffered_reader = std.io.bufferedReader(file.reader()); 63 | var line_reader = buffered_reader.reader(); 64 | 65 | while (try line_reader.readUntilDelimiterOrEofAlloc(s.allocator, '\n', 4096)) |line| { 66 | try s.lines.append(line); 67 | } 68 | 69 | if (s.file_path) |old_path| s.allocator.free(old_path); 70 | s.file_path = try s.allocator.dupe(u8, ctx.path); 71 | 72 | s.is_parsed = true; 73 | 74 | // A postcondition to verify that new lines were read successfully. 75 | dbc.ensure(.{ s.lines.items.len > ctx.old_lines_count, "Parsing failed to read any new lines." }); 76 | } 77 | }.run); 78 | } 79 | }; 80 | 81 | pub fn main() !void { 82 | std.debug.print("Contracts are active in this build mode: {}\n", .{builtin.mode != .ReleaseFast}); 83 | 84 | const temp_dir_path = "temp"; 85 | 86 | std.fs.cwd().makeDir(temp_dir_path) catch |err| if (err != error.PathAlreadyExists) return err; 87 | 88 | var temp_dir = try std.fs.cwd().openDir(temp_dir_path, .{}); 89 | defer temp_dir.close(); 90 | 91 | const temp_file_name = "test.txt"; 92 | const temp_file = try temp_dir.createFile(temp_file_name, .{}); 93 | defer temp_file.close(); 94 | 95 | try temp_file.writeAll( 96 | \\line 1 97 | \\line 2 98 | \\line 3 99 | \\ 100 | ); 101 | 102 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 103 | defer _ = gpa.deinit(); 104 | 105 | var parser = FileParser.init(gpa.allocator()); 106 | defer parser.deinit(); 107 | 108 | const file_path_in_temp = try std.fmt.allocPrint(gpa.allocator(), "{s}/{s}", .{ temp_dir_path, temp_file_name }); 109 | defer gpa.allocator().free(file_path_in_temp); 110 | 111 | parser.parseFile(file_path_in_temp) catch |err| { 112 | std.debug.print("Parse error: {s}\n", .{@errorName(err)}); 113 | }; 114 | 115 | std.debug.print("Successfully parsed file with {} lines.\n", .{parser.lines.items.len}); 116 | 117 | if (builtin.mode != .ReleaseFast) { 118 | std.debug.print("Attempting to re-parse file (will panic in debug modes).\n", .{}); 119 | parser.reset(); 120 | parser.parseFile(file_path_in_temp) catch |err| { 121 | std.debug.print("Parse error on retry: {s}\n", .{@errorName(err)}); 122 | }; 123 | std.debug.print("Successfully re-parsed file with {} lines.\n", .{parser.lines.items.len}); 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /examples/e5_generic_data_structure.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const dbc = @import("dbc"); 4 | 5 | pub fn DynamicArray(comptime T: type) type { 6 | return struct { 7 | const Self = @This(); 8 | 9 | items: []T, 10 | len: usize, 11 | capacity: usize, 12 | allocator: std.mem.Allocator, 13 | 14 | fn invariant(self: Self) void { 15 | dbc.require(.{ self.len <= self.capacity, "Length cannot exceed capacity" }); 16 | dbc.requireCtx(self.items.len == self.capacity, "self.items.len == self.capacity"); 17 | } 18 | 19 | pub fn init(allocator: std.mem.Allocator, initial_capacity: usize) !Self { 20 | dbc.require(.{ initial_capacity > 0, "Initial capacity must be positive" }); 21 | 22 | const items = try allocator.alloc(T, initial_capacity); 23 | return Self{ 24 | .items = items, 25 | .len = 0, 26 | .capacity = initial_capacity, 27 | .allocator = allocator, 28 | }; 29 | } 30 | 31 | pub fn deinit(self: *Self) void { 32 | self.allocator.free(self.items); 33 | self.* = undefined; 34 | } 35 | 36 | pub fn append(self: *Self, item: T) !void { 37 | const old = .{ .len = self.len, .capacity = self.capacity, .item = item }; 38 | 39 | return dbc.contract(self, old, struct { 40 | fn run(ctx: @TypeOf(old), s: *Self) !void { 41 | // Resize if needed 42 | if (s.len >= s.capacity) { 43 | const new_capacity = s.capacity * 2; 44 | const new_items = try s.allocator.realloc(s.items, new_capacity); 45 | s.items = new_items; 46 | s.capacity = new_capacity; 47 | } 48 | 49 | s.items[s.len] = ctx.item; 50 | s.len += 1; 51 | 52 | dbc.ensure(.{ s.len == ctx.len + 1, "Length should increment by 1" }); 53 | dbc.ensureCtx(s.len <= s.capacity, "s.len <= s.capacity"); 54 | } 55 | }.run); 56 | } 57 | 58 | pub fn get(self: *Self, index: usize) T { 59 | const old = .{ .index = index }; 60 | 61 | return dbc.contract(self, old, struct { 62 | fn run(ctx: @TypeOf(old), s: *Self) T { 63 | dbc.require(.{ ctx.index < s.len, "Index out of bounds" }); 64 | return s.items[ctx.index]; 65 | } 66 | }.run); 67 | } 68 | 69 | pub fn pop(self: *Self) ?T { 70 | if (self.len == 0) return null; 71 | 72 | const old = .{ .len = self.len }; 73 | return dbc.contract(self, old, struct { 74 | fn run(ctx: @TypeOf(old), s: *Self) T { 75 | dbc.require(.{ s.len > 0, "Cannot pop from empty array" }); 76 | 77 | s.len -= 1; 78 | const item = s.items[s.len]; 79 | 80 | dbc.ensure(.{ s.len == ctx.len - 1, "Length should decrement by 1" }); 81 | return item; 82 | } 83 | }.run); 84 | } 85 | }; 86 | } 87 | 88 | // Validator examples 89 | const PositiveValidator = struct { 90 | pub fn run(_: @This(), num: i32) bool { 91 | return num > 0; 92 | } 93 | }; 94 | 95 | const RangeValidator = struct { 96 | min: i32, 97 | max: i32, 98 | 99 | pub fn run(self: @This(), value: i32) bool { 100 | return value >= self.min and value <= self.max; 101 | } 102 | }; 103 | 104 | pub fn validateNumber(num: i32) i32 { 105 | const validator = PositiveValidator{}; 106 | dbc.require(.{ validator, num, "Number must be positive" }); 107 | 108 | const result = num * 2; 109 | const range_validator = RangeValidator{ .min = 2, .max = 200 }; 110 | dbc.ensure(.{ range_validator, result, "Result must be in range [2, 200]" }); 111 | 112 | return result; 113 | } 114 | 115 | pub fn main() !void { 116 | std.debug.print("Generic Data Structure Example (contracts active: {})\n", .{builtin.mode != .ReleaseFast}); 117 | 118 | var gpa = std.heap.GeneralPurposeAllocator(.{}){}; 119 | defer _ = gpa.deinit(); 120 | 121 | var array = try DynamicArray(i32).init(gpa.allocator(), 2); 122 | defer array.deinit(); 123 | 124 | try array.append(10); 125 | try array.append(20); 126 | try array.append(30); // This will trigger resize 127 | 128 | std.debug.print("Array length: {d}, capacity: {d}\n", .{ array.len, array.capacity }); 129 | std.debug.print("Element at index 1: {d}\n", .{array.get(1)}); 130 | 131 | if (array.pop()) |value| { 132 | std.debug.print("Popped value: {d}\n", .{value}); 133 | } 134 | 135 | // Validator example 136 | const validated_result = validateNumber(42); 137 | std.debug.print("Validated and processed number: {d}\n", .{validated_result}); 138 | } 139 | -------------------------------------------------------------------------------- /examples/e4_banking_system.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | const builtin = @import("builtin"); 3 | const dbc = @import("dbc"); 4 | 5 | const BankAccount = struct { 6 | const Self = @This(); 7 | 8 | balance: u64, 9 | account_number: u32, 10 | is_active: bool, 11 | transaction_count: u32, 12 | 13 | fn invariant(self: Self) void { 14 | // Simple API for basic conditions 15 | dbc.require(.{ self.account_number > 0, "Account number must be positive" }); 16 | 17 | // Context capture for simple expressions 18 | dbc.requireCtx(self.balance >= 0 or !self.is_active, "self.balance >= 0 or !self.is_active"); 19 | } 20 | 21 | pub fn init(account_number: u32) Self { 22 | dbc.require(.{ account_number > 0, "Account number must be positive" }); 23 | 24 | return Self{ 25 | .balance = 0, 26 | .account_number = account_number, 27 | .is_active = true, 28 | .transaction_count = 0, 29 | }; 30 | } 31 | 32 | pub fn deposit(self: *Self, amount: u64) void { 33 | const old = .{ .balance = self.balance, .tx_count = self.transaction_count, .amount = amount }; 34 | 35 | return dbc.contract(self, old, struct { 36 | fn run(ctx: @TypeOf(old), s: *Self) void { 37 | // Mixed API usage showing different styles 38 | dbc.require(.{ s.is_active, "Account must be active for deposits" }); 39 | dbc.require(.{ ctx.amount > 0, "Deposit amount must be positive" }); 40 | dbc.requireCtx(ctx.amount <= 1_000_000, "ctx.amount <= 1_000_000"); 41 | 42 | // Business logic 43 | s.balance += ctx.amount; 44 | s.transaction_count += 1; 45 | 46 | // Postconditions using different API styles 47 | dbc.ensure(.{ s.balance == ctx.balance + ctx.amount, "Balance calculation error" }); 48 | dbc.ensureCtx(s.transaction_count == ctx.tx_count + 1, "s.transaction_count == ctx.tx_count + 1"); 49 | } 50 | }.run); 51 | } 52 | 53 | pub fn withdraw(self: *Self, amount: u64) !void { 54 | const old = .{ .balance = self.balance, .tx_count = self.transaction_count, .amount = amount }; 55 | 56 | return dbc.contract(self, old, struct { 57 | fn run(ctx: @TypeOf(old), s: *Self) !void { 58 | // Preconditions 59 | dbc.require(.{ s.is_active, "Cannot withdraw from inactive account" }); 60 | dbc.require(.{ ctx.amount > 0, "Withdrawal amount must be positive" }); 61 | dbc.require(.{ ctx.amount <= s.balance, "Insufficient funds" }); 62 | 63 | // Simulate potential error 64 | if (ctx.amount > 50_000) return error.DailyLimitExceeded; 65 | 66 | // Business logic 67 | s.balance -= ctx.amount; 68 | s.transaction_count += 1; 69 | 70 | // Postconditions 71 | dbc.ensure(.{ s.balance == ctx.balance - ctx.amount, "Withdrawal calculation error" }); 72 | dbc.ensure(.{ s.transaction_count == ctx.tx_count + 1, "Transaction count should increment" }); 73 | } 74 | }.run); 75 | } 76 | 77 | pub fn transfer(self: *Self, to: *Self, amount: u64) !void { 78 | const old = .{ .from_balance = self.balance, .to_balance = to.balance, .from_tx = self.transaction_count, .to_tx = to.transaction_count, .amount = amount, .to_ptr = to }; 79 | 80 | return dbc.contract(self, old, struct { 81 | fn run(ctx: @TypeOf(old), from: *Self) !void { 82 | dbc.require(.{ from.is_active, "Sender account must be active" }); 83 | dbc.require(.{ ctx.to_ptr.is_active, "Recipient account must be active" }); 84 | dbc.require(.{ from.balance >= ctx.amount, "Insufficient funds for transfer" }); 85 | 86 | // Business logic - both accounts updated within contract 87 | from.balance -= ctx.amount; 88 | from.transaction_count += 1; 89 | ctx.to_ptr.balance += ctx.amount; 90 | ctx.to_ptr.transaction_count += 1; 91 | 92 | // Postconditions 93 | dbc.ensure(.{ from.balance == ctx.from_balance - ctx.amount, "Transfer amount calculation error" }); 94 | dbc.ensureCtx(from.transaction_count == ctx.from_tx + 1, "from.transaction_count == ctx.from_tx + 1"); 95 | dbc.ensure(.{ ctx.to_ptr.balance == ctx.to_balance + ctx.amount, "Recipient balance calculation error" }); 96 | dbc.ensureCtx(ctx.to_ptr.transaction_count == ctx.to_tx + 1, "ctx.to_ptr.transaction_count == ctx.to_tx + 1"); 97 | } 98 | }.run); 99 | } 100 | }; 101 | 102 | pub fn main() !void { 103 | std.debug.print("Banking Systsem Example (contracts active: {})\n", .{builtin.mode != .ReleaseFast}); 104 | 105 | var account1 = BankAccount.init(12345); 106 | var account2 = BankAccount.init(67890); 107 | 108 | account1.deposit(1000); 109 | account2.deposit(500); 110 | 111 | std.debug.print("Account 1 balance: {d}\n", .{account1.balance}); 112 | std.debug.print("Account 2 balance: {d}\n", .{account2.balance}); 113 | 114 | try account1.transfer(&account2, 250); 115 | 116 | std.debug.print("After transfer - Account 1: {d}, Account 2: {d}\n", .{ account1.balance, account2.balance }); 117 | 118 | try account1.withdraw(100); 119 | std.debug.print("After withdrawal - Account 1: {d}\n", .{account1.balance}); 120 | } 121 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Zig-DbC Logo 4 | 5 |
6 | 7 |

Zig-DbC

8 | 9 | [![Tests](https://img.shields.io/github/actions/workflow/status/habedi/zig-dbc/tests.yml?label=tests&style=flat&labelColor=282c34&logo=github)](https://github.com/habedi/zig-dbc/actions/workflows/tests.yml) 10 | [![CodeFactor](https://img.shields.io/codefactor/grade/github/habedi/zig-dbc?label=code%20quality&style=flat&labelColor=282c34&logo=codefactor)](https://www.codefactor.io/repository/github/habedi/zig-dbc) 11 | [![Zig Version](https://img.shields.io/badge/Zig-0.14.1-orange?logo=zig&labelColor=282c34)](https://ziglang.org/download/) 12 | [![Docs](https://img.shields.io/github/v/tag/habedi/zig-dbc?label=docs&color=blue&style=flat&labelColor=282c34&logo=read-the-docs)](https://habedi.github.io/zig-dbc/) 13 | [![Release](https://img.shields.io/github/release/habedi/zig-dbc.svg?label=release&style=flat&labelColor=282c34&logo=github)](https://github.com/habedi/zig-dbc/releases/latest) 14 | [![License](https://img.shields.io/badge/license-MIT-007ec6?label=license&style=flat&labelColor=282c34&logo=open-source-initiative)](https://github.com/habedi/zig-dbc/blob/main/LICENSE) 15 | 16 | A Design by Contract Library for Zig 17 | 18 |
19 | 20 | --- 21 | 22 | Zig-DbC is a small library that provides a collection of functions to use 23 | [design by contract](https://en.wikipedia.org/wiki/Design_by_contract) (DbC) principles in Zig programs. 24 | It provides a simple and idiomatic API for defining preconditions, postconditions, and invariants that can be 25 | checked at runtime. 26 | 27 | A common use case for DbC (and by extension Zig-DbC) is adding checks that guarantee the code behaves as intended. 28 | This can be especially useful during the implementation of complex data structures and algorithms (like balanced trees 29 | and graphs) where correctness depends on specific conditions being met. 30 | 31 | ### Features 32 | 33 | - A simple API to define preconditions, postconditions, and invariants 34 | - `require` and `ensure` functions check preconditions and postconditions 35 | - `requiref` and `ensuref` functions check preconditions and postconditions with formatted error messages 36 | - `requireCtx` and `ensureCtx` functions check preconditions and postconditions with a context string 37 | - `contract` and `contractWithErrorTolerance` functions check invariants 38 | - Checks are active in `Debug`, `ReleaseSafe`, and `ReleaseSmall` build modes to catch bugs 39 | - In `ReleaseFast` mode, all checks are removed at compile time to remove overhead 40 | 41 | > [!IMPORTANT] 42 | > Zig-DbC is in early development, so bugs and breaking API changes are expected. 43 | > Please use the [issues page](https://github.com/habedi/zig-dbc/issues) to report bugs or request features. 44 | 45 | --- 46 | 47 | ### Getting Started 48 | 49 | You can add Zig-DbC to your project and start using it by following the steps below. 50 | 51 | #### Installation 52 | 53 | Run the following command in the root directory of your project to download Zig-DbC: 54 | 55 | ```sh 56 | zig fetch --save=dbc "https://github.com/habedi/zig-dbc/archive/.tar.gz" 57 | ``` 58 | 59 | Replace `` with the desired branch or tag, like `main` (for the development version) or `v0.2.0` 60 | (for the latest release). 61 | This command will download zig-dbc and add it to Zig's global cache and update your project's `build.zig.zon` file. 62 | 63 | #### Adding to Build Script 64 | 65 | Next, modify your `build.zig` file to make zig-dbc available to your build target as a module. 66 | 67 | ```zig 68 | const std = @import("std"); 69 | 70 | pub fn build(b: *std.Build) void { 71 | const target = b.standardTargetOptions(.{}); 72 | const optimize = b.standardOptimizeOption(.{}); 73 | const exe = b.addExecutable(.{ 74 | .name = "your-zig-program", 75 | .root_source_file = b.path("src/main.zig"), 76 | .target = target, 77 | .optimize = optimize, 78 | }); 79 | 80 | // 1. Get the dependency object from the builder 81 | const zig_dbc_dep = b.dependency("dbc", .{}); 82 | 83 | // 2. Get Zig-DbC's top-level module 84 | const zig_dbc_module = zig_dbc_dep.module("dbc"); 85 | 86 | // 3. Add the module to your executable so you can @import("dbc") 87 | exe.root_module.addImport("dbc", zig_dbc_module); 88 | 89 | b.installArtifact(exe); 90 | } 91 | ``` 92 | 93 | #### Using Zig-DbC in Your Code 94 | 95 | Finally, you can `@import("dbc")` and start using it in your Zig code. 96 | 97 | ```zig 98 | const std = @import("std"); 99 | const dbc = @import("dbc"); 100 | 101 | pub fn MyStruct() type { 102 | return struct { 103 | const Self = @This(); 104 | field: i32, is_ready: bool, 105 | 106 | // The invariant guarantees that the object's state is always valid. 107 | // It's checked automatically by the `contract` function. 108 | fn invariant(self: Self) void { 109 | dbc.require(.{ self.field > 0, "Field must always be positive" }); 110 | } 111 | 112 | pub fn doSomething(self: *Self) !void { 113 | const old = .{ .field = self.field }; 114 | return dbc.contract(self, old, 115 | struct {fn run(ctx: @TypeOf(old), s: *Self) !void { 116 | // Precondition 117 | dbc.require(.{ s.is_ready, "Struct not ready" }); 118 | 119 | // ... method logic ... 120 | s.field += 1; 121 | 122 | // Postcondition 123 | dbc.ensure(.{ s.field > ctx.field, "Field must increase" }); 124 | }}.run); 125 | } 126 | }; 127 | } 128 | ``` 129 | 130 | --- 131 | 132 | ### Documentation 133 | 134 | You can find the API documentation for the latest release of Zig-DbC [here](https://habedi.github.io/zig-dbc/). 135 | 136 | Alternatively, you can use the `make docs` command to generate the documentation for the current version of Zig-DbC. 137 | This will generate HTML documentation in the `docs/api` directory, which you can serve locally with `make serve-docs` 138 | and view in a web browser. 139 | 140 | #### Validators 141 | 142 | Zig-DbC supports reusable validators in `require` and `ensure` functions by passing as argument either: 143 | 144 | - a function with signature `fn(T) bool`, or 145 | - a struct value with a `run` function with this signature: `pub fn run(self, T) bool`. 146 | 147 | ### Examples 148 | 149 | Check out the [examples](examples/) directory for example usages of Zig-DbC. 150 | 151 | --- 152 | 153 | ### Contributing 154 | 155 | See [CONTRIBUTING.md](CONTRIBUTING.md) for details on how to make a contribution. 156 | 157 | ### License 158 | 159 | Zig-DbC is licensed under the MIT License (see [LICENSE](LICENSE)). 160 | 161 | ### Acknowledgements 162 | 163 | * The chain links logo is from [SVG Repo](https://www.svgrepo.com/svg/9153/chain-links). 164 | -------------------------------------------------------------------------------- /src/dbc.zig: -------------------------------------------------------------------------------- 1 | //! ## Zig-DbC -- A Design by Contract Library for Zig 2 | //! 3 | //! This module provides a set of functions to use design by contract (DbC) principles in Zig programs. 4 | //! 5 | //! ### Features 6 | //! 7 | //! - Preconditions: Assert conditions that must be true when entering a function (at call time) 8 | //! - Postconditions: Assert conditions that must be true when exiting a function (at return time) 9 | //! - Invariants: Assert structural consistency of objects before and after method calls 10 | //! - Zero-cost abstractions: All contract checks are compiled out in `ReleaseFast` mode 11 | //! - Error tolerance: Optional mode to preserve partial state changes when errors occur 12 | //! - Formatted messages: Compile-time formatted assertion messages with automatic context capture 13 | //! 14 | //! ### Usage 15 | //! 16 | //! #### Basic Assertions 17 | //! 18 | //! ```zig 19 | //! const dbc = @import("dbc.zig"); 20 | //! 21 | //! fn divide(a: f32, b: f32) f32 { 22 | //! dbc.require(.{b != 0.0, "Division by zero"}); 23 | //! const result = a / b; 24 | //! dbc.ensure(.{!std.math.isNan(result), "Result must be a valid number"}); 25 | //! return result; 26 | //! } 27 | //! ``` 28 | //! 29 | //! #### Formatted Assertions 30 | //! 31 | //! ```zig 32 | //! fn sqrt(x: f64) f64 { 33 | //! dbc.requiref(x >= 0.0, "Square root requires non-negative input, got {d}", .{x}); 34 | //! const result = std.math.sqrt(x); 35 | //! dbc.ensuref(!std.math.isNan(result), "Expected valid result, got {d}", .{result}); 36 | //! return result; 37 | //! } 38 | //! ``` 39 | //! 40 | //! #### Contract-based Methods 41 | //! 42 | //! ```zig 43 | //! const BankAccount = struct { 44 | //! balance: u64, 45 | //! is_open: bool, 46 | //! 47 | //! // Invariant - checked before and after each contract 48 | //! fn invariant(self: BankAccount) void { 49 | //! dbc.require(.{ if (!self.is_open) self.balance == 0 else true, 50 | //! "Closed accounts must have zero balance" }); 51 | //! } 52 | //! 53 | //! pub fn withdraw(self: *BankAccount, amount: u64) !void { 54 | //! const old_state = .{ .balance = self.balance }; 55 | //! return dbc.contract(self, old_state, struct { 56 | //! fn run(ctx: @TypeOf(old_state), account: *BankAccount) !void { 57 | //! // Enhanced preconditions with context 58 | //! dbc.requiref(account.is_open, "Account must be open", .{}); 59 | //! dbc.requiref(amount <= account.balance, 60 | //! "Insufficient funds: requested {d}, available {d}", 61 | //! .{amount, account.balance}); 62 | //! 63 | //! // Business logic 64 | //! account.balance -= amount; 65 | //! 66 | //! // Enhanced postconditions 67 | //! dbc.ensuref(account.balance == ctx.balance - amount, 68 | //! "Balance mismatch: expected {d}, got {d}", 69 | //! .{ctx.balance - amount, account.balance}); 70 | //! } 71 | //! }.run); 72 | //! } 73 | //! }; 74 | //! ``` 75 | 76 | const std = @import("std"); 77 | const builtin = @import("builtin"); 78 | 79 | /// Assert a precondition that must be true at function entry. 80 | /// 81 | /// Can be called in two ways: 82 | /// 1. With a boolean condition: `require(.{condition, "error message"})` 83 | /// 2. With a reusable validator: `require(.{validator, value, "error message"})` 84 | /// 85 | /// A validator can be a function pointer or a struct with a public `run` method that 86 | /// accepts `value` and returns a boolean. 87 | /// 88 | /// Only active in `Debug`, `ReleaseSafe`, and `ReleaseSmall` builds. 89 | /// 90 | /// Panics with the provided message if the condition is false or the validator returns false. 91 | pub inline fn require(args: anytype) void { 92 | if (builtin.mode == .ReleaseFast) return; 93 | 94 | comptime { 95 | const info = @typeInfo(@TypeOf(args)); 96 | if (info != .@"struct" or info.@"struct".is_tuple == false) { 97 | @compileError("arguments to require must be a tuple, like require(.{condition, msg})"); 98 | } 99 | } 100 | 101 | const condition = blk: { 102 | if (args.len == 2) { 103 | if (@TypeOf(args[0]) != bool) @compileError("Expected a boolean condition for 2-argument require"); 104 | break :blk args[0]; 105 | } else if (args.len == 3) { 106 | const validator = args[0]; 107 | const value = args[1]; 108 | const ValidatorType = @TypeOf(validator); 109 | 110 | switch (@typeInfo(ValidatorType)) { 111 | .@"fn" => break :blk validator(value), 112 | .@"struct" => { 113 | if (@hasDecl(ValidatorType, "run")) { 114 | break :blk validator.run(value); 115 | } else { 116 | @compileError("Validator struct must have a public 'run' method"); 117 | } 118 | }, 119 | .pointer => |ptr_info| { 120 | if (@typeInfo(ptr_info.child) == .@"fn") { 121 | break :blk validator(value); 122 | } else { 123 | @compileError("Validator must be a function or a struct with a public 'run' method"); 124 | } 125 | }, 126 | else => @compileError("Validator must be a function or a struct with a public 'run' method"), 127 | } 128 | } else { 129 | @compileError("require expects a tuple with 2 or 3 arguments"); 130 | } 131 | }; 132 | 133 | const msg = comptime blk: { 134 | if (args.len == 2) { 135 | break :blk args[1]; 136 | } else { 137 | break :blk args[2]; 138 | } 139 | }; 140 | 141 | if (!condition) @panic(msg); 142 | } 143 | 144 | /// Assert a precondition with formatted message support. 145 | /// 146 | /// Provides compile-time formatted messages for better debugging experience. 147 | /// All formatting is done at compile-time and completely eliminated in ReleaseFast. 148 | pub inline fn requiref(condition: bool, comptime fmt: []const u8, args: anytype) void { 149 | if (builtin.mode == .ReleaseFast) return; 150 | 151 | if (!condition) { 152 | const msg = comptime std.fmt.comptimePrint(fmt, args); 153 | @panic(msg); 154 | } 155 | } 156 | 157 | /// Assert a precondition with contextual information captured in the message. 158 | /// 159 | /// Automatically includes a context string in the panic message. 160 | /// Pass a comptime string (typically a literal) to avoid allocations. 161 | pub inline fn requireCtx(condition: bool, comptime context: []const u8) void { 162 | if (builtin.mode == .ReleaseFast) return; 163 | 164 | if (!condition) { 165 | const msg = std.fmt.comptimePrint("Precondition failed: {s}", .{context}); 166 | @panic(msg); 167 | } 168 | } 169 | 170 | /// Assert a postcondition that must be true at function exit. 171 | /// 172 | /// Can be called in two ways: 173 | /// 1. With a boolean condition: `ensure(.{condition, "error message"})` 174 | /// 2. With a reusable validator: `ensure(.{validator, value, "error message"})` 175 | /// 176 | /// A validator can be a function pointer or a struct with a public `run` method that 177 | /// accepts `value` and returns a boolean. 178 | /// 179 | /// Only active in `Debug`, `ReleaseSafe`, and `ReleaseSmall` builds. 180 | /// 181 | /// Panics with the provided message if the condition is false or the validator returns false. 182 | pub inline fn ensure(args: anytype) void { 183 | if (builtin.mode == .ReleaseFast) return; 184 | 185 | comptime { 186 | const info = @typeInfo(@TypeOf(args)); 187 | if (info != .@"struct" or info.@"struct".is_tuple == false) { 188 | @compileError("arguments to ensure must be a tuple, like ensure(.{condition, msg})"); 189 | } 190 | } 191 | 192 | const condition = blk: { 193 | if (args.len == 2) { 194 | if (@TypeOf(args[0]) != bool) @compileError("Expected a boolean condition for 2-argument ensure"); 195 | break :blk args[0]; 196 | } else if (args.len == 3) { 197 | const validator = args[0]; 198 | const value = args[1]; 199 | const ValidatorType = @TypeOf(validator); 200 | 201 | switch (@typeInfo(ValidatorType)) { 202 | .@"fn" => break :blk validator(value), 203 | .@"struct" => { 204 | if (@hasDecl(ValidatorType, "run")) { 205 | break :blk validator.run(value); 206 | } else { 207 | @compileError("Validator struct must have a public 'run' method"); 208 | } 209 | }, 210 | .pointer => |ptr_info| { 211 | if (@typeInfo(ptr_info.child) == .@"fn") { 212 | break :blk validator(value); 213 | } else { 214 | @compileError("Validator must be a function or a struct with a public 'run' method"); 215 | } 216 | }, 217 | else => @compileError("Validator must be a function or a struct with a public 'run' method"), 218 | } 219 | } else { 220 | @compileError("ensure expects a tuple with 2 or 3 arguments"); 221 | } 222 | }; 223 | 224 | const msg = comptime blk: { 225 | if (args.len == 2) { 226 | break :blk args[1]; 227 | } else { 228 | break :blk args[2]; 229 | } 230 | }; 231 | 232 | if (!condition) @panic(msg); 233 | } 234 | 235 | /// Assert a postcondition with formatted message support. 236 | /// 237 | /// Provides compile-time formatted messages for better debugging experience. 238 | /// All formatting is done at compile-time and completely eliminated in ReleaseFast. 239 | pub inline fn ensuref(condition: bool, comptime fmt: []const u8, args: anytype) void { 240 | if (builtin.mode == .ReleaseFast) return; 241 | 242 | if (!condition) { 243 | const msg = comptime std.fmt.comptimePrint(fmt, args); 244 | @panic(msg); 245 | } 246 | } 247 | 248 | /// Assert a postcondition with contextual information captured in the message. 249 | /// 250 | /// Automatically includes a context string in the panic message. 251 | /// Pass a comptime string (typically a literal) to avoid allocations. 252 | pub inline fn ensureCtx(condition: bool, comptime context: []const u8) void { 253 | if (builtin.mode == .ReleaseFast) return; 254 | 255 | if (!condition) { 256 | const msg = std.fmt.comptimePrint("Postcondition failed: {s}", .{context}); 257 | @panic(msg); 258 | } 259 | } 260 | 261 | /// Execute a function with design by contract semantics. 262 | /// 263 | /// This function provides: 264 | /// - Automatic invariant checking (if the object has an `invariant` method) 265 | /// - Captured pre-state for postconditions 266 | /// - Error handling that preserves invariants 267 | /// 268 | /// Parameters: 269 | /// - `self`: Object instance (must be a pointer type for mutation) 270 | /// - `old_state`: Captured state before the operation 271 | /// - `operation`: Function to execute with signature `fn(old_state, self) ReturnType` 272 | pub inline fn contract(self: anytype, old_state: anytype, operation: anytype) @typeInfo(@TypeOf(operation)).@"fn".return_type.? { 273 | if (builtin.mode == .ReleaseFast) { 274 | return operation(old_state, self); 275 | } 276 | 277 | // Check pre-invariant if available 278 | if (@hasDecl(@TypeOf(self.*), "invariant")) { 279 | self.invariant(); 280 | } 281 | 282 | // Execute the operation 283 | const result = operation(old_state, self); 284 | 285 | // Check post-invariant if available 286 | if (@hasDecl(@TypeOf(self.*), "invariant")) { 287 | self.invariant(); 288 | } 289 | 290 | return result; 291 | } 292 | 293 | /// Execute a function with design by contract semantics and error tolerance. 294 | /// 295 | /// Similar to `contract` but if an error occurs during the operation, the invariant 296 | /// is still checked to guarantee the object remains in a valid state. 297 | /// 298 | /// Parameters: 299 | /// - `self`: Object instance (must be a pointer type for mutation) 300 | /// - `old_state`: Captured state before the operation 301 | /// - `operation`: Function to execute with signature `fn(old_state, self) ReturnType` 302 | pub inline fn contractWithErrorTolerance(self: anytype, old_state: anytype, operation: anytype) @typeInfo(@TypeOf(operation)).@"fn".return_type.? { 303 | if (builtin.mode == .ReleaseFast) { 304 | return operation(old_state, self); 305 | } 306 | 307 | // Check pre-invariant if available 308 | if (@hasDecl(@TypeOf(self.*), "invariant")) { 309 | self.invariant(); 310 | } 311 | 312 | // Execute the operation with error handling 313 | const result = operation(old_state, self) catch |err| { 314 | // Even on error, ensure invariant is maintained 315 | if (@hasDecl(@TypeOf(self.*), "invariant")) { 316 | self.invariant(); 317 | } 318 | return err; 319 | }; 320 | 321 | // Check post-invariant if available 322 | if (@hasDecl(@TypeOf(self.*), "invariant")) { 323 | self.invariant(); 324 | } 325 | 326 | return result; 327 | } 328 | 329 | // Tests for `dbc` module 330 | 331 | const testing = std.testing; 332 | 333 | /// Example implementation demonstrating DbC principles with formatted messages. 334 | /// This Account struct showcases preconditions, postconditions, and invariants. 335 | const Account = struct { 336 | balance: u32, 337 | is_active: bool, 338 | 339 | /// Invariant: inactive accounts must have zero balance. 340 | /// This is automatically checked before and after each contracted method. 341 | fn invariant(self: Account) void { 342 | if (!self.is_active) { 343 | requiref(self.balance == 0, "Inactive account has non-zero balance: {d}", .{self.balance}); 344 | } 345 | } 346 | 347 | /// Deposit money into the account. 348 | /// Demonstrates preconditions and postconditions with formatted messages. 349 | pub fn deposit(self: *Account, amount: u32) void { 350 | const old = .{ .balance = self.balance }; 351 | return contract(self, old, struct { 352 | fn run(ctx: @TypeOf(old), s: *Account) void { 353 | // Enhanced preconditions with context 354 | requiref(s.is_active, "Cannot deposit to inactive account", .{}); 355 | requiref(amount > 0, "Deposit amount must be positive, got {d}", .{amount}); 356 | 357 | // Business logic 358 | s.balance += amount; 359 | 360 | // Enhanced postconditions with before/after context 361 | ensuref(s.balance == ctx.balance + amount, "Balance mismatch: expected {d}, got {d}", .{ ctx.balance + amount, s.balance }); 362 | } 363 | }.run); 364 | } 365 | 366 | /// Withdraw money from the account. 367 | /// Demonstrates error handling with enhanced error messages. 368 | pub fn withdraw(self: *Account, amount: u32) !void { 369 | const old = .{ .balance = self.balance }; 370 | return contract(self, old, struct { 371 | fn run(ctx: @TypeOf(old), s: *Account) !void { 372 | // Enhanced preconditions with detailed context 373 | requiref(s.is_active, "Cannot withdraw from inactive account", .{}); 374 | requiref(amount <= s.balance, "Insufficient funds: requested {d}, available {d}", .{ amount, s.balance }); 375 | 376 | // Business logic 377 | s.balance -= amount; 378 | 379 | // Enhanced postconditions 380 | ensuref(s.balance == ctx.balance - amount, "Withdrawal calculation error: expected {d}, got {d}", .{ ctx.balance - amount, s.balance }); 381 | } 382 | }.run); 383 | } 384 | 385 | /// Close the account. 386 | /// Demonstrates context capture for clear error messages. 387 | pub fn close(self: *Account) void { 388 | return contract(self, null, struct { 389 | fn run(_: @TypeOf(null), s: *Account) void { 390 | requireCtx(s.balance == 0, "s.balance == 0"); 391 | s.is_active = false; 392 | ensureCtx(!s.is_active, "!s.is_active"); 393 | } 394 | }.run); 395 | } 396 | }; 397 | 398 | // Comprehensive test suite demonstrating various contract scenarios 399 | test "successful deposit and withdraw with formatted messages" { 400 | if (builtin.mode == .ReleaseFast) return; 401 | 402 | var acc = Account{ .balance = 100, .is_active = true }; 403 | acc.deposit(50); 404 | try testing.expectEqual(@as(u32, 150), acc.balance); 405 | 406 | try acc.withdraw(20); 407 | try testing.expectEqual(@as(u32, 130), acc.balance); 408 | } 409 | 410 | test "formatted error messages in precondition failures" { 411 | if (builtin.mode == .ReleaseFast) return; 412 | 413 | const TestStruct = struct { 414 | fn testPanic() void { 415 | var acc = Account{ .balance = 0, .is_active = false }; 416 | acc.deposit(100); 417 | } 418 | }; 419 | 420 | try std.testing.expectPanic(TestStruct.testPanic); 421 | } 422 | 423 | test "formatted error messages in postcondition failures" { 424 | if (builtin.mode == .ReleaseFast) return; 425 | 426 | const BuggyAccount = struct { 427 | balance: u32, 428 | is_active: bool, 429 | fn invariant(_: @This()) void {} 430 | pub fn withdraw(self: *@This(), amount: u32) void { 431 | const old = .{ .balance = self.balance }; 432 | return contract(self, old, struct { 433 | fn run(ctx: @TypeOf(old), s: *@This()) void { 434 | s.balance -= amount; 435 | s.balance += 1; // Intentional bug 436 | ensuref(s.balance == ctx.balance - amount, "Balance error: expected {d}, got {d}", .{ ctx.balance - amount, s.balance }); 437 | } 438 | }.run); 439 | } 440 | }; 441 | 442 | const TestStruct = struct { 443 | fn testPanic() void { 444 | var buggy_acc = BuggyAccount{ .balance = 100, .is_active = true }; 445 | buggy_acc.withdraw(50); 446 | } 447 | }; 448 | 449 | try std.testing.expectPanic(TestStruct.testPanic); 450 | } 451 | 452 | test "context capture assertions" { 453 | if (builtin.mode == .ReleaseFast) return; 454 | 455 | const TestStruct = struct { 456 | fn testPanic() void { 457 | const x: i32 = 5; 458 | const y: i32 = 3; 459 | requireCtx(x < y, "x < y"); 460 | } 461 | }; 462 | 463 | try std.testing.expectPanic(TestStruct.testPanic); 464 | } 465 | 466 | test "formatted require and ensure functions" { 467 | if (builtin.mode == .ReleaseFast) return; 468 | 469 | const x: f32 = 4.0; 470 | requiref(x >= 0.0, "Square root input must be non-negative, got {d}", .{x}); 471 | 472 | const result = @sqrt(x); 473 | ensuref(result * result == x, "Square root verification failed: {d}^2 != {d}", .{ result, x }); 474 | } 475 | 476 | test "formatted messages are eliminated in ReleaseFast" { 477 | if (builtin.mode != .ReleaseFast) return; 478 | 479 | // These would normally cause panics but should be completely compiled out 480 | requiref(false, "This should not panic in ReleaseFast mode", .{}); 481 | ensuref(false, "This should also not panic in ReleaseFast mode", .{}); 482 | requireCtx(false, "false"); 483 | ensureCtx(false, "false"); 484 | } 485 | 486 | test "reusable validators with enhanced error messages" { 487 | if (builtin.mode == .ReleaseFast) return; 488 | 489 | const isPositive = struct { 490 | fn check(n: i32) bool { 491 | return n > 0; 492 | } 493 | }.check; 494 | 495 | require(.{ isPositive, 10, "10 should be positive" }); 496 | ensure(.{ isPositive, 1, "1 should be positive" }); 497 | 498 | const IsLongerThan = struct { 499 | min_len: usize, 500 | fn run(self: @This(), s: []const u8) bool { 501 | return s.len > self.min_len; 502 | } 503 | }; 504 | 505 | const longerThan5 = IsLongerThan{ .min_len = 5 }; 506 | require(.{ longerThan5, "hello world", "string should be longer than 5" }); 507 | ensure(.{ longerThan5, "a long string", "string should be longer than 5" }); 508 | } 509 | 510 | test "requiref and ensuref with simple boolean conditions" { 511 | if (builtin.mode == .ReleaseFast) return; 512 | 513 | const x: f64 = 3.14159; 514 | const y: i32 = 42; 515 | 516 | require(.{ x > 0.0, "x must be positive" }); 517 | require(.{ y >= 0, "y must be non-negative" }); 518 | 519 | const result = x * 2; 520 | ensure(.{ result > x, "Result should be greater than input" }); 521 | } 522 | 523 | test "context capture with complex expressions" { 524 | if (builtin.mode == .ReleaseFast) return; 525 | 526 | const a: f64 = 3.0; 527 | const b: f64 = 4.0; 528 | const c: f64 = 5.0; 529 | 530 | requireCtx(a * a + b * b == c * c, "a * a + b * b == c * c"); 531 | 532 | const result = @sqrt(a * a + b * b); 533 | ensureCtx(@abs(result - c) < 0.0001, "@abs(result - c) < 0.0001"); 534 | } 535 | 536 | test "reusable validators with all API variants" { 537 | if (builtin.mode == .ReleaseFast) return; 538 | 539 | const IsInRange = struct { 540 | min: i32, 541 | max: i32, 542 | fn run(self: @This(), value: i32) bool { 543 | return value >= self.min and value <= self.max; 544 | } 545 | }; 546 | 547 | const range_validator = IsInRange{ .min = 10, .max = 50 }; 548 | const test_value: i32 = 25; 549 | 550 | // Simple API 551 | require(.{ range_validator, test_value, "Value must be in range [10, 50]" }); 552 | 553 | // Context API 554 | requireCtx(test_value % 5 == 0, "test_value % 5 == 0"); 555 | 556 | const result = test_value * 2; 557 | ensure(.{ range_validator, result, "Result must be in range [10, 50]" }); 558 | ensureCtx(result == test_value * 2, "result == test_value * 2"); 559 | } 560 | --------------------------------------------------------------------------------