├── .gitattributes ├── .github └── workflows │ └── zig.yml ├── .gitignore ├── LICENSE ├── README.md ├── build.zig ├── example.zig ├── src └── lib.zig ├── zig.mod └── zigmod.lock /.gitattributes: -------------------------------------------------------------------------------- 1 | * text=auto 2 | *.zig text eol=lf 3 | zig.mod text eol=lf 4 | zigmod.* text eol=lf 5 | zig.mod linguist-language=YAML 6 | zig.mod gitlab-language=yaml 7 | -------------------------------------------------------------------------------- /.github/workflows/zig.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: 3 | push: 4 | branches: [master] 5 | pull_request: 6 | branches: [master] 7 | 8 | jobs: 9 | test: 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: [ubuntu-latest, macos-latest, windows-latest] 14 | runs-on: ${{matrix.os}} 15 | steps: 16 | - uses: actions/checkout@v2 17 | - uses: goto-bus-stop/setup-zig@v1 18 | with: 19 | version: master 20 | 21 | - run: zig version 22 | - run: zig env 23 | - uses: yrashk/actions-setup-zigmod@v1_1 24 | with: 25 | version: 91 26 | - run: zigmod version 27 | - run: zigmod ci 28 | - run: zig build test 29 | lint: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - uses: actions/checkout@v2 33 | - uses: goto-bus-stop/setup-zig@v1 34 | with: 35 | version: master 36 | - run: zig fmt --check src/*.zig 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | *.swp 2 | zig-* 3 | .zigmod 4 | deps.zig 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 Yurii Rashkovskii 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Compile-time Contracts for Zig 2 | 3 | Compile-time contracts address the duck-typing problem in Zig. When you have 4 | a function taking `type` or `anytype` parameters, it's not trivial to tell 5 | what that type should be like. 6 | 7 | One can write good documentation but that requires extra maintenance efforts 8 | and is not very formalized. 9 | 10 | There's `std.meta.trait` but it is limited in terms of what it can do, and particulary, 11 | it's not great at identifying the cause of contract (trait) and have limited composition. 12 | 13 | This library offers simple, composable contracts that can track causes of contract violation. 14 | 15 | There are two primary ways contracts can be used. They can be used directly in function's body: 16 | 17 | ```zig 18 | const contracts = @import("./src/lib.zig"); 19 | 20 | fn body_contract(t: anytype) void { 21 | comptime contracts.require(contracts.is(@TypeOf(t), u8)); 22 | } 23 | ``` 24 | 25 | or function's signature: 26 | 27 | ```zig 28 | fn signature_contract(t: anytype) contracts.RequiresAndReturns( 29 | contracts.is(@TypeOf(t), u8), 30 | void, 31 | ) {} 32 | 33 | pub fn main() void { 34 | body_contract(@as(u8, 1)); 35 | signature_contract(@as(u8, 1)); 36 | } 37 | ``` 38 | -------------------------------------------------------------------------------- /build.zig: -------------------------------------------------------------------------------- 1 | const std = @import("std"); 2 | 3 | pub fn build(b: *std.build.Builder) void { 4 | // Standard release options allow the person running `zig build` to select 5 | // between Debug, ReleaseSafe, ReleaseFast, and ReleaseSmall. 6 | const mode = b.standardReleaseOptions(); 7 | 8 | const lib = b.addStaticLibrary("zig-ctc", "src/lib.zig"); 9 | lib.setBuildMode(mode); 10 | lib.install(); 11 | 12 | const lib_tests = b.addTest("src/lib.zig"); 13 | lib_tests.setBuildMode(mode); 14 | 15 | const test_step = b.step("test", "Run library tests"); 16 | test_step.dependOn(&lib_tests.step); 17 | } 18 | -------------------------------------------------------------------------------- /example.zig: -------------------------------------------------------------------------------- 1 | const contracts = @import("./src/lib.zig"); 2 | 3 | fn body_contract(t: anytype) void { 4 | comptime contracts.require(contracts.is(@TypeOf(t), u8)); 5 | } 6 | 7 | fn signature_contract(t: anytype) contracts.RequiresAndReturns( 8 | contracts.is(@TypeOf(t), u8), 9 | void, 10 | ) {} 11 | 12 | pub fn main() void { 13 | body_contract(@as(u8, 1)); 14 | signature_contract(@as(u8, 1)); 15 | } 16 | -------------------------------------------------------------------------------- /src/lib.zig: -------------------------------------------------------------------------------- 1 | //! Compile-time Contracts 2 | //! 3 | //! Compile-time contracts address the duck-typing problem in Zig. When you have 4 | //! a function taking `type` or `anytype` parameters, it's not trivial to tell 5 | //! what that type should be like. 6 | //! 7 | //! One can write good documentation but that requires extra maintenance efforts 8 | //! and is not very formalized. 9 | //! 10 | //! There's `std.meta.trait` but it is limited in terms of what it can do, and particulary, 11 | //! it's not great at identifying the cause of contract (trait) and have limited composition. 12 | //! 13 | //! This library offers simple, composable contracts that can track causes of contract violation. 14 | //! 15 | //! There are two primary ways contracts can be used. They can be used directly in function's body: 16 | //! 17 | //! ``` 18 | //! const contracts = @import("./src/lib.zig"); 19 | //! 20 | //! fn body_contract(t: anytype) void { 21 | //! comptime contracts.require(contracts.is(@TypeOf(t), u8)); 22 | //! } 23 | //! ``` 24 | //! 25 | //! or function's signature: 26 | //! 27 | //! ``` 28 | //! fn signature_contract(t: anytype) contracts.RequiresAndReturns( 29 | //! contracts.is(@TypeOf(t), u8), 30 | //! void, 31 | //! ) {} 32 | //! 33 | //! pub fn main() void { 34 | //! body_contract(@as(u8, 1)); 35 | //! signature_contract(@as(u8, 1)); 36 | //! } 37 | //! ``` 38 | 39 | const std = @import("std"); 40 | const expect = std.testing.expect; 41 | 42 | pub const Identifier = []const u8; 43 | 44 | pub const Valid = struct { 45 | identifier: Identifier, 46 | 47 | fn clone(self: @This()) @This() { 48 | return Invalid{ 49 | .identifier = self.identifier, 50 | }; 51 | } 52 | }; 53 | 54 | pub const Invalid = struct { 55 | identifier: Identifier, 56 | 57 | /// contracts that caused this contract to be invalid 58 | /// (primarily concerns composite contracts) 59 | causes: []const Contract = &[0]Contract{}, 60 | 61 | /// reason for violation 62 | reason: []const u8 = "none given", 63 | 64 | /// Returns the first contract that causes this contract's failure 65 | pub fn cause(comptime self: @This()) Contract { 66 | if (self.causes.len > 0) 67 | return self.causes[0].Invalid.cause(); 68 | return Contract.init(false, self.clone()); 69 | } 70 | 71 | fn clone(self: @This()) @This() { 72 | return Invalid{ 73 | .identifier = self.identifier, 74 | .reason = self.reason, 75 | .causes = self.causes, 76 | }; 77 | } 78 | }; 79 | 80 | /// Contract 81 | pub const Contract = union(enum) { 82 | Valid: Valid, 83 | Invalid: Invalid, 84 | 85 | pub fn init(validity: bool, invalid: Invalid) @This() { 86 | return if (validity) .{ 87 | .Valid = .{ .identifier = invalid.identifier }, 88 | } else .{ 89 | .Invalid = invalid, 90 | }; 91 | } 92 | 93 | fn clone(comptime self: @This()) @This() { 94 | return @This().init( 95 | self == .Valid, 96 | (if (self == .Valid) Invalid{ 97 | .identifier = self.identifier(), 98 | } else self.Invalid.clone()), 99 | ); 100 | } 101 | 102 | /// Returns the identifier of a contract 103 | pub fn identifier(comptime self: @This()) Identifier { 104 | return switch (self) { 105 | .Valid => |v| v.identifier, 106 | .Invalid => |i| i.identifier, 107 | }; 108 | } 109 | 110 | fn collectFailures(comptime t1: @This(), comptime t2: @This()) []const @This() { 111 | var n: usize = 0; 112 | if (t1 == .Invalid) n += 1; 113 | if (t2 == .Invalid) n += 1; 114 | 115 | var causes: [n]@This() = [_]@This(){undefined} ** n; 116 | 117 | var i: usize = 0; 118 | if (t1 == .Invalid) { 119 | causes[i] = t1; 120 | i += 1; 121 | } 122 | if (t2 == .Invalid) { 123 | causes[i] = t2; 124 | } 125 | return &causes; 126 | } 127 | 128 | /// A contract that requires both `self` and `t` to be valid 129 | pub fn andAlso(comptime self: @This(), comptime t: @This()) @This() { 130 | return @This().init(self == .Valid and t == .Valid, Invalid{ 131 | .identifier = std.fmt.comptimePrint("{s}.andAlso({s})", .{ self.identifier(), t.identifier() }), 132 | .causes = self.collectFailures(t), 133 | }); 134 | } 135 | 136 | /// A contract that requires `self` to be valid and if it is, the 137 | /// contract returned by Then.then() has to be valid as well 138 | pub fn andThen(comptime self: @This(), comptime Then: type) @This() { 139 | if (self == .Invalid) 140 | return self.clone(); 141 | return Then.then().named(std.fmt.comptimePrint("andThen({s})", .{@typeName(Then)})); 142 | } 143 | 144 | /// A contract that requires `self` to be valid and if it is not, the 145 | /// contract returned by Then.then() has to be valid 146 | pub fn orThen(comptime self: @This(), comptime Then: type) @This() { 147 | if (self == .Valid) 148 | return self.clone(); 149 | return Then.then().named(std.fmt.comptimePrint("orThen({s})", .{@typeName(Then)})); 150 | } 151 | 152 | /// A contract that requires either `self` or `t` to be valid 153 | pub fn orElse(comptime self: @This(), comptime t: @This()) @This() { 154 | return @This().init(self == .Valid or t == .Valid, Invalid{ 155 | .identifier = std.fmt.comptimePrint("{s}.orElse({s})", .{ self.identifier(), t.identifier() }), 156 | .causes = t.collectFailures(self), 157 | }); 158 | } 159 | 160 | /// Gives a contract a new identifier, wrapping the original contract as a cause 161 | /// 162 | /// Useful for creating named contracts that compose other contracts together 163 | /// 164 | /// ``` 165 | /// pub fn isMyThing(compile T: type) contracts.Contract { 166 | /// return contracts.isType(T, .Struct).named("isMyThing"); 167 | /// } 168 | /// ``` 169 | pub fn named(comptime self: @This(), identifier_: []const u8) @This() { 170 | return @This().init(self == .Valid, Invalid{ .identifier = identifier_, .causes = &[1]@This(){self} }); 171 | } 172 | }; 173 | 174 | test "andAlso" { 175 | comptime { 176 | const T = u8; 177 | const valid_contract = is(T, u8).andAlso(is(T, u8)); 178 | try expect(valid_contract == .Valid); 179 | 180 | const contract = is(T, u8).andAlso(is(T, u16)); 181 | try expect(contract == .Invalid); 182 | 183 | try expect(std.mem.eql(u8, "is(u8, u8).andAlso(is(u8, u16))", contract.identifier())); 184 | try expect(contract.Invalid.causes.len == 1); 185 | 186 | try expect(std.mem.eql(u8, contract.Invalid.causes[0].identifier(), is(T, u16).identifier())); 187 | } 188 | } 189 | 190 | test "andThen" { 191 | comptime { 192 | var a = 1; 193 | const contract = is(u8, u8).andThen(struct { 194 | pub fn then() Contract { 195 | a = 2; 196 | return is(u8, u16); 197 | } 198 | }); 199 | try expect(contract == .Invalid); 200 | // andThen got to executed 201 | try expect(a == 2); 202 | 203 | const contract1 = is(u8, u16).andThen(struct { 204 | pub fn then() Contract { 205 | a = 3; 206 | return is(u8, u16); 207 | } 208 | }); 209 | 210 | try expect(contract1 == .Invalid); 211 | // andThen didn't get to execute 212 | try expect(a == 2); 213 | } 214 | } 215 | 216 | test "orElse" { 217 | comptime { 218 | const T = u8; 219 | const valid_contract = is(T, u8).orElse(is(T, u16)); 220 | try expect(valid_contract == .Valid); 221 | 222 | const contract = is(T, u1).orElse(is(T, u17)); 223 | try expect(contract == .Invalid); 224 | 225 | try expect(std.mem.eql(u8, "is(u8, u1).orElse(is(u8, u17))", contract.identifier())); 226 | try expect(contract.Invalid.causes.len == 2); 227 | 228 | try expect(std.mem.eql(u8, contract.Invalid.causes[0].identifier(), is(T, u17).identifier())); 229 | try expect(std.mem.eql(u8, contract.Invalid.causes[1].identifier(), is(T, u1).identifier())); 230 | } 231 | } 232 | 233 | test "orThen" { 234 | comptime { 235 | var a = 1; 236 | const contract = is(u8, u8).orThen(struct { 237 | pub fn then() Contract { 238 | a = 2; 239 | return is(u8, u16); 240 | } 241 | }); 242 | try expect(contract == .Valid); 243 | // orThen didn't get executed 244 | try expect(a == 1); 245 | 246 | const contract1 = is(u8, u16).orThen(struct { 247 | pub fn then() Contract { 248 | a = 2; 249 | return is(u8, u16); 250 | } 251 | }); 252 | 253 | try expect(contract1 == .Invalid); 254 | //// orThen got to execute 255 | try expect(a == 2); 256 | } 257 | } 258 | 259 | test "invalid contract cause" { 260 | comptime { 261 | const T = u8; 262 | try expect(std.mem.eql( 263 | u8, 264 | is(T, u16).identifier(), 265 | is(T, u16).Invalid.cause().identifier(), 266 | )); 267 | 268 | const contract = is(T, u8).andAlso(is(T, u16)); 269 | try expect(contract == .Invalid); 270 | try expect(std.mem.eql( 271 | u8, 272 | is(T, u16).identifier(), 273 | contract.Invalid.cause().identifier(), 274 | )); 275 | try expect(std.mem.eql( 276 | u8, 277 | contract.Invalid.reason, 278 | contract.Invalid.cause().Invalid.reason, 279 | )); 280 | 281 | const custom_reason = Contract{ .Invalid = .{ .identifier = "custom", .reason = "custom" } }; 282 | try expect(std.mem.eql(u8, custom_reason.Invalid.reason, custom_reason.Invalid.cause().Invalid.reason)); 283 | 284 | const nested = (Contract{ .Valid = .{ .identifier = "1" } }) 285 | .andAlso( 286 | (Contract{ .Valid = .{ .identifier = "2" } }) 287 | .andAlso(Contract{ .Invalid = .{ .identifier = "3", .reason = "special" } }), 288 | ); 289 | try expect(std.mem.eql(u8, "3", nested.Invalid.cause().identifier())); 290 | try expect(std.mem.eql(u8, "special", nested.Invalid.cause().Invalid.reason)); 291 | } 292 | } 293 | 294 | test "Contract.named" { 295 | comptime { 296 | const contract = is(u8, u16).named("contract"); 297 | try expect(contract == .Invalid); 298 | try expect(std.mem.eql(u8, "contract", contract.identifier())); 299 | try expect(std.mem.eql(u8, "is(u8, u16)", contract.Invalid.cause().identifier())); 300 | } 301 | } 302 | 303 | fn fnArgsEql(comptime a: []const std.builtin.TypeInfo.FnArg, comptime b: []const std.builtin.TypeInfo.FnArg) bool { 304 | if (a.len != b.len) return false; 305 | if (a.ptr == b.ptr) return true; 306 | for (a) |item, index| { 307 | if (b[index].is_generic != item.is_generic) return false; 308 | if (b[index].is_noalias != item.is_noalias) return false; 309 | if (b[index].arg_type == null and item.arg_type != null) return false; 310 | if (b[index].arg_type != null and item.arg_type != null and b[index].arg_type.? != item.arg_type.?) return false; 311 | } 312 | return true; 313 | } 314 | 315 | fn isGenericFnEqual(comptime T: type, comptime T1: type) bool { 316 | comptime { 317 | if (@typeInfo(T) != .Fn or @typeInfo(T) != .Fn) 318 | return false; 319 | const ti = @typeInfo(T).Fn; 320 | const ti1 = @typeInfo(T1).Fn; 321 | return ti.calling_convention == ti1.calling_convention and ti.alignment == ti1.alignment and 322 | ((ti.return_type == null and ti1.return_type == null) or (ti.return_type.? == ti1.return_type.?)) and 323 | fnArgsEql(ti.args, ti1.args); 324 | } 325 | } 326 | 327 | /// A contract that requires type `T` to be the same type as `T1` 328 | pub fn is(comptime T: type, comptime T1: type) Contract { 329 | const valid = if (isGenericFn(T) == .Valid and isGenericFn(T1) == .Valid) isGenericFnEqual(T, T1) else T == T1; 330 | return Contract.init(valid, Invalid{ 331 | .identifier = std.fmt.comptimePrint("is({}, {})", .{ T, T1 }), 332 | }); 333 | } 334 | 335 | test "is" { 336 | comptime { 337 | try expect(is(u8, u8) == .Valid); 338 | try expect(is(u8, u16) == .Invalid); 339 | try expect(std.mem.eql(u8, "is(u8, u16)", is(u8, u16).identifier())); 340 | try expect(is(u8, u16).Invalid.causes.len == 0); 341 | try expect(is(fn (u8) u8, fn (u8) u8) == .Valid); 342 | try expect(is(fn (type) u8, fn (type) u8) == .Valid); 343 | // FIXME: figure out what to do with the return type, it's not really validating it 344 | // try expect(is(fn (type) u8, fn (type) bool) == .Invalid); 345 | } 346 | } 347 | 348 | /// A contract that requires function type T be generic 349 | pub fn isGenericFn(comptime T: type) Contract { 350 | comptime { 351 | const identifier = std.fmt.comptimePrint("isGenericFn({})", .{T}); 352 | return isType(T, .Fn).andThen(struct { 353 | pub fn then() Contract { 354 | return Contract.init(@typeInfo(T).Fn.is_generic, Invalid{ .identifier = identifier }); 355 | } 356 | }).named(identifier); 357 | } 358 | } 359 | 360 | test "isGenericFn" { 361 | comptime { 362 | try expect(isGenericFn(fn (type) bool) == .Valid); 363 | try expect(isGenericFn(fn (bool) bool) == .Invalid); 364 | try expect(isGenericFn(u8) == .Invalid); 365 | } 366 | } 367 | 368 | /// A contract that requires that type `T` has to be of a certain 369 | /// type (as in .Struct, .Int, etc.) 370 | /// 371 | /// Includes violation reason into `Invalid.reason` 372 | pub fn isType(comptime T: type, comptime type_id: std.builtin.TypeId) Contract { 373 | return Contract.init(@typeInfo(T) == type_id, Invalid{ 374 | .identifier = std.fmt.comptimePrint("isType({}, .{s})", .{ T, @tagName(type_id) }), 375 | .reason = std.fmt.comptimePrint("got .{s}", .{@tagName(@typeInfo(T))}), 376 | }); 377 | } 378 | 379 | test "isType" { 380 | comptime { 381 | try expect(isType(struct {}, .Struct) == .Valid); 382 | try expect(isType(u8, .Struct) == .Invalid); 383 | try expect(std.mem.eql(u8, "got .Int", isType(u8, .Struct).Invalid.reason)); 384 | try expect(std.mem.eql(u8, "isType(u8, .Struct)", isType(u8, .Struct).identifier())); 385 | } 386 | } 387 | 388 | /// A contract that requires that a given type is a struct, 389 | /// enum, union or an opaque type that has a declaration by the given name. 390 | pub fn hasDecl(comptime T: type, comptime name: []const u8) Contract { 391 | const ti = @typeInfo(T); 392 | const validType = ti == .Struct or ti == .Enum or ti == .Union or ti == .Opaque; 393 | 394 | const valid = if (validType) @hasDecl(T, name) else false; 395 | const reason = if (validType) "declaration not found" else std.fmt.comptimePrint("{s} is not a struct, enum, union or an opaque type", .{T}); 396 | 397 | return Contract.init(valid, Invalid{ 398 | .identifier = std.fmt.comptimePrint("hasDecl({}, {s})", .{ T, name }), 399 | .reason = reason, 400 | }); 401 | } 402 | 403 | test "hasDecl" { 404 | comptime { 405 | try expect(hasDecl(u8, "a") == .Invalid); 406 | try expect(std.mem.eql(u8, hasDecl(u8, "a").Invalid.reason, "u8 is not a struct, enum, union or an opaque type")); 407 | try expect(hasDecl(struct {}, "a") == .Invalid); 408 | try expect(hasDecl(struct { 409 | const a = 1; 410 | }, "a") == .Valid); 411 | try expect(hasDecl(enum { 412 | a, 413 | }, "a") == .Invalid); 414 | try expect(hasDecl(enum { 415 | a, 416 | const b = 1; 417 | }, "b") == .Valid); 418 | 419 | try expect(hasDecl(union(enum) { 420 | a: void, 421 | }, "a") == .Invalid); 422 | try expect(hasDecl(union(enum) { 423 | a: void, 424 | const b = 1; 425 | }, "b") == .Valid); 426 | } 427 | } 428 | 429 | /// A contract that requires that a given type is a struct, 430 | /// enum, union or an opaque type that has a struct declaration by the given name. 431 | pub fn hasStruct(comptime T: type, comptime name: []const u8) Contract { 432 | return hasDecl(T, name).andThen(struct { 433 | pub fn then() Contract { 434 | return isType(@field(T, name), .Struct); 435 | } 436 | }) 437 | .named(std.fmt.comptimePrint("hasStruct({}, {s})", .{ T, name })); 438 | } 439 | 440 | test "hasStruct" { 441 | comptime { 442 | try expect(hasStruct(u8, "a") == .Invalid); 443 | try expect(std.mem.eql( 444 | u8, 445 | hasStruct(u8, "a").Invalid.cause().Invalid.reason, 446 | "u8 is not a struct, enum, union or an opaque type", 447 | )); 448 | try expect(hasStruct(struct {}, "a") == .Invalid); 449 | try expect(hasStruct(struct { 450 | const a = struct {}; 451 | }, "a") == .Valid); 452 | try expect(hasStruct(enum { 453 | a, 454 | }, "a") == .Invalid); 455 | try expect(hasStruct(enum { 456 | a, 457 | const b = struct {}; 458 | }, "b") == .Valid); 459 | 460 | try expect(hasStruct(union(enum) { 461 | a: void, 462 | }, "a") == .Invalid); 463 | try expect(hasStruct(union(enum) { 464 | a: void, 465 | const b = struct {}; 466 | }, "b") == .Valid); 467 | } 468 | } 469 | 470 | /// A contract that requires that a given type is a struct, 471 | /// enum, union or an opaque type that has a function declaration by the given name. 472 | pub fn hasFn(comptime T: type, comptime name: []const u8) Contract { 473 | return hasDecl(T, name).andThen(struct { 474 | pub fn then() Contract { 475 | return isType(@TypeOf(@field(T, name)), .Fn); 476 | } 477 | }) 478 | .named(std.fmt.comptimePrint("hasFn({}, {s})", .{ T, name })); 479 | } 480 | 481 | test "hasFn" { 482 | comptime { 483 | try expect(hasFn(u8, "a") == .Invalid); 484 | try expect(std.mem.eql( 485 | u8, 486 | hasFn(u8, "a").Invalid.cause().Invalid.reason, 487 | "u8 is not a struct, enum, union or an opaque type", 488 | )); 489 | try expect(hasFn(struct {}, "a") == .Invalid); 490 | try expect(hasFn(struct { 491 | fn a() void {} 492 | }, "a") == .Valid); 493 | try expect(hasFn(enum { 494 | a, 495 | }, "a") == .Invalid); 496 | try expect(hasFn(enum { 497 | a, 498 | fn b() void {} 499 | }, "b") == .Valid); 500 | 501 | try expect(hasFn(union(enum) { 502 | a: void, 503 | }, "a") == .Invalid); 504 | try expect(hasFn(union(enum) { 505 | a: void, 506 | fn b() void {} 507 | }, "b") == .Valid); 508 | } 509 | } 510 | 511 | fn isEquivalent_(comptime A: type, comptime B: type) Contract { 512 | return hasStruct(A, "contracts") 513 | .andThen(struct { 514 | pub fn then() Contract { 515 | comptime { 516 | return hasFn(@field(A, "contracts"), "isEquivalent"); 517 | } 518 | } 519 | }) 520 | .andThen(struct { 521 | pub fn then() Contract { 522 | return is(@TypeOf(A.contracts.isEquivalent), fn (type) bool); 523 | } 524 | }) 525 | .andThen(struct { 526 | pub fn then() Contract { 527 | return Contract.init(A.contracts.isEquivalent(B), Invalid{ 528 | .identifier = std.fmt.comptimePrint("{s}.contracts.isEquivalent({s})", .{ A, B }), 529 | }); 530 | } 531 | }); 532 | } 533 | 534 | /// A contract that requires that a given types A and B are a struct, enum, union or 535 | /// an opaque type, and either A, or B, or both define `contracts` struct with `isEquivalent(type) bool` 536 | /// function, and that function of either A or B returns true. 537 | /// 538 | /// This is used to establish equivalency of otherwise unequal types: 539 | /// 540 | /// ``` 541 | /// const A = struct { 542 | /// pub const contracts = struct { 543 | /// pub fn isEquivalent(comptime T: type) bool { 544 | /// return T == B; 545 | /// } 546 | /// }; 547 | /// 548 | /// pub const contracts = struct { 549 | /// pub fn isEquivalent(comptime T: type) bool { 550 | /// return T == B; 551 | /// } 552 | /// }; 553 | /// }; 554 | /// 555 | /// assert(isEquivalent(A, B) == .Valid); 556 | /// ``` 557 | pub fn isEquivalent(comptime A: type, comptime B: type) Contract { 558 | return isEquivalent_(A, B).orElse(isEquivalent_(B, A)); 559 | } 560 | 561 | test "isEquivalent" { 562 | comptime { 563 | const Tequiv = struct { 564 | pub const contracts = struct { 565 | pub fn isEquivalent(comptime _: type) bool { 566 | return true; 567 | } 568 | }; 569 | }; 570 | const Tnonequiv = struct { 571 | pub const contracts = struct { 572 | pub fn isEquivalent(comptime _: type) bool { 573 | return false; 574 | } 575 | }; 576 | }; 577 | _ = Tequiv; 578 | _ = Tnonequiv; 579 | try expect(isEquivalent(Tequiv, Tnonequiv) == .Valid); 580 | try expect(isEquivalent(Tnonequiv, Tequiv) == .Valid); 581 | try expect(isEquivalent(Tnonequiv, Tnonequiv) == .Invalid); 582 | } 583 | } 584 | 585 | /// Requires a contract to be valid, throws a compile-time error otherwise 586 | pub fn require(comptime contract: Contract) void { 587 | if (contract == .Invalid) { 588 | const err = if (std.mem.eql(u8, contract.identifier(), contract.Invalid.cause().identifier()) and 589 | std.mem.eql(u8, contract.Invalid.reason, contract.Invalid.cause().Invalid.reason)) 590 | std.fmt.comptimePrint( 591 | "requirement failure in {s} (reason: {s})", 592 | .{ 593 | contract.identifier(), 594 | contract.Invalid.reason, 595 | }, 596 | ) 597 | else 598 | std.fmt.comptimePrint( 599 | "requirement failure in {s} (reason: {s}), cause: {s} (reason: {s})", 600 | .{ 601 | contract.identifier(), 602 | contract.Invalid.reason, 603 | contract.Invalid.cause().identifier(), 604 | contract.Invalid.cause().Invalid.reason, 605 | }, 606 | ); 607 | @compileError(err); 608 | } 609 | } 610 | 611 | /// Requires a contract to be valid, throws a compile-time error otherwise 612 | /// 613 | /// Used in function signatures: 614 | /// 615 | /// ``` 616 | /// fn signature_contract(t: anytype) contracts.RequiresAndReturns( 617 | /// contracts.is(@TypeOf(t), u8), 618 | /// void, 619 | /// ) {} 620 | /// ``` 621 | pub fn RequiresAndReturns(contract: Contract, comptime T: type) type { 622 | require(contract); 623 | return T; 624 | } 625 | -------------------------------------------------------------------------------- /zig.mod: -------------------------------------------------------------------------------- 1 | id: wcbpsq9gg5gvra1wz24vb11x78fa17mpr1q44l4v6h1demjr 2 | name: ctc 3 | main: src/lib.zig 4 | license: MIT 5 | description: Compile-time contracts 6 | dependencies: 7 | -------------------------------------------------------------------------------- /zigmod.lock: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/yrashk/zig-ctc/7dd018b3615ce804274248338c938d17ddb3e129/zigmod.lock --------------------------------------------------------------------------------