Skip to content

Commit

Permalink
different try/break match methods, document tap
Browse files Browse the repository at this point in the history
  • Loading branch information
metagn committed Sep 7, 2024
1 parent 3d5b6e3 commit 73403b5
Show file tree
Hide file tree
Showing 7 changed files with 358 additions and 82 deletions.
2 changes: 1 addition & 1 deletion assigns.nimble
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,6 @@ task docs, "build docs for all modules":

task tests, "run tests for multiple backends":
when declared(runTests):
runTests(backends = {c, js, nims}, optionCombos = @["", "-d:assignsMatchBreakpoint"])
runTests(backends = {c, js, nims}, optionCombos = @[""])
else:
echo "tests task not implemented, need nimbleutils"
2 changes: 2 additions & 0 deletions src/assigns/impl.nim
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ type
## error for failed bound checks in assignments

template assignCheckDefaultFail*(body) =
## default behavior for assignment check failing with expression `body`,
## which is just to run `body`
body

template assignCheckFail*(body) =
Expand Down
200 changes: 155 additions & 45 deletions src/assigns/syntax.nim
Original file line number Diff line number Diff line change
Expand Up @@ -56,21 +56,27 @@ macro `:=`*(a, b): untyped =
result = openAssign(a, b)

template `:=?`*(a, b): bool =
## Executes (!) ``a := b`` and returns false if it gives a runtime error.
## Otherwise returns `true`.
## Executes (!) ``a := b`` and returns false if the checks in the assignment
## fail. Otherwise returns `true`.
## Note that the executed ``a := b`` will not have any
## affect on the scope of the following statements since
## it uses a `try` statement.
## it is in a `block` statement.
runnableExamples:
doAssert (a, b) :=? (1, 2)
import options
let a = none(int)
doAssert not (some(n) :=? a)
try:
a := b
true
except AssignError:
false
block match:
var passed = false
block success:
template assignCheckBreakpoint(checkBody) {.redefine, used.} =
passed = false
break success
`a` := `b`
template assignCheckBreakpoint(checkBody) {.redefine, used.} =
checkBody
passed = true
passed

template `:=?`*(a, b, body): untyped =
## Executes `body` if ``a := b`` doesn't give a runtime error.
Expand All @@ -92,9 +98,13 @@ template `:=?`*(a, b, body): untyped =
`body`

macro `:=?`*(a, b, body, elseBranch): untyped =
## Executes `body` if ``a ::= b`` doesn't give a runtime error,
## Executes `body` if ``a := b`` doesn't fail any checks,
## otherwise executes `elseBranch`.
## `body` will be in the same scope as the definition ``a := b``.
##
## Uses `break` instead of exceptions for handling check failures unilke
## `tryAssign`. Due to Nim limitations, this means this cannot be used
## as an expression and must be a statement.
##
## Example:
##
Expand All @@ -106,42 +116,107 @@ macro `:=?`*(a, b, body, elseBranch): untyped =
## else:
## doAssert false
let elseExpr = if elseBranch.kind == nnkElse: elseBranch[0] else: elseBranch
when not defined(assignsMatchBreakpoint):
result = quote:
var assignFinished = false
try:
result = quote:
block match:
block success:
template assignCheckBreakpoint(checkBody) {.redefine, used.} =
break success
`a` := `b`
assignFinished = true
template assignCheckBreakpoint(checkBody) {.redefine, used.} =
checkBody
`body`
except AssignError:
if assignFinished:
raise
else:
`elseExpr`
break match
`elseExpr`

macro tryAssign*(a, b, body, elseBranch): untyped =
## Executes `body` if ``a := b`` doesn't fail any checks,
## otherwise executes `elseBranch`.
## `body` will be in the same scope as the definition ``a := b``.
##
## Uses exceptions instead of `break` unlike `:=?`. This allows it to be
## used as an expression but has a runtime cost.
##
## Example:
##
## .. code-block::nim
## import options
## let a = some(3)
## tryAssign some(n), a:
## doAssert n == 3
## else:
## doAssert false
let elseExpr = if elseBranch.kind == nnkElse: elseBranch[0] else: elseBranch
result = quote:
var assignFinished = false
try:
`a` := `b`
assignFinished = true
`body`
except AssignError:
if assignFinished:
raise
else:
`elseExpr`

macro tryAssign*(a, b, c): untyped =
## Version of `tryAssign` with either no `else` block, or with an infix
## assignment as the first argument.
##
## Example:
##
## .. code-block::nim
## import options
## let a = some(3)
## tryAssign some(n) := a: # or = a
## doAssert n == 3
## else:
## doAssert false
##
## tryAssign some(n), a:
## doAssert n == 3
if a.kind == nnkInfix and a[0].eqIdent":=":
let x = a[1]
let y = a[2]
result = getAst(tryAssign(x, y, b, c))
elif a.kind in {nnkAsgn, nnkExprEqExpr}:
let x = a[0]
let y = a[1]
result = getAst(tryAssign(x, y, b, c))
else:
result = quote:
const isExpr = compiles:
let x = `body`
when isExpr:
var res: typeof(`body`)
block match:
block success:
template assignCheckBreakpoint(checkBody) {.redefine, used.} =
break success
`a` := `b`
template assignCheckBreakpoint(checkBody) {.redefine, used.} =
checkBody
when isExpr:
res = `body`
else:
`body`
break match
when isExpr:
res = `elseExpr`
else:
`elseExpr`
when isExpr:
res
let disc = newTree(nnkDiscardStmt, newEmptyNode())
result = getAst(tryAssign(a, b, c, disc))

