Compare commits

...

42 Commits
0.10.5 ... main

Author SHA1 Message Date
Adam Uhlíř
2906828765
Add errorOption example to Readme
Added example for errorOption conversion in Readme.
2025-10-22 16:12:58 +02:00
Marcin Czenko
2e7f20392b Update Readme.md
Missing "to"
2024-12-04 01:01:47 +01:00
Mark Spanbroek
3dcf21491d without with error variable works for any Result type 2024-05-29 12:45:40 +02:00
Mark Spanbroek
82d90b67bc version 0.10.15 2024-04-20 08:08:31 +02:00
Mark Spanbroek
b098ae696a Workaround for Nim gensym bug
Occasionally different `evaluated` symbols would be
gensymmed to the same symbol.
2024-04-20 08:07:28 +02:00
Mark Spanbroek
83ae4a6409 version 0.10.14 2024-03-10 12:14:59 +01:00
Mark Spanbroek
57e467b8b0 Fix: without should work when $ has side effects
It's ok to use unsafeError in bindFailed, because we've
already checked that the result contains an error.
2024-03-10 12:13:25 +01:00
Mark Spanbroek
47692e0d92 version 0.10.13 2024-01-09 16:57:24 +01:00
Mark Spanbroek
6ef525cfe2 Reference types are handled by without statement with error 2024-01-09 16:52:39 +01:00
Mark Spanbroek
43e7deb827 Fix BareExcept warnings 2024-01-09 16:46:01 +01:00
Mark Spanbroek
4a74d65e17 Mark generated error variable explicitly as {.gensym.}
Co-Authored-By: Jaremy Creechley <creechley@gmail.com>
2024-01-09 16:45:32 +01:00
Mark Spanbroek
672248f431 Get rid of trick with type parameter 2024-01-09 16:45:32 +01:00
Mark Spanbroek
d463d491cc 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.
2024-01-09 16:45:32 +01:00
Tomasz Bekas
1f0afff48b
Support for .?[] operator on openArrays (#52)
* Support for .?[] operator on openArrays

* Operator .?[] evaluates openArray expression only once

* Fix for Nim 1.2.x

---------

Co-authored-by: Mark Spanbroek <mark@spanbroek.net>
2023-11-20 14:58:49 +01:00
Mark Spanbroek
2dd6b6b220 version 0.10.12 2023-11-14 10:57:03 +01:00
Nickolay Bukreyev
0f095d6b7c Overload toOption for Result
Necessary for the `=?` operator to work.
2023-11-14 10:52:45 +01:00
Nickolay Bukreyev
cdf639c4ea Support binding closure iterators (except on Nim == 2.0)
See nim-lang/Nim#22932.
2023-11-14 10:52:45 +01:00
Nickolay Bukreyev
fe47a19825 Create identifiers with genSym 2023-11-14 10:52:45 +01:00
Nickolay Bukreyev
440debc7c3 Accept only optional and reference types as RHS of =?
The fact that `option(x)` works for non-reference types (being an alias
for `some`) is an stdlib's design mistake that bites us here.
2023-11-14 10:52:45 +01:00
Mark Spanbroek
c2a08bd703 version 0.10.11 2023-11-08 10:14:25 +01:00
Mark Spanbroek
52e11f2011 Fix compilation issue with Nim 1.6.16
Workaround for https://github.com/nim-lang/Nim/issues/22897
2023-11-08 10:09:10 +01:00
Mark Spanbroek
af4f194597 Fix tests for Nim < 2.0 2023-08-30 11:02:37 +02:00
Mark Spanbroek
5c8d422ac8 fix error binding in without statement on multiple threads 2023-08-30 11:02:37 +02:00
Mark Spanbroek
e56cf86c4a Indexing of strings and sequences should not catch Defect
Catching a Defect does not always work, depending on how
it's compiled.
2023-08-03 15:17:40 +02:00
Tomasz Bekas
416b6dd566 Support seq indexing 2023-08-03 12:56:12 +02:00
Mark Spanbroek
08581f5efd Run CI on Nim 1.2, 1.4, 1.6 and 2.0 2023-08-03 09:41:43 +02:00
Mark Spanbroek
9af4ce1ca1 Better compilation error when calling without with wrong parameter 2023-08-03 09:41:43 +02:00
Mark Spanbroek
1569ef4526 Fix tests for stew with Nim < 1.6
Stew now requires a Nim >= 1.6, so this disables
testing stew with earlier Nim versions.
2023-07-28 09:57:51 +02:00
Mark Spanbroek
b3cf35ac45 version 0.10.10 2023-07-03 11:17:51 +02:00
Mark Spanbroek
3bce3088a7 Use root configuration when running tests 2023-07-03 11:16:56 +02:00
Mark Spanbroek
ffe0faa3bb Only enable --styleCheck:usages on Nim versions that support it 2023-07-03 11:16:56 +02:00
Mark Spanbroek
0d7ce8efde Provide conversion to string $ for Results 2023-06-26 10:04:02 +02:00
Mark Spanbroek
58c10fb333 version 0.10.9 2023-06-26 10:02:25 +02:00
Mark Spanbroek
8daae27089 Do not edit AST nodes, make a copy first
Fixes error "typechecked nodes may not be modified"
2023-06-26 09:59:14 +02:00
Mark Spanbroek
b18444a6d0 Support renamed 'results' library 2023-06-05 17:26:46 +02:00
Mark Spanbroek
f957dd59d6 Fix tests on Nim 1.6.12
Ignore the nimble.lock at the root level when running tests
2023-06-05 17:26:46 +02:00
Mark Spanbroek
6cbbda7e4d version 0.10.8 2023-02-14 09:31:58 +01:00
Eric Mastro
096ca864b0
tuple binding and unpacking support (#29)
* tuple binding and unpacking support

* support tuple binding in nim < 1.6

* Use ? instead of Option[] in tests

* Test binding of optional tuple, not just tuple

* Add tests for tuple unpacking of Result

* Remove unused import

* Rearrange tuple binding code

Introduce separate proc for creation of unpacking
statement.

Use `quote do` to make code as similar as possible
to the `bindLet` and `bindVar` templates.

* Add tests for failed tuple bindings

---------

Co-authored-by: Mark Spanbroek <mark@spanbroek.net>
2023-02-14 09:56:32 +11:00
Mark Spanbroek
1dcef4b302 version 0.10.7 2022-10-20 12:13:43 +02:00
Mark Spanbroek
f78bdd9d58 Fix redefinition of 'T`gensymXX' error
In rare instances, the Nim compiler will generate the same
symbol more than once. Adding a block works around this issue.

Reproducing this behavior in a unit test has proved elusive.
2022-10-20 05:10:44 -05:00
Mark Spanbroek
30e4184a99 version 0.10.6 2022-09-28 11:37:36 +02:00
Mark Spanbroek
4d631b1ba9 Fix: ensure that options and results are only evaluated once 2022-09-28 11:36:28 +02:00
27 changed files with 679 additions and 106 deletions

View File

@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
strategy: strategy:
matrix: matrix:
nim: [stable, 1.4.8, 1.2.18] nim: [stable, 1.6.16, 1.4.8, 1.2.18]
steps: steps:
- uses: actions/checkout@v2 - uses: actions/checkout@v2
- uses: iffy/install-nim@v3 - uses: iffy/install-nim@v3

View File

@ -12,14 +12,14 @@ 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.5 & < 0.11.0" requires "questionable >= 0.10.15 & < 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
[result][2] package, or the [stew][4] package: [result][2] package, or the [stew][4] package:
```nim ```nim
requires "result" # either this requires "results" # either this
requires "stew" # or this requires "stew" # or this
``` ```
@ -151,7 +151,7 @@ have to explicitly import the `questionable/results` module:
import questionable/results import questionable/results
``` ```
You can use `?!` make a Result type. These Result types either hold a value or You can use `?!` to 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,6 +226,7 @@ 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,5 +1,6 @@
# Style Check # Style Check
--styleCheck:usages if (NimMajor, NimMinor, NimPatch) >= (1, 6, 6):
--styleCheck:usages
if (NimMajor, NimMinor) < (1, 6): if (NimMajor, NimMinor) < (1, 6):
--styleCheck:hint --styleCheck:hint
else: else:

View File

@ -1,10 +1,10 @@
version = "0.10.5" version = "0.10.15"
author = "Questionable Authors" author = "Questionable Authors"
description = "Elegant optional types" description = "Elegant optional types"
license = "MIT" license = "MIT"
task test, "Runs the test suite": task test, "Runs the test suite":
for module in ["options", "result", "stew"]: for module in ["options", "results", "stew"]:
withDir "testmodules/" & module: withDir "testmodules/" & module:
delEnv "NIMBLE_DIR" # use nimbledeps dir delEnv "NIMBLE_DIR" # use nimbledeps dir
exec "nimble install -d -y" exec "nimble install -d -y"

View File

@ -2,15 +2,25 @@ import std/options
import std/macros import std/macros
import ./private/binderror import ./private/binderror
proc option[T](option: Option[T]): Option[T] = when (NimMajor, NimMinor) < (1, 1):
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): bool = template bindLet(name, expression): untyped =
let evaluated = expression let evaluated = expression
let option = evaluated.option let option = evaluated.toOption
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()
@ -19,9 +29,9 @@ template bindLet(name, expression): bool =
placeholder(T) placeholder(T)
option.isSome option.isSome
template bindVar(name, expression): bool = template bindVar(name, expression): untyped =
let evaluated = expression let evaluated = expression
let option = evaluated.option let option = evaluated.toOption
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()
@ -30,14 +40,50 @@ template bindVar(name, expression): bool =
placeholder(T) placeholder(T)
option.isSome option.isSome
proc newUnpackTupleNode(names: NimNode, value: NimNode): NimNode =
# builds tuple unpacking statement, eg: let (a, b) = value
let vartuple = nnkVarTuple.newTree()
for i in 0..<names.len:
vartuple.add names[i]
vartuple.add newEmptyNode()
vartuple.add value
nnkLetSection.newTree(vartuple)
macro bindTuple(names, expression): bool =
let opt = genSym(nskLet, "option")
let evaluated = genSym(nskLet, "evaluated")
let T = genSym(nskType, "T")
let value = quote do:
if `opt`.isSome:
`opt`.unsafeGet()
else:
bindFailed(`evaluated`)
placeholder(`T`)
let letsection = newUnpackTupleNode(names, value)
quote do:
let `evaluated` = `expression`
let `opt` = `evaluated`.toOption
type `T` = typeof(`opt`.unsafeGet())
`letsection`
`opt`.isSome
macro `=?`*(name, expression): bool = macro `=?`*(name, expression): bool =
## The `=?` operator lets you bind the value inside an Option or Result to a ## The `=?` operator lets you bind the value inside an Option or Result to a
## new variable. It can be used inside of a conditional expression, for ## new variable. It can be used inside of a conditional expression, for
## instance in an `if` statement. ## instance in an `if` statement.
name.expectKind({nnkIdent, nnkVarTy}) when (NimMajor, NimMinor) < (1, 6):
name.expectKind({nnkIdent, nnkVarTy, nnkTupleConstr, nnkPar})
else:
name.expectKind({nnkIdent, nnkVarTy, nnkTupleConstr})
if name.kind == nnkIdent: if name.kind == nnkIdent:
quote do: bindLet(`name`, `expression`) quote do: bindLet(`name`, `expression`)
elif name.kind == nnkTupleConstr or name.kind == nnkPar:
quote do: bindTuple(`name`, `expression`)
else: else:
let name = name[0] let name = name[0]
quote do: bindVar(`name`, `expression`) quote do: bindVar(`name`, `expression`)

View File

@ -15,50 +15,34 @@ macro expectReturnType(identifier: untyped, expression: untyped): untyped =
when compiles(`expression`) and not compiles(typeof `expression`): when compiles(`expression`) and not compiles(typeof `expression`):
{.error: `message`.} {.error: `message`.}
template `.?`*(option: typed, identifier: untyped{nkIdent}): untyped = template chain(option: typed, identifier: untyped{nkIdent}): untyped =
## The `.?` chaining operator is used to safely access fields and call procs
## on Options or Results. The expression is only evaluated when the preceding
## Option or Result has a value.
# chain is of shape: option.?identifier # chain is of shape: option.?identifier
expectReturnType(identifier, option.unsafeGet.identifier) expectReturnType(identifier, option.unsafeGet.identifier)
option ->? option.unsafeGet.identifier option ->? option.unsafeGet.identifier
macro `.?`*(option: typed, infix: untyped{nkInfix}): untyped = macro chain(option: typed, infix: untyped{nkInfix}): untyped =
## The `.?` chaining operator is used to safely access fields and call procs
## on Options or Results. The expression is only evaluated when the preceding
## Option or Result has a value.
# chain is of shape: option.?left `operator` right # chain is of shape: option.?left `operator` right
let infix = infix.copyNimTree()
let left = infix[1] let left = infix[1]
infix[1] = quote do: `option`.?`left` infix[1] = quote do: `option`.?`left`
infix infix
macro `.?`*(option: typed, bracket: untyped{nkBracketExpr}): untyped = macro chain(option: typed, bracket: untyped{nkBracketExpr}): untyped =
## The `.?` chaining operator is used to safely access fields and call procs
## on Options or Results. The expression is only evaluated when the preceding
## Option or Result has a value.
# chain is of shape: option.?left[right] # chain is of shape: option.?left[right]
let bracket = bracket.copyNimTree()
let left = bracket[0] let left = bracket[0]
bracket[0] = quote do: `option`.?`left` bracket[0] = quote do: `option`.?`left`
bracket bracket
macro `.?`*(option: typed, dot: untyped{nkDotExpr}): untyped = macro chain(option: typed, dot: untyped{nkDotExpr}): untyped =
## The `.?` chaining operator is used to safely access fields and call procs
## on Options or Results. The expression is only evaluated when the preceding
## Option or Result has a value.
# chain is of shape: option.?left.right # chain is of shape: option.?left.right
let dot = dot.copyNimTree()
let left = dot[0] let left = dot[0]
dot[0] = quote do: `option`.?`left` dot[0] = quote do: `option`.?`left`
dot dot
macro `.?`*(option: typed, call: untyped{nkCall}): untyped = macro chain(option: typed, call: untyped{nkCall}): untyped =
## The `.?` chaining operator is used to safely access fields and call procs let call = call.copyNimTree()
## on Options or Results. The expression is only evaluated when the preceding
## Option or Result has a value.
let procedure = call[0] let procedure = call[0]
if call.len == 1: if call.len == 1:
# chain is of shape: option.?procedure() # chain is of shape: option.?procedure()
@ -81,11 +65,15 @@ macro `.?`*(option: typed, call: untyped{nkCall}): untyped =
expectReturnType(`procedure`, `call`) expectReturnType(`procedure`, `call`)
`option` ->? `call` `option` ->? `call`
macro `.?`*(option: typed, symbol: untyped): untyped = macro chain(option: typed, symbol: untyped): untyped =
## The `.?` chaining operator is used to safely access fields and call procs
## on Options or Results. The expression is only evaluated when the preceding
## Option or Result has a value.
symbol.expectSym() symbol.expectSym()
let expression = ident($symbol) let expression = ident($symbol)
quote do: `option`.?`expression` quote do: `option`.?`expression`
template `.?`*(left: typed, right: untyped): untyped =
## The `.?` chaining operator is used to safely access fields and call procs
## on Options or Results. The expression is only evaluated when the preceding
## Option or Result has a value.
block:
let evaluated = left
chain(evaluated, right)

View File

@ -1,11 +1,32 @@
import std/macros import std/macros
import std/options
proc safeGet[T](expression: seq[T] | openArray[T], index: int): Option[T] =
if index >= expression.low and index <= expression.high:
expression[index].some
else:
T.none
proc safeGet(expression: string, index: int): Option[char] =
if index >= expression.low and index <= expression.high:
expression[index].some
else:
char.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]
let index = brackets[0] let index = brackets[0]
quote do: quote do:
type T = typeof(`expression`[`index`]) block:
try: type T = typeof(`expression`[`index`])
`expression`[`index`].some try:
except KeyError: `expression`[`index`].some
T.none except KeyError:
T.none

