Compare commits

..

No commits in common. "main" and "0.10.11" have entirely different histories.

10 changed files with 45 additions and 278 deletions

View File

@ -12,7 +12,7 @@ Use the [Nimble][3] package manager to add `questionable` to an existing
project. Add the following to its .nimble file: project. Add the following to its .nimble file:
```nim ```nim
requires "questionable >= 0.10.15 & < 0.11.0" requires "questionable >= 0.10.11 & < 0.11.0"
``` ```
If you want to make use of Result types, then you also have to add either the If you want to make use of Result types, then you also have to add either the
@ -151,7 +151,7 @@ have to explicitly import the `questionable/results` module:
import questionable/results import questionable/results
``` ```
You can use `?!` to make a Result type. These Result types either hold a value or You can use `?!` make a Result type. These Result types either hold a value or
an error. For example the type `?!int` is short for `Result[int, ref an error. For example the type `?!int` is short for `Result[int, ref
CatchableError]`. CatchableError]`.
@ -226,7 +226,6 @@ Any Result can be converted to an Option:
```nim ```nim
let converted = works().option # equals @[1, 1, 2, 2, 2].some let converted = works().option # equals @[1, 1, 2, 2, 2].some
let errOption = fails().errorOption # option that is set when the Result holds an error
``` ```
[1]: https://nim-lang.org/docs/options.html [1]: https://nim-lang.org/docs/options.html

View File

@ -1,4 +1,4 @@
version = "0.10.15" version = "0.10.11"
author = "Questionable Authors" author = "Questionable Authors"
description = "Elegant optional types" description = "Elegant optional types"
license = "MIT" license = "MIT"

View File

@ -2,25 +2,15 @@ import std/options
import std/macros import std/macros
import ./private/binderror import ./private/binderror
when (NimMajor, NimMinor) < (1, 1): proc option[T](option: Option[T]): Option[T] =
type SomePointer = ref | ptr | pointer
elif (NimMajor, NimMinor) == (2, 0): # Broken in 2.0.0, fixed in 2.1.1.
type SomePointer = ref | ptr | pointer | proc
else:
type SomePointer = ref | ptr | pointer | proc | iterator {.closure.}
template toOption[T](option: Option[T]): Option[T] =
option option
template toOption[T: SomePointer](value: T): Option[T] =
value.option
proc placeholder(T: type): T = proc placeholder(T: type): T =
discard discard
template bindLet(name, expression): untyped = template bindLet(name, expression): untyped =
let evaluated = expression let evaluated = expression
let option = evaluated.toOption let option = evaluated.option
type T = typeof(option.unsafeGet()) type T = typeof(option.unsafeGet())
let name {.used.} = if option.isSome: let name {.used.} = if option.isSome:
option.unsafeGet() option.unsafeGet()
@ -31,7 +21,7 @@ template bindLet(name, expression): untyped =
template bindVar(name, expression): untyped = template bindVar(name, expression): untyped =
let evaluated = expression let evaluated = expression
let option = evaluated.toOption let option = evaluated.option
type T = typeof(option.unsafeGet()) type T = typeof(option.unsafeGet())
var name {.used.} = if option.isSome: var name {.used.} = if option.isSome:
option.unsafeGet() option.unsafeGet()
@ -50,9 +40,9 @@ proc newUnpackTupleNode(names: NimNode, value: NimNode): NimNode =
nnkLetSection.newTree(vartuple) nnkLetSection.newTree(vartuple)
macro bindTuple(names, expression): bool = macro bindTuple(names, expression): bool =
let opt = genSym(nskLet, "option") let opt = ident("option")
let evaluated = genSym(nskLet, "evaluated") let evaluated = ident("evaluated")
let T = genSym(nskType, "T") let T = ident("T")
let value = quote do: let value = quote do:
if `opt`.isSome: if `opt`.isSome:
@ -65,7 +55,7 @@ macro bindTuple(names, expression): bool =
quote do: quote do:
let `evaluated` = `expression` let `evaluated` = `expression`
let `opt` = `evaluated`.toOption let `opt` = `evaluated`.option
type `T` = typeof(`opt`.unsafeGet()) type `T` = typeof(`opt`.unsafeGet())
`letsection` `letsection`
`opt`.isSome `opt`.isSome

View File

@ -1,24 +1,16 @@
import std/macros import std/macros
import std/options
proc safeGet[T](expression: seq[T] | openArray[T], index: int): Option[T] = macro `.?`*(expression: seq | string, brackets: untyped{nkBracket}): untyped =
if index >= expression.low and index <= expression.high: # chain is of shape: (seq or string).?[index]
expression[index].some let index = brackets[0]
else: quote do:
T.none block:
type T = typeof(`expression`[`index`])
proc safeGet(expression: string, index: int): Option[char] = let evaluated = `expression`
if index >= expression.low and index <= expression.high: if `index` < evaluated.len:
expression[index].some evaluated[`index`].some
else: else:
char.none T.none
macro `.?`*(expression: seq | string | openArray, brackets: untyped{nkBracket}): untyped =
# chain is of shape: (seq or string or openArray).?[index]
let index = brackets[0]
quote do:
block:
safeGet(`expression`, `index`)
macro `.?`*(expression: typed, brackets: untyped{nkBracket}): untyped = macro `.?`*(expression: typed, brackets: untyped{nkBracket}): untyped =
# chain is of shape: expression.?[index] # chain is of shape: expression.?[index]

