Skip to content

Commit 025dce2

Browse files
authored
The first revision (#4)
1 parent c3cd3e7 commit 025dce2

File tree

10 files changed

+654
-352
lines changed

10 files changed

+654
-352
lines changed

README.md

Lines changed: 30 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,22 +20,23 @@ A Design by Contract Library for Zig
2020
---
2121

2222
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 software written in Zig
24-
programming language.
25-
It provides a simple and idiomatic API for defining *preconditions*, *postconditions*, and *invariants* that can be
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
2625
checked at runtime.
2726

2827
A common use case for DbC (and by extension Zig-DbC) is adding checks that guarantee the code behaves as intended.
29-
This can be especially useful, for example, during the implementation of complex data structures and algorithms
30-
(like balanced trees and graphs) where correctness depends on specific conditions being met.
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.
3130

3231
### Features
3332

34-
* A simple API to define `preconditions`, `postconditions`, and `invariants`
35-
* Contracts are active in `Debug`, `ReleaseSafe`, and `ReleaseSmall` modes to catch bugs early
36-
* In `ReleaseFast` mode, all contract checks are removed at compile time
37-
* The `contract` function passes errors from your code to the caller
38-
* An optional mode to handle partial state changes in functions that can return errors
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
3940

4041
> [!IMPORTANT]
4142
> Zig-DbC is in early development, so bugs and breaking API changes are expected.
@@ -55,7 +56,7 @@ Run the following command in the root directory of your project to download Zig-
5556
zig fetch --save=dbc "https://github.com/habedi/zig-dbc/archive/<branch_or_tag>.tar.gz"
5657
```
5758

58-
Replace `<branch_or_tag>` with the desired branch or tag, like `main` (for the development version) or `v0.1.0`
59+
Replace `<branch_or_tag>` with the desired branch or tag, like `main` (for the development version) or `v0.2.0`
5960
(for the latest release).
6061
This command will download zig-dbc and add it to Zig's global cache and update your project's `build.zig.zon` file.
6162

@@ -69,7 +70,6 @@ const std = @import("std");
6970
pub fn build(b: *std.Build) void {
7071
const target = b.standardTargetOptions(.{});
7172
const optimize = b.standardOptimizeOption(.{});
72-
7373
const exe = b.addExecutable(.{
7474
.name = "your-zig-program",
7575
.root_source_file = b.path("src/main.zig"),
@@ -83,7 +83,7 @@ pub fn build(b: *std.Build) void {
8383
// 2. Get Zig-DbC's top-level module
8484
const zig_dbc_module = zig_dbc_dep.module("dbc");
8585
86-
// 3. Add the module to your executable so you can @import("zig-dbc")
86+
// 3. Add the module to your executable so you can @import("dbc")
8787
exe.root_module.addImport("dbc", zig_dbc_module);
8888
8989
b.installArtifact(exe);
@@ -95,32 +95,33 @@ pub fn build(b: *std.Build) void {
9595
Finally, you can `@import("dbc")` and start using it in your Zig code.
9696

9797
```zig
98+
const std = @import("std");
9899
const dbc = @import("dbc");
99100
100101
pub fn MyStruct() type {
101102
return struct {
102103
const Self = @This();
103-
field: i32,
104-
is_ready: bool,
104+
field: i32, is_ready: bool,
105105
106+
// The invariant guarantees that the object's state is always valid.
107+
// It's checked automatically by the `contract` function.
106108
fn invariant(self: Self) void {
107-
dbc.require(self.field > 0, "Field must always be positive");
109+
dbc.require(.{ self.field > 0, "Field must always be positive" });
108110
}
109111
110112
pub fn doSomething(self: *Self) !void {
111113
const old = .{ .field = self.field };
112-
return dbc.contract(self, @TypeOf(old), old, struct {
113-
fn run(ctx: @TypeOf(old), s: *Self) !void {
114+
return dbc.contract(self, old,
115+
struct {fn run(ctx: @TypeOf(old), s: *Self) !void {
114116
// Precondition
115-
dbc.require(s.is_ready, "Struct not ready");
117+
dbc.require(.{ s.is_ready, "Struct not ready" });
116118
117119
// ... method logic ...
118120
s.field += 1;
119121
120122
// Postcondition
121-
dbc.ensure(s.field > ctx.field, "Field must increase");
122-
}
123-
}.run);
123+
dbc.ensure(.{ s.field > ctx.field, "Field must increase" });
124+
}}.run);
124125
}
125126
};
126127
}
@@ -136,6 +137,13 @@ Alternatively, you can use the `make docs` command to generate the documentation
136137
This will generate HTML documentation in the `docs/api` directory, which you can serve locally with `make serve-docs`
137138
and view in a web browser.
138139

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+
139147
### Examples
140148

141149
Check out the [examples](examples/) directory for example usages of Zig-DbC.

build.zig.zon

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
.{
22
.name = .zig_dbc,
3-
.version = "0.1.0",
3+
.version = "0.2.0",
44
.fingerprint = 0x8e5de01b7c473456, // Changing this has security and trust implications.
55
.minimum_zig_version = "0.14.1",
66
.paths = .{

examples/README.md

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
## Zig-DbC Examples
22

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 |
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 |

examples/e1_bounded_queue.zig

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const std = @import("std");
2+
const builtin = @import("builtin");
23
const dbc = @import("dbc");
34

45
pub fn BoundedQueue(comptime T: type, comptime capacity: usize) type {
@@ -13,7 +14,7 @@ pub fn BoundedQueue(comptime T: type, comptime capacity: usize) type {
1314
// An invariant is a condition that must hold true before and after every method call.
1415
// This invariant checks that the queue's state is valid.
1516
fn invariant(self: Self) void {
16-
dbc.require(self.count <= capacity, "Queue count exceeds capacity");
17+
dbc.require(.{ self.count <= capacity, "Queue count exceeds capacity" });
1718
}
1819

1920
pub fn enqueue(self: *Self, item: T) void {
@@ -22,15 +23,15 @@ pub fn BoundedQueue(comptime T: type, comptime capacity: usize) type {
2223
return dbc.contract(self, old, struct {
2324
// Preconditions are checked at the start of a function.
2425
fn run(ctx: @TypeOf(old), s: *Self) void {
25-
dbc.require(s.count < capacity, "Cannot enqueue to a full queue");
26+
dbc.require(.{ s.count < capacity, "Cannot enqueue to a full queue" });
2627

2728
// Core method logic
2829
s.items[s.tail] = ctx.item;
2930
s.tail = (s.tail + 1) % capacity;
3031
s.count += 1;
3132

3233
// Postconditions are checked at the end of a function.
33-
dbc.ensure(s.count == ctx.count + 1, "Enqueue failed to increment count");
34+
dbc.ensure(.{ s.count == ctx.count + 1, "Enqueue failed to increment count" });
3435
}
3536
}.run);
3637
}
@@ -39,13 +40,13 @@ pub fn BoundedQueue(comptime T: type, comptime capacity: usize) type {
3940
const old = .{ .count = self.count };
4041
return dbc.contract(self, old, struct {
4142
fn run(ctx: @TypeOf(old), s: *Self) T {
42-
dbc.require(s.count > 0, "Cannot dequeue from an empty queue");
43+
dbc.require(.{ s.count > 0, "Cannot dequeue from an empty queue" });
4344

4445
const dequeued_item = s.items[s.head];
4546
s.head = (s.head + 1) % capacity;
4647
s.count -= 1;
4748

48-
dbc.ensure(s.count == ctx.count - 1, "Dequeue failed to decrement count");
49+
dbc.ensure(.{ s.count == ctx.count - 1, "Dequeue failed to decrement count" });
4950
return dequeued_item;
5051
}
5152
}.run);
@@ -54,7 +55,6 @@ pub fn BoundedQueue(comptime T: type, comptime capacity: usize) type {
5455
}
5556

5657
pub fn main() !void {
57-
const builtin = @import("builtin");
5858
const MyQueue = BoundedQueue(u32, 3);
5959
var q = MyQueue{};
6060

examples/e2_file_parser.zig

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const std = @import("std");
2+
const builtin = @import("builtin");
23
const dbc = @import("dbc");
34

45
const Allocator = std.mem.Allocator;
@@ -14,7 +15,7 @@ const FileParser = struct {
1415
// a file path must exist. This helps maintain data integrity.
1516
fn invariant(self: Self) void {
1617
if (self.is_parsed) {
17-
dbc.require(self.file_path != null, "Parsed file must have a path");
18+
dbc.require(.{ self.file_path != null, "Parsed file must have a path" });
1819
}
1920
}
2021

@@ -53,7 +54,7 @@ const FileParser = struct {
5354
fn run(ctx: @TypeOf(old), s: *Self) !void {
5455
// A precondition to ensure the parser is not already in a parsed state.
5556
// This prevents re-parsing without calling `reset`.
56-
dbc.require(!s.is_parsed, "Parser is already in a parsed state. Call reset first.");
57+
dbc.require(.{ !s.is_parsed, "Parser is already in a parsed state. Call reset first." });
5758

5859
const file = try std.fs.cwd().openFile(ctx.path, .{});
5960
defer file.close();
@@ -71,14 +72,13 @@ const FileParser = struct {
7172
s.is_parsed = true;
7273

7374
// A postcondition to verify that new lines were read successfully.
74-
dbc.ensure(s.lines.items.len > ctx.old_lines_count, "Parsing failed to read any new lines.");
75+
dbc.ensure(.{ s.lines.items.len > ctx.old_lines_count, "Parsing failed to read any new lines." });
7576
}
7677
}.run);
7778
}
7879
};
7980

8081
pub fn main() !void {
81-
const builtin = @import("builtin");
8282
std.debug.print("Contracts are active in this build mode: {}\n", .{builtin.mode != .ReleaseFast});
8383

8484
const temp_dir_path = "temp";

examples/e3_linked_list.zig

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
const std = @import("std");
2+
const builtin = @import("builtin");
23
const dbc = @import("dbc");
34

45
const Allocator = std.mem.Allocator;
@@ -24,7 +25,7 @@ const SinglyLinkedList = struct {
2425
actual_count += 1;
2526
current = node.next;
2627
}
27-
dbc.require(self.count == actual_count, "List count is inconsistent with node count.");
28+
dbc.require(.{ self.count == actual_count, "List count is inconsistent with node count." });
2829
}
2930

3031
pub fn init(allocator: Allocator) Self {
@@ -57,7 +58,7 @@ const SinglyLinkedList = struct {
5758
s.count += 1;
5859

5960
// The postcondition verifies that the count was correctly incremented.
60-
dbc.ensure(s.count == ctx.count + 1, "Push_front failed to decrement count.");
61+
dbc.ensure(.{ s.count == ctx.count + 1, "Push_front failed to decrement count." });
6162
}
6263
}.run);
6364
}
@@ -68,7 +69,7 @@ const SinglyLinkedList = struct {
6869
const old = .{ .count = self.count };
6970
return dbc.contract(self, old, struct {
7071
fn run(ctx: @TypeOf(old), s: *Self) u32 {
71-
dbc.require(s.head != null, "Cannot pop from an empty list.");
72+
dbc.require(.{ s.head != null, "Cannot pop from an empty list." });
7273

7374
const head_node = s.head.?;
7475
const value = head_node.data;
@@ -77,7 +78,7 @@ const SinglyLinkedList = struct {
7778
s.count -= 1;
7879

7980
// The postcondition verifies that the count was correctly decremented.
80-
dbc.ensure(s.count == ctx.count - 1, "Pop_front failed to decrement count.");
81+
dbc.ensure(.{ s.count == ctx.count - 1, "Pop_front failed to decrement count." });
8182

8283
return value;
8384
}
@@ -86,7 +87,6 @@ const SinglyLinkedList = struct {
8687
};
8788

8889
pub fn main() !void {
89-
const builtin = @import("builtin");
9090
std.debug.print("Contracts are active in this build mode: {}\n", .{builtin.mode != .ReleaseFast});
9191

9292
var gpa = std.heap.GeneralPurposeAllocator(.{}){};

0 commit comments

Comments
 (0)