View File

@ -1,12 +1,19 @@
template liftUnary*(T: type, operator: untyped) = template liftUnary*(T: type, operator: untyped) =
template `operator`*(a: T): untyped = template `operator`*(a: T): untyped =
a ->? `operator`(a.unsafeGet()) block:
let evaluated = a
evaluated ->? `operator`(evaluated.unsafeGet())
template liftBinary*(T: type, operator: untyped) = template liftBinary*(T: type, operator: untyped) =
template `operator`*(a: T, b: T): untyped = template `operator`*(a: T, b: T): untyped =
(a, b) ->? `operator`(a.unsafeGet, b.unsafeGet) block:
let evalA = a
let evalB = b
(evalA, evalB) ->? `operator`(evalA.unsafeGet, evalB.unsafeGet)
template `operator`*(a: T, b: typed): untyped = template `operator`*(a: T, b: typed): untyped =
a ->? `operator`(a.unsafeGet(), b) block:
let evalA = a
evalA ->? `operator`(evalA.unsafeGet(), b)

View File

@ -44,7 +44,7 @@ template `->?`*[T,U,V](options: (?T, ?U), expression: ?V): ?V =
template `->?`*[T,U,V](options: (?T, ?U), expression: V): ?V = template `->?`*[T,U,V](options: (?T, ?U), expression: V): ?V =
options ->? expression.some options ->? expression.some
template `|?`*[T](option: ?T, fallback: T): T = proc `|?`*[T](option: ?T, fallback: T): T =
## Use the `|?` operator to supply a fallback value when an Option does not ## Use the `|?` operator to supply a fallback value when an Option does not
## hold a value. ## hold a value.
@ -56,11 +56,13 @@ template `|?`*[T](option: ?T, fallback: T): T =
macro `.?`*[T](option: ?T, brackets: untyped{nkBracket}): untyped = macro `.?`*[T](option: ?T, brackets: untyped{nkBracket}): untyped =
let index = brackets[0] let index = brackets[0]
quote do: quote do:
type U = typeof(`option`.unsafeGet().?[`index`].unsafeGet()) block:
if `option`.isSome: let evaluated = `option`
`option`.unsafeGet().?[`index`] type U = typeof(evaluated.unsafeGet().?[`index`].unsafeGet())
else: if evaluated.isSome:
U.none evaluated.unsafeGet().?[`index`]
else:
U.none
Option.liftUnary(`-`) Option.liftUnary(`-`)
Option.liftUnary(`+`) Option.liftUnary(`+`)