View File

@ -1,6 +0,0 @@
template ignoreBareExceptWarning*(body) =
when defined(nimHasWarnBareExcept):
{.push warning[BareExcept]:off warning[UnreachableCode]:off.}
body
when defined(nimHasWarnBareExcept):
{.pop.}

View File

@ -1,54 +1,24 @@
import std/options import std/options
import std/macros
# A stack of names of error variables. Keeps track of the error variables that var captures {.global, compileTime.}: int
# are given to captureBindError(). var errorVariable {.threadvar.}: ptr ref CatchableError
var errorVariableNames {.global, compileTime.}: seq[string]
macro captureBindError*(error: var ref CatchableError, expression): auto = template captureBindError*(error: var ref CatchableError, expression): auto =
## Ensures that an error is assigned to the error variable when a binding (=?) let previousErrorVariable = errorVariable
## fails inside the expression. errorVariable = addr error
# name of the error variable as a string literal static: inc captures
let errorVariableName = newLit($error) let evaluated = expression
static: dec captures
let evaluated = genSym(nskLet, "evaluated") errorVariable = previousErrorVariable
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 unsafeCatchableError[T](_: Option[T]): ref CatchableError = evaluated
func error[T](option: Option[T]): ref CatchableError =
newException(ValueError, "Option is set to `none`") newException(ValueError, "Option is set to `none`")
func unsafeCatchableError[T](_: ref T): ref CatchableError = template bindFailed*(expression) =
newException(ValueError, "ref is nil") when captures > 0:
mixin error
func unsafeCatchableError[T](_: ptr T): ref CatchableError = errorVariable[] = expression.error
newException(ValueError, "ptr is nil")
func unsafeCatchableError[Proc: proc | iterator](_: Proc): ref CatchableError =
newException(ValueError, "proc or iterator is nil")
macro bindFailed*(expression: typed) =
## Called when a binding (=?) fails.
## Assigns an error to the error variable (specified in captureBindError())
## when appropriate.
# The `expression` parameter is typed to ensure that the compiler does not
# expand bindFailed() 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`.unsafeCatchableError

View File

@ -7,7 +7,6 @@ import ./indexing
import ./operators import ./operators
import ./without import ./without
import ./withoutresult import ./withoutresult
import ./private/bareexcept
include ./private/errorban include ./private/errorban
@ -110,30 +109,12 @@ proc option*[T,E](value: Result[T,E]): ?T =
## Converts a Result into an Option. ## Converts a Result into an Option.
if value.isOk: if value.isOk:
ignoreBareExceptWarning: try: # workaround for erroneous exception tracking when T is a closure
try: # workaround for erroneous exception tracking when T is a closure value.unsafeGet.some
return value.unsafeGet.some except Exception as exception:
except Exception as exception: raise newException(Defect, exception.msg, exception)
raise newException(Defect, exception.msg, exception)
else: else:
return T.none T.none
template toOption*[T, E](value: Result[T, E]): ?T =
## Converts a Result into an Option.
value.option
proc unsafeCatchableError*[T, E](value: Result[T, E]): ref CatchableError =
## Returns the error from the Result, converted to `ref CatchableError` if
## necessary. Behaviour is undefined when the result holds a value instead of
## an error.
when E is ref CatchableError:
value.unsafeError
else:
when compiles($value.unsafeError):
newException(ResultFailure, $value.unsafeError)
else:
newException(ResultFailure, "Result is an error")
proc errorOption*[T, E](value: Result[T, E]): ?E = proc errorOption*[T, E](value: Result[T, E]): ?E =
## Returns an Option that contains the error from the Result, if it has one. ## Returns an Option that contains the error from the Result, if it has one.

View File

@ -34,7 +34,7 @@ macro without*(condition, errorname, body: untyped): untyped =
let body = body.undoSymbolResolution(errorIdent) let body = body.undoSymbolResolution(errorIdent)
quote do: quote do:
var error {.gensym.}: ref CatchableError var error: ref CatchableError
without captureBindError(error, `condition`): without captureBindError(error, `condition`):
template `errorIdent`: ref CatchableError = error template `errorIdent`: ref CatchableError = error

View File