macro tryAssign*(a, b): untyped =
## Version of `tryAssign` with an infix assignment as the first argument
## and no `else` block.
##
## Example:
##
## .. code-block::nim
## import options
## let a = some(3)
## tryAssign some(n) := a: # or = a
## doAssert n == 3
if a.kind == nnkInfix and a[0].eqIdent":=":
let x = a[1]
let y = a[2]
result = getAst(tryAssign(x, y, b))
elif a.kind in {nnkAsgn, nnkExprEqExpr}:
let x = a[0]
let y = a[1]
result = getAst(tryAssign(x, y, b))
else:
let t = newLit(true)
let f = newLit(false)
result = getAst(tryAssign(a, b, t, f))

template tryAssign*(a): untyped =
## Version of `tryAssign` that returns `false` if the assignment failed
## and `true` otherwise.
## Note that the executed ``a := b`` will not have any
## affect on the scope of the following statements since
## it is in a `try` statement.
tryAssign(a, true, false)

macro unpackArgs*(args, routine): untyped =
## Injects unpacking assignments into the body of a given routine.
Expand Down Expand Up @@ -176,10 +251,45 @@ macro unpackArgs*(args, routine): untyped =
error("unrecognized routine expression for unpackArgs with kind " & $routine.kind, routine)

macro match*(val: untyped, branches: varargs[untyped]): untyped =
## Naive pattern matching implementation based on `:=?`.
## Naive pattern matching implementation based on `:=?`. Has the same
## limitation as `:=?`, which is that it cannot be used as an expression.
runnableExamples:
proc fizzbuzz(n: int): string =
match (n mod 3, n mod 5):
of (0, 0): result = "FizzBuzz"
of (0, _): result = "Fizz"
of (_, 0): result = "Buzz"
else: result = $n
for i in 1..100:
echo fizzbuzz(i)
result = newEmptyNode()
for i in countdown(branches.len - 1, 0):
let b = branches[i]
case b.kind
of nnkElse:
result = b[0]
of nnkElifBranch:
let cond = b[0]
let bod = b[1]
result = quote do:
if `cond`:
`bod`
else:
`result`
of nnkOfBranch:
let bod = b[^1]
for vi in 0..<b.len - 1:
let v = b[vi]
result = getAst(`:=?`(v, val, bod, result))
else:
error("invalid branch for match", b)

macro tryMatch*(val: untyped, branches: varargs[untyped]): untyped =
## Naive pattern matching implementation based on `tryAssign`. Has the same
## caveat of `tryAssign`, which is that it uses exceptions.
runnableExamples:
proc fizzbuzz(n: int): string =
tryMatch (n mod 3, n mod 5):
of (0, 0): "FizzBuzz"
of (0, _): "Fizz"
of (_, 0): "Buzz"
Expand All @@ -204,6 +314,6 @@ macro match*(val: untyped, branches: varargs[untyped]): untyped =
let bod = b[^1]
for vi in 0..<b.len - 1:
let v = b[vi]
result = getAst(`:=?`(v, val, bod, result))
result = getAst(tryAssign(v, val, bod, result))
else:
error("invalid branch for match", b)
error("invalid branch for tryMatch", b)
44 changes: 43 additions & 1 deletion src/assigns/tap.nim
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,10 @@ proc lhsToVal(n: NimNode, context = None): NimNode =
of nnkPrefix:
let a = $n[0]
case a
of "@", "^", "==":
of "@":
result = lhsToVal(n[1])
of "^", "==":
result = n[1]
of "*", "..", "...":
if context in {Tuple, Array}:
result = newTree(nnkDerefExpr, n[1])
Expand Down Expand Up @@ -231,4 +233,44 @@ proc tapImpl(nodes: NimNode): NimNode =
if not finallyBody.isNil: result.add(finallyBody)

macro tap*(nodes: varargs[untyped]): untyped =
## Processes a series of "assignment" expressions, applying them in reverse
## to the given body. `else`/`elif` and `except`/`finally` can also be given,
## where `else`/`elif` triggers if any "matching" assignments fail,
## and `except`/`finally` have the usual behavior.
##
## An assignment expression can be one of:
## * a normal assignment, of the form `a := b` or `a = b`;
## prepended to the body verbatim
## * a matching assignment, of the form `a :=? b` or `a =? b`;
## prepends an assignment `a := b` and stops execution or
## jumps to `else` block if the assignment fails
## * result assignment, of the form `result a` or `result a := b`;
## returns `a` as the value of the expression
## * an iterator assignment, of the form `a in b`;
## wraps the body in `for a in b: ...` , allowing complex assignments
## for `a`
## * a filter, of the form `filter cond`;
## wraps the body in `if cond: ...` statement
## * a list of other assignment expressions, as in `tap: a; b; c; ... do: ...`;
## applies assignments in reverse order
## * any other statement; also prepended to the block verbatim
runnableExamples:
import assigns
let val = tap(a := 5): a + 1
var s: seq[int]
tap a := 5, i in 1 .. a, filter i mod 2 != 0:
s.add(i)
doAssert s == @[1, 3, 5]
doAssert val == 6
let x = tap(a := 5, result s := newSeq[int](a), i in 0 ..< a):
s[i] = i + 1
doAssert x == @[1, 2, 3, 4, 5]
var s2: seq[int]
tap:
a = 5
i in 1 .. a
filter i mod 2 != 0
do:
s2.add(i)
doAssert s2 == @[1, 3, 5]
result = tapImpl(nodes)
Loading

0 comments on commit 73403b5

Please sign in to comment.