View File

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

View File

@ -1,24 +1,54 @@
import std/options import std/options
import std/macros
var captures {.global, compileTime.}: int # A stack of names of error variables. Keeps track of the error variables that
var errorVariable: ptr ref CatchableError # are given to captureBindError().
var errorVariableNames {.global, compileTime.}: seq[string]
template captureBindError*(error: var ref CatchableError, expression): auto = macro captureBindError*(error: var ref CatchableError, expression): auto =
let previousErrorVariable = errorVariable ## Ensures that an error is assigned to the error variable when a binding (=?)
errorVariable = addr error ## fails inside the expression.
static: inc captures # name of the error variable as a string literal
let evaluated = expression let errorVariableName = newLit($error)
static: dec captures
errorVariable = previousErrorVariable let evaluated = genSym(nskLet, "evaluated")
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`
evaluated func unsafeCatchableError[T](_: Option[T]): ref CatchableError =
func error[T](option: Option[T]): ref CatchableError =
newException(ValueError, "Option is set to `none`") newException(ValueError, "Option is set to `none`")
template bindFailed*(expression) = func unsafeCatchableError[T](_: ref T): ref CatchableError =
when captures > 0: newException(ValueError, "ref is nil")
mixin error
errorVariable[] = expression.error func unsafeCatchableError[T](_: ptr T): ref CatchableError =
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,6 +7,7 @@ import ./indexing
import ./operators import ./operators
import ./without import ./without
import ./withoutresult import ./withoutresult
import ./private/bareexcept
include ./private/errorban include ./private/errorban
@ -73,6 +74,12 @@ proc isFailure*[T](value: ?!T): bool =
value.isErr value.isErr
proc `$`*[T](value: ?!T): string =
if value.isSuccess:
"success(" & $(!value) & ")"
else:
"failure(\"" & $(value.error.msg) & "\")"
template `->?`*[T,U](value: ?!T, expression: ?!U): ?!U = template `->?`*[T,U](value: ?!T, expression: ?!U): ?!U =
if value.isFailure: if value.isFailure:
U.failure(value.error) U.failure(value.error)
@ -93,7 +100,7 @@ template `->?`*[T,U,V](values: (?!T, ?!U), expression: ?!V): ?!V =
template `->?`*[T,U,V](values: (?!T, ?!U), expression: V): ?!V = template `->?`*[T,U,V](values: (?!T, ?!U), expression: V): ?!V =
values ->? expression.success values ->? expression.success
template `|?`*[T,E](value: Result[T,E], fallback: T): T = proc `|?`*[T,E](value: Result[T,E], fallback: T): T =
## Use the `|?` operator to supply a fallback value when a Result does not ## Use the `|?` operator to supply a fallback value when a Result does not
## hold a value. ## hold a value.
@ -103,12 +110,30 @@ 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:
try: # workaround for erroneous exception tracking when T is a closure ignoreBareExceptWarning:
value.unsafeGet.some try: # workaround for erroneous exception tracking when T is a closure
except Exception as exception: return value.unsafeGet.some
raise newException(Defect, exception.msg, exception) except Exception as exception:
raise newException(Defect, exception.msg, exception)
else: else:
T.none return 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

@ -1,7 +1,7 @@
template tryImport(module) = import module template tryImport(module) = import module
when compiles tryImport pkg/result: when compiles tryImport pkg/results:
import pkg/result/../results import pkg/results
else: else:
import pkg/stew/results import pkg/stew/results

View File

@ -2,16 +2,18 @@ import std/macros
import ./without import ./without
import ./private/binderror import ./private/binderror
const symbolKinds = {nnkSym, nnkOpenSymChoice, nnkClosedSymChoice}
const identKinds = {nnkIdent} + symbolKinds
proc undoSymbolResolution(expression, ident: NimNode): NimNode = proc undoSymbolResolution(expression, ident: NimNode): NimNode =
## Finds symbols in the expression that match the `ident` and replaces them ## Finds symbols in the expression that match the `ident` and replaces them
## with `ident`, effectively undoing any symbol resolution that happened ## with `ident`, effectively undoing any symbol resolution that happened
## before. ## before.
const symbolKinds = {nnkSym, nnkOpenSymChoice, nnkClosedSymChoice}
if expression.kind in symbolKinds and eqIdent($expression, $ident): if expression.kind in symbolKinds and eqIdent($expression, $ident):
return ident return ident
let expression = expression.copyNimTree()
for i in 0..<expression.len: for i in 0..<expression.len:
expression[i] = undoSymbolResolution(expression[i], ident) expression[i] = undoSymbolResolution(expression[i], ident)
@ -21,6 +23,9 @@ macro without*(condition, errorname, body: untyped): untyped =
## Used to place guards that ensure that a Result contains a value. ## Used to place guards that ensure that a Result contains a value.
## Exposes error when Result does not contain a value. ## Exposes error when Result does not contain a value.
if errorname.kind notin identKinds:
error("expected an identifier, got " & errorname.repr, errorname)
let errorIdent = ident $errorname let errorIdent = ident $errorname
# Nim's early symbol resolution might have picked up a symbol with the # Nim's early symbol resolution might have picked up a symbol with the
@ -29,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: ref CatchableError var error {.gensym.}: ref CatchableError
without captureBindError(error, `condition`): without captureBindError(error, `condition`):
template `errorIdent`: ref CatchableError = error template `errorIdent`: ref CatchableError = error

View File

@ -0,0 +1,2 @@
--path:"../.."
import "../../config.nims"

View File

@ -1 +0,0 @@
--path:"../.."

View File

@ -115,17 +115,6 @@ suite "optionals":
else: else:
fail fail
test "=? evaluates optional expression only once":
var count = 0
if a =? (inc count; 42.some):
let b {.used.} = a
check count == 1
count = 0
if var a =? (inc count; 42.some):
let b {.used.} = a
check count == 1
test "=? works in generic code": test "=? works in generic code":
proc toString[T](option: ?T): string = proc toString[T](option: ?T): string =
if value =? option: if value =? option:
@ -176,6 +165,150 @@ 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":
if (a, b) =? (some ("test", 1)):
check a == "test"
check b == 1
else:
fail()
if (a, b) =? none (string, int):
discard a
discard b
fail()
test "=? binds and unpacks tuples with named fields":
if (a, b) =? (some (desc: "test", id: 1)):
check a == "test"
check b == 1
else:
fail()
test "=? binds and unpacks tuples returned from proc":
proc returnsTuple(): ?tuple[name: string, id: int] = some ("test", 1)
if (a, b) =? returnsTuple():
check a == "test"
check b == 1
else:
fail()
test "=? binds and unpacks tuples returned from proc with unnamed fields":
proc returnsTuple(): ?(string, int,) = some ("test", 1,)
if (a, b,) =? returnsTuple():
check a == "test"
check b == 1
else:
fail()
test "=? binds and unpacks tuples with _":
if (_, b) =? some ("test", 1):
check b == 1
else:
fail()
test "=? binds and unpacks tuples with named fields":
if (a, b) =? some (desc: "test", id: 1):
check a == "test"
check b == 1
else:
fail()
test "=? binds variable to tuples with named fields":
if t =? some (desc: "test", id: 1):
check t.desc == "test"
check t.id == 1
else:
fail()
test "=? binds to tuple types":
type MyTuple = tuple
desc: string
id: int
let mt: MyTuple = ("test", 1)
if t =? (some mt):
check t.desc == "test"
check t.id == 1
else:
fail()
if (a, b) =? (some mt):
check a == "test"
check b == 1
else:
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:
@ -202,6 +335,31 @@ suite "optionals":
check table.?["a"] == 1.some check table.?["a"] == 1.some
check table.?["c"] == int.none check table.?["c"] == int.none
test ".?[] can be used for indexing strings without raising IndexDefect":
let str = "a"
check str.?[0] == 'a'.some
check str.?[1] == char.none
check str.?[-1] == char.none
test ".?[] can be used for indexing sequences without raising IndexDefect":
let sequence = @[1]
check sequence.?[0] == 1.some
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
check table.?["a"].isSome check table.?["a"].isSome
@ -274,6 +432,58 @@ suite "optionals":
check a.?[1] == 42.some check a.?[1] == 42.some
test ".? chain evaluates optional expression only once":
var count = 0
discard (inc count; @[41, 42].some).?len
check count == 1
test "=? evaluates optional expression only once":
var count = 0
if a =? (inc count; 42.some):
let b {.used.} = a
check count == 1
count = 0
if var a =? (inc count; 42.some):
let b {.used.} = a
check count == 1
test "|? evaluates optional expression only once":
var count = 0
discard (inc count; 42.some) |? 43
check count == 1
test ".?[] evaluates optional expression only once":
# indexing on optional sequence:
block:
var count = 0
discard (inc count; @[41, 42].some).?[0]
check count == 1
# indexing on normal sequence:
block:
var count = 0
discard (inc count; @[41, 42]).?[0]
check count == 1
test "lifted unary operators evaluate optional expression only once":
var count = 0
discard -(inc count; 42.some)
check count == 1
test "lifted binary operators evaluate optional expressions only once":
# lifted operator on two options:
block:
var count1, count2 = 0
discard (inc count1; 40.some) + (inc count2; 2.some)
check count1 == 1
check count2 == 1
# lifted operator on option and value:
block:
var count1, count2 = 0
discard (inc count1; 40.some) + (inc count2; 2)
check count1 == 1
check count2 == 1
test "examples from readme work": test "examples from readme work":
var x: ?int var x: ?int

View File

@ -4,7 +4,7 @@ description = "Questionable tests for std/option"
license = "MIT" license = "MIT"
task test, "Runs the test suite": task test, "Runs the test suite":
var options = "-f -r" var options = "-f -r --skipParentCfg"
when (NimMajor, NimMinor) >= (1, 4): when (NimMajor, NimMinor) >= (1, 4):
options &= " --warningAsError[UnsafeDefault]:on" options &= " --warningAsError[UnsafeDefault]:on"
options &= " --warningAsError[ProveInit]:on" options &= " --warningAsError[ProveInit]:on"

View File

@ -1 +0,0 @@
--path:"../.."

View File

@ -0,0 +1,3 @@
--path:"../.."
--threads:on
import "../../config.nims"

View File

@ -3,8 +3,11 @@ import std/options
import std/sequtils import std/sequtils
import std/strutils import std/strutils
import std/sugar import std/sugar
import std/threadpool
import pkg/questionable/results import pkg/questionable/results
{.experimental: "parallel".}
suite "result": suite "result":
let error = newException(CatchableError, "error") let error = newException(CatchableError, "error")
@ -14,6 +17,10 @@ suite "result":
check (?!string is Result[string, ref CatchableError]) check (?!string is Result[string, ref CatchableError])
check (?!seq[bool] is Result[seq[bool], ref CatchableError]) check (?!seq[bool] is Result[seq[bool], ref CatchableError])
test "conversion to string $ works for ?!Types":
check $42.success == "success(42)"
check $(int.failure "some error") == "failure(\"some error\")"
test "! gets value or raises Defect": test "! gets value or raises Defect":
check !42.success == 42 check !42.success == 42
expect Defect: discard !int.failure error expect Defect: discard !int.failure error
@ -165,6 +172,82 @@ suite "result":
check called check called
test "=? binds and unpacks tuples":
if (a, b) =? (success ("test", 1)):
check a == "test"
check b == 1
else:
fail()
if (a, b) =? (string, int).failure(error):
discard a
discard b
fail()
test "=? binds and unpacks tuples with named fields":
if (a, b) =? (success (desc: "test", id: 1)):
check a == "test"
check b == 1
else:
fail()
test "=? binds and unpacks tuples returned from proc":
proc returnsTuple(): ?!tuple[name: string, id: int] = success ("test", 1)
if (a, b) =? returnsTuple():
check a == "test"
check b == 1
else:
fail()
test "=? binds and unpacks tuples returned from proc with unnamed fields":
proc returnsTuple(): ?!(string, int,) = success ("test", 1,)
if (a, b,) =? returnsTuple():
check a == "test"
check b == 1
else:
fail()
test "=? binds and unpacks tuples with _":
if (_, b) =? success ("test", 1):
check b == 1
else:
fail()
test "=? binds and unpacks tuples with named fields":
if (a, b) =? success (desc: "test", id: 1):
check a == "test"
check b == 1
else:
fail()
test "=? binds variable to tuples with named fields":
if t =? success (desc: "test", id: 1):
check t.desc == "test"
check t.id == 1
else:
fail()
test "=? binds to tuple types":
type MyTuple = tuple
desc: string
id: int
let mt: MyTuple = ("test", 1)
if t =? (success mt):
check t.desc == "test"
check t.id == 1
else:
fail()
if (a, b) =? (success mt):
check a == "test"
check b == 1
else:
fail()
test "without statement works for results": test "without statement works for results":
proc test1 = proc test1 =
without a =? 42.success: without a =? 42.success:
@ -243,6 +326,48 @@ 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:
@ -321,6 +446,25 @@ suite "result":
foo() foo()
test "without statement with error works with multiple threads":
proc fail(number: int) =
without _ =? int.failure "error" & $number, error:
check error.msg == "error" & $number
parallel:
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": 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
@ -396,6 +540,61 @@ suite "result":
check (a & b) == 42.success check (a & b) == 42.success
test ".? chain evaluates result only once":
var count = 0
discard (inc count; @[41, 42].success).?len
check count == 1
test "=? evaluates result only once":
var count = 0
if a =? (inc count; 42.success):
let b {.used.} = a
check count == 1
count = 0
if var a =? (inc count; 42.success):
let b {.used.} = a
check count == 1
test "|? evaluates result only once":
var count = 0
discard (inc count; 42.success) |? 43
check count == 1
test ".?[] evaluates result only once":
var count = 0
discard (inc count; @[41, 42].success).?[0]
check count == 1
test "lifted unary operators evaluate result only once":
var count = 0
discard -(inc count; 42.success)
check count == 1
test "lifted binary operators evaluate results only once":
# lifted operator on two options:
block:
var count1, count2 = 0
discard (inc count1; 40.success) + (inc count2; 2.success)
check count1 == 1
check count2 == 1
# lifted operator on option and value:
block:
var count1, count2 = 0
discard (inc count1; 40.success) + (inc count2; 2)
check count1 == 1
check count2 == 1
test "conversion to option evaluates result only once":
var count = 0
discard (inc count; 42.success).option
check count == 1
test "conversion to error evaluates result only once":
var count = 0
discard (inc count; int.failure(error)).errorOption
check count == 1
test "examples from readme work": test "examples from readme work":
proc works: ?!seq[int] = proc works: ?!seq[int] =
@ -444,6 +643,19 @@ 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
@ -451,7 +663,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 "error" let bad = R.err "some 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
@ -469,3 +681,13 @@ 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"

View File

@ -3,7 +3,7 @@ author = "Questionable Authors"
description = "Questionable tests for pkg/result" description = "Questionable tests for pkg/result"
license = "MIT" license = "MIT"
requires "result" requires "results"
task test, "Runs the test suite": task test, "Runs the test suite":
exec "nim c -f -r test.nim" exec "nim c -f -r --skipParentCfg test.nim"

View File

@ -0,0 +1,3 @@
--path:"../.."
--threads:on
import "../../config.nims"

View File

@ -1 +0,0 @@
--path:"../.."

View File

@ -1 +1 @@
include ../result/test include ../results/test

View File

@ -3,7 +3,11 @@ author = "Questionable Authors"
description = "Questionable tests for pkg/stew" description = "Questionable tests for pkg/stew"
license = "MIT" license = "MIT"
requires "stew" when (NimMajor, NimMinor) >= (1, 6):
requires "stew"
task test, "Runs the test suite": task test, "Runs the test suite":
exec "nim c -f -r test.nim" exec "nim c -f -r --skipParentCfg test.nim"
else:
task test, "Runs the test suite":
echo "Warning: Skipping test with stew on Nim < 1.6"