Skip to content

Commit aa80d10

Browse files
committed
feat: DbC contracts
0 parents  commit aa80d10

File tree

6 files changed

+200
-0
lines changed

6 files changed

+200
-0
lines changed

LICENCE

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
MIT License
2+
3+
Copyright (c) 2025 alexekdahl
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.

README.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# Design by Contract for Nim
2+
3+
A lightweight Design by Contract implementation for Nim allowing you to enforce preconditions and postconditions
4+
5+
## Installation
6+
7+
```
8+
nimble install assert
9+
```
10+
11+
## Usage
12+
13+
```nim
14+
import assert
15+
import math
16+
17+
proc divide(a, b: int): int {.contract.} =
18+
## Integer division of a by b.
19+
## Requires:
20+
## b != 0
21+
## Ensures:
22+
## result * b == a
23+
result = a div b
24+
```
25+
26+
## Compile Options
27+
28+
- **Regular Build**: Contracts fully enabled
29+
```
30+
nim c program.nim
31+
```
32+
33+
- **Production Build**: No contract checks (zero overhead)
34+
```
35+
nim c -d:noContracts program.nim
36+
```
37+
38+
## Note
39+
40+
When a contract is violated, the program terminates with an error message

assert.nimble

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
version = "0.1.2"
2+
author = "alexekdahl"
3+
description = "DbC library for Nim providing precondition and postcondition assertions"
4+
license = "MIT"
5+
srcDir = "src"
6+
7+
# Dependencies
8+
requires "nim >= 2.2.2"

src/assert.nim

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
# Main module that exports all submodules
2+
import assert/contracts
3+
4+
export contracts

src/assert/contracts.nim

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import macros, strutils
2+
3+
const moduleName = "assert/contracts"
4+
5+
6+
proc contractViolation(msg: string) {.noreturn.} =
7+
stderr.writeLine("CONTRACT VIOLATION:\n\t" & msg)
8+
9+
when compileOption("stacktrace"):
10+
let trace = getStackTrace()
11+
for line in trace.splitLines():
12+
if not line.contains(moduleName) and line.len > 0:
13+
stderr.writeLine(line)
14+
15+
quit(QuitFailure)
16+
17+
18+
proc extractContracts(docComment: string): tuple[requires, ensures,
19+
invariants: seq[string]] =
20+
result.requires = @[]
21+
result.ensures = @[]
22+
23+
let lines = docComment.splitLines()
24+
var currentSection = ""
25+
26+
for line in lines:
27+
let trimmedLine = line.strip()
28+
if trimmedLine.startsWith("Requires:"):
29+
currentSection = "requires"
30+
continue
31+
elif trimmedLine.startsWith("Ensures:"):
32+
currentSection = "ensures"
33+
continue
34+
35+
if currentSection == "requires" and trimmedLine.len > 0:
36+
result.requires.add(trimmedLine)
37+
elif currentSection == "ensures" and trimmedLine.len > 0:
38+
result.ensures.add(trimmedLine)
39+
40+
41+
macro contract*(procDef: untyped): untyped =
42+
## Applies Design by Contract principles to a procedure based on its documentation.
43+
result = procDef
44+
# For production builds, don't add contract checks
45+
if defined(noContracts):
46+
return
47+
48+
var docComment = ""
49+
if procDef.kind == nnkProcDef:
50+
51+
# Extract doc comment if it exists
52+
if procDef[6].kind == nnkStmtList and procDef[6].len > 0:
53+
if procDef[6][0].kind == nnkCommentStmt:
54+
docComment = procDef[6][0].strVal
55+
56+
if docComment == "":
57+
return # No documentation, so no contracts to apply
58+
59+
# Extract contracts from the documentation
60+
let contracts = extractContracts(docComment)
61+
62+
# Create the contract checking code
63+
var preconditions = newStmtList()
64+
for req in contracts.requires:
65+
let condExpr = parseExpr(req)
66+
preconditions.add(quote do:
67+
if not(`condExpr`):
68+
let info = instantiationInfo()
69+
contractViolation(info.filename & ":" & $info.line &
70+
"\n\t\tPrecondition failed: " & `req`)
71+
)
72+
73+
var postconditions = newStmtList()
74+
for ens in contracts.ensures:
75+
let condExpr = parseExpr(ens)
76+
postconditions.add(quote do:
77+
if not(`condExpr`):
78+
let info = instantiationInfo()
79+
contractViolation(info.filename & ":" & $info.line &
80+
"\n\t\tPostcondition failed: " & `ens`)
81+
)
82+
83+
# Insert the contract checking code into the procedure
84+
let procBody = procDef[6]
85+
var newBody = newStmtList()
86+
87+
# Copy the comment if it exists
88+
if procBody.len > 0 and procBody[0].kind == nnkCommentStmt:
89+
newBody.add(procBody[0])
90+
91+
# Add precondition checks
92+
for precheck in preconditions:
93+
newBody.add(precheck)
94+
95+
# Copy the original body except the first statement if it's a comment
96+
for i in 0..<procBody.len:
97+
if i > 0 or procBody[0].kind != nnkCommentStmt:
98+
newBody.add(procBody[i])
99+
100+
# Add postcondition checks
101+
for postcheck in postconditions:
102+
newBody.add(postcheck)
103+
104+
# Replace the procedure body with the new one
105+
procDef[6] = newBody

src/example.nim

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
import assert
2+
import math
3+
4+
proc divide(a, b: int): int {.contract.} =
5+
## Integer division
6+
## Requires:
7+
## b != 0
8+
## Ensures:
9+
## result * b == a
10+
11+
result = a div b
12+
13+
proc add5to5(x: int): int {.contract.} =
14+
## Adds 5 to 5
15+
## Requires:
16+
## x == 5
17+
18+
result = x + 5
19+
20+
when isMainModule:
21+
echo "10 / 2 = ", divide(10, 2) # Works fine
22+
echo "adding 5 to 10 = ", add5to5(10) # This will panic if contracts are enabled

0 commit comments

Comments
 (0)