@ -165,65 +165,6 @@ suite "optionals":
else: else:
fail() fail()
test "=? works with reference types":
var x = new int
x[] = 42
if a =? x:
check a[] == 42
else:
fail
x = nil
if a =? x:
fail
var p = proc = discard
if a =? p:
a()
else:
fail
p = nil
if a =? p:
fail
when (NimMajor, NimMinor) >= (1, 1) and (NimMajor, NimMinor) != (2, 0):
var it = iterator: int = yield 2
if a =? it:
for x in a:
check x == 2
else:
fail
it = nil
if a =? it:
fail
test "=? rejects non-reference types":
check `not` compiles do:
if a =? 0:
discard
check `not` compiles do:
if var a =? 0:
discard
check `not` compiles do:
if (a,) =? (0,):
discard
test "=? works with custom optional types":
type MyOption = distinct int
proc isSome(x: MyOption): bool = x.int >= 0
proc unsafeGet(x: MyOption): int = x.int
template toOption(x: MyOption): MyOption = x
if a =? MyOption 42:
check a == 42
else:
fail
if a =? MyOption -1:
fail
test "=? binds and unpacks tuples": test "=? binds and unpacks tuples":
if (a, b) =? (some ("test", 1)): if (a, b) =? (some ("test", 1)):
check a == "test" check a == "test"
@ -300,15 +241,6 @@ suite "optionals":
else: else:
fail() fail()
test "=? for tuples does not leak symbols into caller's scope":
const evaluated = ""
type T = string
if (a,) =? some (0,):
check a == 0
check option is proc
check evaluated is string
check T is string
test "without statement can be used for early returns": test "without statement can be used for early returns":
proc test1 = proc test1 =
without a =? 42.some: without a =? 42.some:
@ -337,28 +269,13 @@ suite "optionals":
test ".?[] can be used for indexing strings without raising IndexDefect": test ".?[] can be used for indexing strings without raising IndexDefect":
let str = "a" let str = "a"
check str.?[0] == 'a'.some check str.?[0] == 'a'.some
check str.?[1] == char.none check str.?[1] == char.none
check str.?[-1] == char.none
test ".?[] can be used for indexing sequences without raising IndexDefect": test ".?[] can be used for indexing sequences without raising IndexDefect":
let sequence = @[1] let sequence = @[1]
check sequence.?[0] == 1.some check sequence.?[0] == 1.some
check sequence.?[1] == int.none check sequence.?[1] == int.none
check sequence.?[-1] == int.none
test ".?[] can be used for indexing openArrays without raising IndexDefect":
proc checkOpenArray(oa: openArray[int]): void =
check oa.?[0] == 1.some
check oa.?[1] == int.none
check oa.?[-1] == int.none
checkOpenArray(@[1])
test ".?[] evaluates openArray expression only once":
var count = 0
discard (inc count; @[1].toOpenArray(0, 0)).?[0]
check count == 1
test ".?[] can be followed by calls, operators and indexing": test ".?[] can be followed by calls, operators and indexing":
let table = @{"a": @[41, 42]}.toTable let table = @{"a": @[41, 42]}.toTable

View File

@ -326,48 +326,6 @@ suite "result":
test1() test1()
test2() test2()
test "without statement with error handles references as well":
proc test =
var x: ref int = nil
without a =? x, error:
check error.msg == "ref is nil"
return
fail
test()
test "without statement with error handles pointers as well":
proc test =
var x: ptr int = nil
without a =? x, error:
check error.msg == "ptr is nil"
return
fail
test()
test "without statement with error handles closures as well":
proc test =
var x = proc = discard
x = nil
without a =? x, error:
check error.msg == "proc or iterator is nil"
return
fail
test()
test "without statement with error handles iterators as well":
when (NimMajor, NimMinor) != (2, 0):
proc test =
var x: iterator: int = nil
without a =? x, error:
check error.msg == "proc or iterator is nil"
return
fail
test()
test "without statement with error can be used more than once": test "without statement with error can be used more than once":
proc test = proc test =
without a =? 42.success, error: without a =? 42.success, error:
@ -454,17 +412,6 @@ suite "result":
for i in 0..<1000: for i in 0..<1000:
spawn fail(i) 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": test "catch can be used to convert exceptions to results":
check parseInt("42").catch == 42.success check parseInt("42").catch == 42.success
check parseInt("foo").catch.error of ValueError check parseInt("foo").catch.error of ValueError
@ -643,19 +590,6 @@ suite "result":
someProc(42.success) someProc(42.success)
someProc(int.failure "some error") someProc(int.failure "some error")
type TypeWithSideEffect = object
proc `$`*(value: TypeWithSideEffect): string {.sideEffect.} =
discard
suite "result side effects":
test "without statement with error works when `$` has side effects":
proc foo =
without x =? TypeWithSideEffect.failure("error"), error:
discard error
return
fail()
foo()
import pkg/questionable/resultsbase import pkg/questionable/resultsbase
@ -663,7 +597,7 @@ suite "result compatibility":
type R = Result[int, string] type R = Result[int, string]
let good = R.ok 42 let good = R.ok 42
let bad = R.err "some error" let bad = R.err "error"
test "|?, =? and .option work on other types of Result": test "|?, =? and .option work on other types of Result":
check bad |? 43 == 43 check bad |? 43 == 43
@ -681,13 +615,3 @@ suite "result compatibility":
fail fail
without b =? good: without b =? good:
fail fail
test "without statement with error works on other type of Result":
without value =? bad, error:
check error of ResultFailure
check error.msg == "some error"
test "without statement with error works on Result[T, void]":
without value =? Result[int, void].err, error:
check error of ResultFailure
check error.msg == "Result is an error"