Handle bind (=?) errors in without statements differently

Keeps track of the current error variable at compile time,
instead of using a pointer to the error variable at runtime.

Employs a trick with an unused type parameter to ensure that
invocations of the bindFailed() macro are expanded after
captureBindError() is expanded.
This commit is contained in:
Mark Spanbroek 2023-12-18 17:01:37 +01:00 committed by markspanbroek
parent 1f0afff48b
commit d463d491cc
2 changed files with 46 additions and 16 deletions

View File

@ -1,24 +1,43 @@
import std/options
import std/macros
var captures {.global, compileTime.}: int
var errorVariable {.threadvar.}: ptr ref CatchableError
# A stack of names of error variables. Keeps track of the error variables that
# are given to captureBindError().
var errorVariableNames {.global, compileTime.}: seq[string]
template captureBindError*(error: var ref CatchableError, expression): auto =
let previousErrorVariable = errorVariable
errorVariable = addr error
macro captureBindError*(error: var ref CatchableError, expression): auto =
## Ensures that an error is assigned to the error variable when a binding (=?)
## fails inside the expression.
static: inc captures
let evaluated = expression
static: dec captures
errorVariable = previousErrorVariable
evaluated
# name of the error variable as a string literal
let errorVariableName = newLit($error)
quote do:
# add error variable to the top of the stack
static: errorVariableNames.add(`errorVariableName`)
# evaluate the expression
let evaluated = `expression`
# pop error variable from the stack
static: discard errorVariableNames.pop()
# return the evaluated result
evaluated
func error[T](option: Option[T]): ref CatchableError =
newException(ValueError, "Option is set to `none`")
template bindFailed*(expression) =
when captures > 0:
mixin error
errorVariable[] = expression.error
macro bindFailed*(expression; _: type = void) =
## Called when a binding (=?) fails.
## Assigns an error to the error variable (specified in captureBindError())
## when appropriate.
# This macro has a type parameter to ensure that the compiler does not
# expand it before it expands invocations of captureBindError().
# check that we have an error variable on the stack
if errorVariableNames.len > 0:
# create an identifier that references the current error variable
let errorVariable = ident errorVariableNames[^1]
return quote do:
# check that the error variable is in scope
when compiles(`errorVariable`):
# assign bind error to error variable
`errorVariable` = `expression`.error

View File

@ -412,6 +412,17 @@ suite "result":
for i in 0..<1000:
spawn fail(i)
test "without statement doesn't interfere with generic code called elsewhere":
proc foo(_: type): ?!int =
if error =? success(1).errorOption:
discard
proc bar {.used.} = # defined, but not used
without x =? bool.foo(), error:
discard error
discard bool.foo() # same type parameter 'bool' as used in bar()
test "catch can be used to convert exceptions to results":
check parseInt("42").catch == 42.success
check parseInt("foo").catch.error of ValueError