Compare commits

...

75 Commits
0.9.1 ... 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
Mark Spanbroek
82408a5ca2 version 0.10.5 2022-08-10 13:39:00 +02:00
Mark Spanbroek
cbdd882b9f Fix: unused variable 2022-08-10 13:35:47 +02:00
Mark Spanbroek
9e3a822877 Fix: disregard early symbol lookup for error variable 2022-08-10 13:35:47 +02:00
Mark Spanbroek
cfe17ca899 Fix error when using an existing name as error variable 2022-08-10 13:35:47 +02:00
Mark Spanbroek
90ea780ba9 Fix: nested without calls in generic code 2022-08-10 13:29:09 +02:00
Mark Spanbroek
4abeef5c36 Fix: without statement with error works in nested calls 2022-08-10 13:29:09 +02:00
Mark Spanbroek
22f2c9761a Disable warning about DotLikeOps
Give a clear compiler error when questionable
is used with the -d:nimPreviewDotLikeOps flag.

Reason: the option is likely to be deprecated
or removed. More info:
https://github.com/nim-lang/Nim/pull/19919
2022-08-10 13:28:52 +02:00
Mark Spanbroek
13c7ff7671 Retrieve optional error from Result 2022-08-04 13:52:07 +02:00
Tanguy
b0666ba4f1
Fixes for styleCheck:usages (#16)
* Fixes for styleCheck:usages
* Bump nim 1.2 in CI
2022-08-03 15:21:00 +02:00
Ivan Yonchovski
955597a4fd
Add setup/lock files (#14) 2022-07-12 21:34:52 +03:00
Mark Spanbroek
0f9b12abc6 version 0.10.4 2022-07-04 09:59:59 +02:00
Mark Spanbroek
d9333a8f03 Remove obsolete ASDF tool version 2022-07-04 09:56:56 +02:00
Mark Spanbroek
0895a9c065 Fix: without statement with error works in generic code 2022-07-04 09:53:02 +02:00
Mark Spanbroek
d7e9f0bf7f Version 0.10.3 2022-04-04 09:56:53 +02:00
Nickolay Bukreyev
361948499b Fix deprecation warning when importing errorban inside the library
`errorban` module [has been deprecated][deprecation] since v0.5.0.
The library continued to use it internally, but because of that, it
was triggering its own deprecation warning. This commit splits
`errorban.nim` into two pieces: the warning and actual code.

[deprecation]: https://github.com/status-im/questionable/commit/e66cd2439b
2022-04-04 09:52:01 +02:00
Mark Spanbroek
6018fd43e0 version 0.10.2 2022-01-12 17:57:03 +01:00
Mark Spanbroek
a748d22350 Fix: ensure that overridden =? operator does not remain in scope 2022-01-12 17:54:24 +01:00
Mark Spanbroek
ef29000f94 Without statement for Results provides access to errors 2022-01-12 17:54:24 +01:00
Mark Spanbroek
91a38040ea Add license 2022-01-10 11:04:48 +01:00
Mark Spanbroek
7b209af6fb version 0.10.1 2021-12-04 17:56:14 +01:00
Mark Spanbroek
92641b552d Switch warningAsError only works for Nim >= 1.4.0 2021-12-04 17:56:14 +01:00
Mark Spanbroek
cfe4c6fc95 Ensure that =? works with types that do not have a default value 2021-12-04 17:26:08 +01:00
Mark Spanbroek
d7a757a8cb Switch to Nim 1.6.0 for development 2021-11-01 17:18:49 +01:00
Mark Spanbroek
0fe40d2347 Run CI on Nim 1.2, 1.4 and 1.6 2021-11-01 17:18:35 +01:00
Mark Spanbroek
56a4bca641 Fix typo 2021-08-30 18:13:43 +02:00
Mark Spanbroek
4967084d22 Better errors when using a proc without a return type in a .? chain 2021-08-30 15:58:29 +02:00
Mark Spanbroek
b9a090d001 Ensure that test modules use local nimbledeps folder
Even when NIMBLE_DIR is set externally.
2021-07-12 11:26:35 +02:00
Mark Spanbroek
8feb684574 Fix "unused variable" warnings 2021-07-07 09:19:44 +02:00
Mark Spanbroek
4a1783c472 Add documentation comments 2021-06-04 17:34:48 +02:00
Mark Spanbroek
a2ded4f01a Simplify 2021-06-04 16:38:44 +02:00
Mark Spanbroek
986716511f version 0.10.0 2021-05-31 16:26:06 +02:00
Mark Spanbroek
3f18970931 Nim 1.4.8 2021-05-31 16:24:30 +02:00
Mark Spanbroek
93f5c919fb Ensure that optional binding works with closures 2021-05-31 16:24:21 +02:00
37 changed files with 1369 additions and 390 deletions

View File

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

2
.gitignore vendored
View File

@ -2,3 +2,5 @@
!*/
!*.*
nimbledeps
nimble.develop
nimble.paths

View File

@ -1 +0,0 @@
nim 1.4.6

5
License.md Normal file
View File

@ -0,0 +1,5 @@
Licensed and distributed under either of
[MIT license](http://opensource.org/licenses/MIT) or
[Apache License, Version 2.0](http://www.apache.org/licenses/LICENSE-2.0)
at your option. These files may not be copied, modified, or distributed except
according to those terms.

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:
```nim
requires "questionable >= 0.9.1 & < 0.10.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
[result][2] package, or the [stew][4] package:
```nim
requires "result" # either this
requires "results" # either this
requires "stew" # or this
```
@ -63,7 +63,7 @@ else:
# this is reached, and y is not defined
```
The `without` statement can be used to place guards that ensure that an optional
The `without` statement can be used to place guards that ensure that an Option
contains a value:
```nim
@ -151,7 +151,7 @@ have to explicitly import the `questionable/results` module:
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
CatchableError]`.
@ -194,6 +194,20 @@ let value = fails() |? @[]
let sum = works()[3] + 40
```
### Without statement
The `without` statement can also be used with Results. It provides access to any
errors that may arise:
```nim
proc someProc(r: ?!int) =
without value =? r, error:
# use `error` to get the error from r
return
# use value
```
### Catching errors
When you want to use Results, but need to call a proc that may raise an
@ -212,6 +226,7 @@ Any Result can be converted to an Option:
```nim
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

16
config.nims Normal file
View File

@ -0,0 +1,16 @@
# Style Check
if (NimMajor, NimMinor, NimPatch) >= (1, 6, 6):
--styleCheck:usages
if (NimMajor, NimMinor) < (1, 6):
--styleCheck:hint
else:
--styleCheck:error
# Disable some warnings
if (NimMajor, NimMinor) >= (1, 6):
switch("warning", "DotLikeOps:off")
# begin Nimble config (version 1)
when fileExists("nimble.paths"):
include "nimble.paths"
# end Nimble config

4
nimble.lock Normal file
View File

@ -0,0 +1,4 @@
{
"version": 1,
"packages": {}
}

View File

@ -1,3 +1,4 @@
import ./questionable/dotlike
import ./questionable/options
export options

View File

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

View File

@ -1,25 +1,89 @@
import std/options
import std/macros
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
template bindLet(name, expression): bool =
let option = expression.option
template name: auto {.used.} = option.unsafeGet()
template toOption[T: SomePointer](value: T): Option[T] =
value.option
proc placeholder(T: type): T =
discard
template bindLet(name, expression): untyped =
let evaluated = expression
let option = evaluated.toOption
type T = typeof(option.unsafeGet())
let name {.used.} = if option.isSome:
option.unsafeGet()
else:
bindFailed(evaluated)
placeholder(T)
option.isSome
template bindVar(name, expression): bool =
let option = expression.option
var name : typeof(option.unsafeGet())
if option.isSome:
name = option.unsafeGet()
template bindVar(name, expression): untyped =
let evaluated = expression
let option = evaluated.toOption
type T = typeof(option.unsafeGet())
var name {.used.} = if option.isSome:
option.unsafeGet()
else:
bindFailed(evaluated)
placeholder(T)
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 =
name.expectKind({nnkIdent, nnkVarTy})
## 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
## instance in an `if` statement.
when (NimMajor, NimMinor) < (1, 6):
name.expectKind({nnkIdent, nnkVarTy, nnkTupleConstr, nnkPar})
else:
name.expectKind({nnkIdent, nnkVarTy, nnkTupleConstr})
if name.kind == nnkIdent:
quote do: bindLet(`name`, `expression`)
elif name.kind == nnkTupleConstr or name.kind == nnkPar:
quote do: bindTuple(`name`, `expression`)
else:
let name = name[0]
quote do: bindVar(`name`, `expression`)

View File

@ -1,5 +1,6 @@
import std/options
import std/macros
import std/strformat
func isSym(node: NimNode): bool =
node.kind in {nnkSym, nnkOpenSymChoice, nnkClosedSymChoice}
@ -7,31 +8,41 @@ func isSym(node: NimNode): bool =
func expectSym(node: NimNode) =
node.expectKind({nnkSym, nnkOpenSymChoice, nnkClosedSymChoice})
template `.?`*(option: typed, identifier: untyped{nkIdent}): untyped =
macro expectReturnType(identifier: untyped, expression: untyped): untyped =
let message =
fmt"'{identifier}' doesn't have a return type, it can't be in a .? chain"
quote do:
when compiles(`expression`) and not compiles(typeof `expression`):
{.error: `message`.}
template chain(option: typed, identifier: untyped{nkIdent}): untyped =
# chain is of shape: option.?identifier
when not compiles(typeof(option.unsafeGet.identifier)):
{.error: ".? chain cannot return void".}
expectReturnType(identifier, option.unsafeGet.identifier)
option ->? option.unsafeGet.identifier
macro `.?`*(option: typed, infix: untyped{nkInfix}): untyped =
macro chain(option: typed, infix: untyped{nkInfix}): untyped =
# chain is of shape: option.?left `operator` right
let infix = infix.copyNimTree()
let left = infix[1]
infix[1] = quote do: `option`.?`left`
infix
macro `.?`*(option: typed, bracket: untyped{nkBracketExpr}): untyped =
macro chain(option: typed, bracket: untyped{nkBracketExpr}): untyped =
# chain is of shape: option.?left[right]
let bracket = bracket.copyNimTree()
let left = bracket[0]
bracket[0] = quote do: `option`.?`left`
bracket
macro `.?`*(option: typed, dot: untyped{nkDotExpr}): untyped =
macro chain(option: typed, dot: untyped{nkDotExpr}): untyped =
# chain is of shape: option.?left.right
let dot = dot.copyNimTree()
let left = dot[0]
dot[0] = quote do: `option`.?`left`
dot
macro `.?`*(option: typed, call: untyped{nkCall}): untyped =
macro chain(option: typed, call: untyped{nkCall}): untyped =
let call = call.copyNimTree()
let procedure = call[0]
if call.len == 1:
# chain is of shape: option.?procedure()
@ -50,9 +61,19 @@ macro `.?`*(option: typed, call: untyped{nkCall}): untyped =
else:
# chain is of shape: option.?procedure(arguments)
call.insert(1, quote do: `option`.unsafeGet)
quote do: `option` ->? `call`
quote do:
expectReturnType(`procedure`, `call`)
`option` ->? `call`
macro `.?`*(option: typed, symbol: untyped): untyped =
macro chain(option: typed, symbol: untyped): untyped =
symbol.expectSym()
let expression = ident($symbol)
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)

2
questionable/dotlike.nim Normal file
View File

@ -0,0 +1,2 @@
when defined(nimPreviewDotLikeOps):
{.error: "DotLikeOps are not supported by questionable".}

View File

@ -1,10 +1,3 @@
{.warning: "errorban is deprecated; use nimble package `upraises` instead".}
## Include this file to indicate that your module does not raise Errors.
## Disables compiler hints about unused declarations in Nim < 1.4.0
when (NimMajor, NimMinor, NimPatch) >= (1, 4, 0):
{.push raises:[].}
else:
{.push raises: [Defect].}
{.hint[XDeclaredButNotUsed]: off.}
include ./private/errorban

View File

@ -1,11 +1,32 @@
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 =
# chain is of shape: expression.?[index]
let index = brackets[0]
quote do:
type T = typeof(`expression`[`index`])
try:
`expression`[`index`].some
except KeyError:
T.none
block:
type T = typeof(`expression`[`index`])
try:
`expression`[`index`].some
except KeyError:
T.none

View File

@ -1,12 +1,19 @@
template liftUnary*(T: type, operator: 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 `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 =
a ->? `operator`(a.unsafeGet(), b)
block:
let evalA = a
evalA ->? `operator`(evalA.unsafeGet(), b)

View File

@ -6,7 +6,7 @@ import ./indexing
import ./operators
import ./without
include ./errorban
include ./private/errorban
export options except get
export binding
@ -15,16 +15,16 @@ export indexing
export without
template `?`*(T: typed): type Option[T] =
## Use `?` to make a type optional. For example the type `?int` is short for
## `Option[int]`.
Option[T]
template `!`*[T](option: ?T): T =
option.get
## Returns the value of an Option when you're absolutely sure that it
## contains value. Using `!` on an Option without a value raises a Defect.
template `->?`*[T,U](option: ?T, expression: U): ?U =
if option.isSome:
expression.some
else:
U.none
option.get
template `->?`*[T,U](option: ?T, expression: ?U): ?U =
if option.isSome:
@ -32,11 +32,8 @@ template `->?`*[T,U](option: ?T, expression: ?U): ?U =
else:
U.none
template `->?`*[T,U,V](options: (?T, ?U), expression: V): ?V =
if options[0].isSome and options[1].isSome:
expression.some
else:
V.none
template `->?`*[T,U](option: ?T, expression: U): ?U =
option ->? expression.some
template `->?`*[T,U,V](options: (?T, ?U), expression: ?V): ?V =
if options[0].isSome and options[1].isSome:
@ -44,7 +41,13 @@ template `->?`*[T,U,V](options: (?T, ?U), expression: ?V): ?V =
else:
V.none
template `|?`*[T](option: ?T, fallback: T): T =
template `->?`*[T,U,V](options: (?T, ?U), expression: V): ?V =
options ->? expression.some
proc `|?`*[T](option: ?T, fallback: T): T =
## Use the `|?` operator to supply a fallback value when an Option does not
## hold a value.
if option.isSome:
option.unsafeGet()
else:
@ -53,11 +56,13 @@ template `|?`*[T](option: ?T, fallback: T): T =
macro `.?`*[T](option: ?T, brackets: untyped{nkBracket}): untyped =
let index = brackets[0]
quote do:
type U = typeof(`option`.unsafeGet().?[`index`].unsafeGet())
if `option`.isSome:
`option`.unsafeGet().?[`index`]
else:
U.none
block:
let evaluated = `option`
type U = typeof(evaluated.unsafeGet().?[`index`].unsafeGet())
if evaluated.isSome:
evaluated.unsafeGet().?[`index`]
else:
U.none
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

@ -0,0 +1,54 @@
import std/options
import std/macros
# A stack of names of error variables. Keeps track of the error variables that
# are given to captureBindError().
var errorVariableNames {.global, compileTime.}: seq[string]
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.
# name of the error variable as a string literal
let errorVariableName = newLit($error)
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`
func unsafeCatchableError[T](_: Option[T]): ref CatchableError =
newException(ValueError, "Option is set to `none`")
func unsafeCatchableError[T](_: ref T): ref CatchableError =
newException(ValueError, "ref is nil")
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

@ -0,0 +1,8 @@
## Include this file to indicate that your module does not raise Errors.
## Disables compiler hints about unused declarations in Nim < 1.4.0
when (NimMajor, NimMinor, NimPatch) >= (1, 4, 0):
{.push raises:[].}
else:
{.push raises: [Defect].}
{.hint[XDeclaredButNotUsed]: off.}

View File

@ -6,52 +6,79 @@ import ./chaining
import ./indexing
import ./operators
import ./without
import ./withoutresult
import ./private/bareexcept
include ./errorban
include ./private/errorban
export resultsbase except ok, err, isOk, isErr, get
export binding
export chaining
export indexing
export without
export withoutresult
type ResultFailure* = object of CatchableError
template `?!`*(T: typed): type Result[T, ref CatchableError] =
## 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 CatchableError]`.
Result[T, ref CatchableError]
template `!`*[T](value: ?!T): T =
## Returns the value of a Result when you're absolutely sure that it
## contains value. Using `!` on a Result without a value raises a Defect.
value.get
proc success*[T](value: T): ?!T =
## Creates a successfull Result containing the value.
##
ok(?!T, value)
proc success*: ?!void =
## Creates a successfull Result without a value.
ok(?!void)
proc failure*(T: type, error: ref CatchableError): ?!T =
## Creates a failed Result containing the error.
err(?!T, error)
proc failure*(T: type, message: string): ?!T =
## Creates a failed Result containing a `ResultFailure` with the specified
## error message.
T.failure newException(ResultFailure, message)
template failure*(error: ref CatchableError): auto =
## Creates a failed Result containing the error.
err error
template failure*(message: string): auto =
## Creates a failed Result containing the error.
failure newException(ResultFailure, message)
proc isSuccess*[T](value: ?!T): bool =
## Returns true when the Result contains a value.
value.isOk
proc isFailure*[T](value: ?!T): bool =
## Returns true when the Result contains an error.
value.isErr
template `->?`*[T,U](value: ?!T, expression: U): ?!U =
if value.isFailure:
U.failure(value.error)
proc `$`*[T](value: ?!T): string =
if value.isSuccess:
"success(" & $(!value) & ")"
else:
expression.success
"failure(\"" & $(value.error.msg) & "\")"
template `->?`*[T,U](value: ?!T, expression: ?!U): ?!U =
if value.isFailure:
@ -59,13 +86,8 @@ template `->?`*[T,U](value: ?!T, expression: ?!U): ?!U =
else:
expression
template `->?`*[T,U,V](values: (?!T, ?!U), expression: V): ?!V =
if values[0].isFailure:
V.failure(values[0].error)
elif values[1].isFailure:
V.failure(values[1].error)
else:
expression.success
template `->?`*[T,U](value: ?!T, expression: U): ?!U =
value ->? expression.success
template `->?`*[T,U,V](values: (?!T, ?!U), expression: ?!V): ?!V =
if values[0].isFailure:
@ -75,14 +97,51 @@ template `->?`*[T,U,V](values: (?!T, ?!U), expression: ?!V): ?!V =
else:
expression
template `|?`*[T,E](value: Result[T,E], fallback: T): T =
template `->?`*[T,U,V](values: (?!T, ?!U), expression: V): ?!V =
values ->? expression.success
proc `|?`*[T,E](value: Result[T,E], fallback: T): T =
## Use the `|?` operator to supply a fallback value when a Result does not
## hold a value.
value.valueOr(fallback)
proc option*[T,E](value: Result[T,E]): ?T =
## Converts a Result into an Option.
if value.isOk:
value.unsafeGet.some
ignoreBareExceptWarning:
try: # workaround for erroneous exception tracking when T is a closure
return value.unsafeGet.some
except Exception as exception:
raise newException(Defect, exception.msg, exception)
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 =
## Returns an Option that contains the error from the Result, if it has one.
if value.isErr:
value.error.some
else:
E.none
Result.liftUnary(`-`)
Result.liftUnary(`+`)

View File

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

View File

@ -1,4 +1,6 @@
template without*(expression, body) =
## Used to place guards that ensure that an Option or Result contains a value.
let ok = expression
if not ok:
body

View File

@ -0,0 +1,41 @@
import std/macros
import ./without
import ./private/binderror
const symbolKinds = {nnkSym, nnkOpenSymChoice, nnkClosedSymChoice}
const identKinds = {nnkIdent} + symbolKinds
proc undoSymbolResolution(expression, ident: NimNode): NimNode =
## Finds symbols in the expression that match the `ident` and replaces them
## with `ident`, effectively undoing any symbol resolution that happened
## before.
if expression.kind in symbolKinds and eqIdent($expression, $ident):
return ident
let expression = expression.copyNimTree()
for i in 0..<expression.len:
expression[i] = undoSymbolResolution(expression[i], ident)
expression
macro without*(condition, errorname, body: untyped): untyped =
## Used to place guards that ensure that a Result contains 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
# Nim's early symbol resolution might have picked up a symbol with the
# same name as our error variable. We need to undo this to make sure that our
# error variable is seen.
let body = body.undoSymbolResolution(errorIdent)
quote do:
var error {.gensym.}: ref CatchableError
without captureBindError(error, `condition`):
template `errorIdent`: ref CatchableError = error
`body`

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:
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":
proc toString[T](option: ?T): string =
if value =? option:
@ -148,6 +137,178 @@ suite "optionals":
check 42.some.toString == "42"
check int.none.toString == "none"
test "=? works with closures":
var called = false
let closure = some(proc () = called = true)
if a =? none(proc ()):
a()
check not called
if a =? closure:
a()
check called
test "=? works with types that do not have a default value":
type NoDefault {.requiresInit.} = distinct int
proc `==`(a,b: NoDefault): bool {.borrow.}
if a =? some NoDefault(42):
check a == NoDefault(42)
else:
fail()
if var a =? some NoDefault(42):
check a == NoDefault(42)
else:
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":
proc test1 =
without a =? 42.some:
@ -174,6 +335,31 @@ suite "optionals":
check table.?["a"] == 1.some
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":
let table = @{"a": @[41, 42]}.toTable
check table.?["a"].isSome
@ -246,6 +432,58 @@ suite "optionals":
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":
var x: ?int

View File

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

View File

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

View File

@ -1,291 +0,0 @@
import std/unittest
import std/options
import std/sequtils
import std/strutils
import std/sugar
import pkg/questionable/results
suite "result":
let error = newException(CatchableError, "error")
test "?!Type is shorthand for Result[Type, ref CatchableError]":
check (?!int is Result[int, ref CatchableError])
check (?!string is Result[string, ref CatchableError])
check (?!seq[bool] is Result[seq[bool], ref CatchableError])
test "! gets value or raises Defect":
check !42.success == 42
expect Defect: discard !int.failure error
test ".? can be used for chaining results":
let a: ?!seq[int] = @[41, 42].success
let b: ?!seq[int] = seq[int].failure error
check a.?len == 2.success
check b.?len == int.failure error
check a.?len.?uint8 == 2'u8.success
check b.?len.?uint8 == uint8.failure error
check a.?len() == 2.success
check b.?len() == int.failure error
check a.?distribute(2).?len() == 2.success
check b.?distribute(2).?len() == int.failure error
test ".? chain can be followed by . calls and operators":
let a = @[41, 42].success
check (a.?len.unsafeGet == 2)
check (a.?len.unsafeGet.uint8.uint64 == 2'u64)
check (a.?len.unsafeGet() == 2)
check (a.?len.unsafeGet().uint8.uint64 == 2'u64)
check (a.?deduplicate()[0].?uint8.?uint64 == 41'u64.success)
check (a.?len + 1 == 3.success)
check (a.?deduplicate()[0] + 1 == 42.success)
check (a.?deduplicate.map(x => x) == @[41, 42].success)
test ".? chains work in generic code":
proc test[T](a: ?!T) =
check (a.?len == 2.success)
check (a.?len.?uint8 == 2'u8.success)
check (a.?len() == 2.success)
check (a.?distribute(2).?len() == 2.success)
check (a.?len.unsafeGet == 2)
check (a.?len.unsafeGet.uint8.uint64 == 2'u64)
check (a.?len.unsafeGet() == 2)
check (a.?len.unsafeGet().uint8.uint64 == 2'u64)
check (a.?deduplicate()[0].?uint8.?uint64 == 41'u64.success)
check (a.?len + 1 == 3.success)
check (a.?deduplicate()[0] + 1 == 42.success)
check (a.?deduplicate.map(x => x) == @[41, 42].success)
test @[41, 42].success
test "[] can be used for indexing results":
let a: ?!seq[int] = @[1, 2, 3].success
let b: ?!seq[int] = seq[int].failure error
check a[1] == 2.success
check a[^1] == 3.success
check a[0..1] == @[1, 2].success
check b[1] == int.failure error
test "|? can be used to specify a fallback value":
check 42.success |? 40 == 42
check int.failure(error) |? 42 == 42
test "=? can be used for optional binding":
if a =? int.failure(error):
fail
if b =? 42.success:
check b == 42
else:
fail
while a =? 42.success:
check a == 42
break
while a =? int.failure(error):
fail
break
test "=? can appear multiple times in conditional expression":
if a =? 42.success and b =? "foo".success:
check a == 42
check b == "foo"
else:
fail
test "=? works with variable hiding":
let a = 42.success
if a =? a:
check a == 42
test "=? works with var":
if var a =? 1.success and var b =? 2.success:
check a == 1
inc a
check a == b
inc b
check b == 3
else:
fail
if var a =? int.failure(error):
fail
test "=? works with .?":
if a =? 42.success.?uint8:
check a == 42.uint8
else:
fail
test "=? evaluates optional expression 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 "=? works in generic code":
proc toString[T](res: ?!T): string =
if value =? res:
$value
else:
"error"
check 42.success.toString == "42"
check int.failure(error).toString == "error"
test "=? works in generic code with variable hiding":
let value {.used.} = "ignored"
proc toString[T](res: ?!T): string =
if value =? res:
$value
else:
"error"
check 42.success.toString == "42"
check int.failure(error).toString == "error"
test "without statement works for results":
proc test1 =
without a =? 42.success:
fail
return
check a == 42
proc test2 =
without a =? int.failure "error":
return
fail
test1()
test2()
test "catch can be used to convert exceptions to results":
check parseInt("42").catch == 42.success
check parseInt("foo").catch.error of ValueError
test "success can be called without argument":
check (success() is ?!void)
test "failure can be called with string argument":
let value = int.failure("some failure")
check value.error of ResultFailure
check value.error.msg == "some failure"
test "unary operator `-` works for results":
check -(-42.success) == 42.success
check -(int.failure(error)) == int.failure(error)
test "other unary operators work for results":
check +(42.success) == 42.success
check @([1, 2].success) == (@[1, 2]).success
test "binary operator `+` works for results":
check 40.success + 2.success == 42.success
check 40.success + 2 == 42.success
check int.failure(error) + 2 == int.failure(error)
check 40.success + int.failure(error) == int.failure(error)
check int.failure(error) + int.failure(error) == int.failure(error)
test "other binary operators work for results":
check (21.success * 2 == 42.success)
check (84'f.success / 2'f == 42'f.success)
check (84.success div 2 == 42.success)
check (85.success mod 43 == 42.success)
check (0b00110011.success shl 1 == 0b01100110.success)
check (0b00110011.success shr 1 == 0b00011001.success)
check (44.success - 2 == 42.success)
check ("f".success & "oo" == "foo".success)
check (40.success <= 42 == true.success)
check (40.success < 42 == true.success)
check (40.success >= 42 == false.success)
check (40.success > 42 == false.success)
test "Result can be converted to Option":
check 42.success.option == 42.some
check int.failure(error).option == int.none
test "failure can be used without type parameter in procs":
proc fails: ?!int =
failure "some error"
check fails().isFailure
check fails().error.msg == "some error"
test ".? avoids wrapping result in result":
let a = 41.success
proc b(x: int): ?!int =
success x + 1
check a.?b == 42.success
test "lifted operators avoid wrapping result in result":
let a = 40.success
let b = 2.success
func `&`(x, y: int): ?!int =
success x + y
check (a & b) == 42.success
test "examples from readme work":
proc works: ?!seq[int] =
success @[1, 1, 2, 2, 2]
proc fails: ?!seq[int] =
failure "something went wrong"
# binding:
if x =? works():
check x == @[1, 1, 2, 2, 2]
else:
fail
# chaining:
let amount = works().?deduplicate.?len
check (amount == 2.success)
# fallback values:
let value = fails() |? @[]
check (value == newSeq[int](0))
# lifted operators:
let sum = works()[3] + 40
check (sum == 42.success)
# catch
let x = parseInt("42").catch
check (x == 42.success)
let y = parseInt("XX").catch
check y.isFailure
# Conversion to Option
let converted = works().option
check (converted == @[1, 1, 2, 2, 2].some)
import pkg/questionable/resultsbase
suite "result compatibility":
test "|?, =? and .option work on other types of Result":
type R = Result[int, string]
let good = R.ok 42
let bad = R.err "error"
check bad |? 43 == 43
if value =? good:
check value == 42
else:
fail
check good.option == 42.some

View File

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

View File

@ -0,0 +1,693 @@
import std/unittest
import std/options
import std/sequtils
import std/strutils
import std/sugar
import std/threadpool
import pkg/questionable/results
{.experimental: "parallel".}
suite "result":
let error = newException(CatchableError, "error")
test "?!Type is shorthand for Result[Type, ref CatchableError]":
check (?!int is Result[int, ref CatchableError])
check (?!string is Result[string, 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":
check !42.success == 42
expect Defect: discard !int.failure error
test ".? can be used for chaining results":
let a: ?!seq[int] = @[41, 42].success
let b: ?!seq[int] = seq[int].failure error
check a.?len == 2.success
check b.?len == int.failure error
check a.?len.?uint8 == 2'u8.success
check b.?len.?uint8 == uint8.failure error
check a.?len() == 2.success
check b.?len() == int.failure error
check a.?distribute(2).?len() == 2.success
check b.?distribute(2).?len() == int.failure error
test ".? chain can be followed by . calls and operators":
let a = @[41, 42].success
check (a.?len.unsafeGet == 2)
check (a.?len.unsafeGet.uint8.uint64 == 2'u64)
check (a.?len.unsafeGet() == 2)
check (a.?len.unsafeGet().uint8.uint64 == 2'u64)
check (a.?deduplicate()[0].?uint8.?uint64 == 41'u64.success)
check (a.?len + 1 == 3.success)
check (a.?deduplicate()[0] + 1 == 42.success)
check (a.?deduplicate.map(x => x) == @[41, 42].success)
test ".? chains work in generic code":
proc test[T](a: ?!T) =
check (a.?len == 2.success)
check (a.?len.?uint8 == 2'u8.success)
check (a.?len() == 2.success)
check (a.?distribute(2).?len() == 2.success)
check (a.?len.unsafeGet == 2)
check (a.?len.unsafeGet.uint8.uint64 == 2'u64)
check (a.?len.unsafeGet() == 2)
check (a.?len.unsafeGet().uint8.uint64 == 2'u64)
check (a.?deduplicate()[0].?uint8.?uint64 == 41'u64.success)
check (a.?len + 1 == 3.success)
check (a.?deduplicate()[0] + 1 == 42.success)
check (a.?deduplicate.map(x => x) == @[41, 42].success)
test @[41, 42].success
test "[] can be used for indexing results":
let a: ?!seq[int] = @[1, 2, 3].success
let b: ?!seq[int] = seq[int].failure error
check a[1] == 2.success
check a[^1] == 3.success
check a[0..1] == @[1, 2].success
check b[1] == int.failure error
test "|? can be used to specify a fallback value":
check 42.success |? 40 == 42
check int.failure(error) |? 42 == 42
test "=? can be used for optional binding":
if a =? int.failure(error):
fail
if b =? 42.success:
check b == 42
else:
fail
while a =? 42.success:
check a == 42
break
while a =? int.failure(error):
fail
break
test "=? can appear multiple times in conditional expression":
if a =? 42.success and b =? "foo".success:
check a == 42
check b == "foo"
else:
fail
test "=? works with variable hiding":
let a = 42.success
if a =? a:
check a == 42
test "=? works with var":
if var a =? 1.success and var b =? 2.success:
check a == 1
inc a
check a == b
inc b
check b == 3
else:
fail
if var a =? int.failure(error):
fail
test "=? works with .?":
if a =? 42.success.?uint8:
check a == 42.uint8
else:
fail
test "=? evaluates optional expression 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 "=? works in generic code":
proc toString[T](res: ?!T): string =
if value =? res:
$value
else:
"error"
check 42.success.toString == "42"
check int.failure(error).toString == "error"
test "=? works in generic code with variable hiding":
let value {.used.} = "ignored"
proc toString[T](res: ?!T): string =
if value =? res:
$value
else:
"error"
check 42.success.toString == "42"
check int.failure(error).toString == "error"
test "=? works with closures":
var called = false
let closure = success(proc () = called = true)
if a =? failure(proc (), error):
a()
check not called
if a =? closure:
a()
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":
proc test1 =
without a =? 42.success:
fail
return
check a == 42
proc test2 =
without a =? int.failure "error":
return
fail
test1()
test2()
test "without statement can expose error":
proc test =
without a =? int.failure "some error", error:
check error.msg == "some error"
return
fail
test()
test "without statement only exposes error variable inside block":
proc test =
without a =? 42.success, errorvar:
fail
discard errorvar # fixes warning about unused variable "errorvar"
return
check not compiles errorvar
test()
test "without statements with multiple bindings exposes first error":
proc test1 =
without (a =? int.failure "error 1") and
(b =? int.failure "error 2"),
error:
check error.msg == "error 1"
return
fail
proc test2 =
without (a =? 42.success) and (b =? int.failure "error 2"), error:
check error.msg == "error 2"
return
fail
test1()
test2()
test "without statement with error evaluates result only once":
proc test =
var count = 0
without a =? (inc count; int.failure "error"):
check count == 1
return
fail
test()
test "without statement with error handles options as well":
proc test1 =
without a =? int.none and b =? int.failure "error", error:
check error.msg == "Option is set to `none`"
return
fail
proc test2 =
without a =? 42.some and b =? int.failure "error", error:
check error.msg == "error"
return
fail
test1()
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":
proc test =
without a =? 42.success, error:
discard error
fail
without b =? 42.success, error:
discard error
fail
test()
test "without statement with error works with deeply nested =? operators":
proc test =
let fail1 = int.failure "error 1"
let fail2 = int.failure "error 2"
without (block: a =? (if b =? fail1: b.success else: fail2)), error:
check error.msg == "error 2"
return
fail
test()
test "without statement with error works in generic code":
proc test(_: type) =
without a =? int.failure "error", e:
check e.msg == "error"
return
fail
test(int)
test "without statements with error can be nested":
without a =? int.failure "error1", e1:
without b =? int.failure "error2", e2:
check e1.msg == "error1"
check e2.msg == "error2"
check e1.msg == "error1"
test "without statement works in generic code using existing error name":
let existingName {.used.} = "some variable"
proc shouldCompile(_: type int): ?!int =
without _ =? int.failure "error", existingName:
check existingName.msg == "error"
return success 42
discard int.shouldCompile()
test "without statements with error work in nested calls":
proc bar(): ?!int =
without _ =? int.failure "error", err:
return failure err.msg
proc foo() =
without _ =? bar(), err:
check err.msg == "error"
return
fail()
foo()
test "without statement with error works in nested generic calls":
proc works(_: type int): ?!int =
without _ =? int.failure "error1", error:
check error.msg == "error1"
return success 42
proc fails(_: type int): ?!int =
return failure "error2"
proc foo =
without a =? int.works() and b =? int.fails(), error:
check error.msg == "error2"
return
fail()
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":
check parseInt("42").catch == 42.success
check parseInt("foo").catch.error of ValueError
test "success can be called without argument":
check (success() is ?!void)
test "failure can be called with string argument":
let value = int.failure("some failure")
check value.error of ResultFailure
check value.error.msg == "some failure"
test "unary operator `-` works for results":
check -(-42.success) == 42.success
check -(int.failure(error)) == int.failure(error)
test "other unary operators work for results":
check +(42.success) == 42.success
check @([1, 2].success) == (@[1, 2]).success
test "binary operator `+` works for results":
check 40.success + 2.success == 42.success
check 40.success + 2 == 42.success
check int.failure(error) + 2 == int.failure(error)
check 40.success + int.failure(error) == int.failure(error)
check int.failure(error) + int.failure(error) == int.failure(error)
test "other binary operators work for results":
check (21.success * 2 == 42.success)
check (84'f.success / 2'f == 42'f.success)
check (84.success div 2 == 42.success)
check (85.success mod 43 == 42.success)
check (0b00110011.success shl 1 == 0b01100110.success)
check (0b00110011.success shr 1 == 0b00011001.success)
check (44.success - 2 == 42.success)
check ("f".success & "oo" == "foo".success)
check (40.success <= 42 == true.success)
check (40.success < 42 == true.success)
check (40.success >= 42 == false.success)
check (40.success > 42 == false.success)
test "Result can be converted to Option":
check 42.success.option == 42.some
check int.failure(error).option == int.none
test "Result error can be converted to Option":
check (int.failure(error).errorOption == error.some)
check (42.success.errorOption == (ref CatchableError).none)
check (void.failure(error).errorOption == error.some)
check (success().errorOption == (ref CatchableError).none)
test "failure can be used without type parameter in procs":
proc fails: ?!int =
failure "some error"
check fails().isFailure
check fails().error.msg == "some error"
test ".? avoids wrapping result in result":
let a = 41.success
proc b(x: int): ?!int =
success x + 1
check a.?b == 42.success
test "lifted operators avoid wrapping result in result":
let a = 40.success
let b = 2.success
func `&`(x, y: int): ?!int =
success x + y
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":
proc works: ?!seq[int] =
success @[1, 1, 2, 2, 2]
proc fails: ?!seq[int] =
failure "something went wrong"
# binding:
if x =? works():
check x == @[1, 1, 2, 2, 2]
else:
fail
# chaining:
let amount = works().?deduplicate.?len
check (amount == 2.success)
# fallback values:
let value = fails() |? @[]
check (value == newSeq[int](0))
# lifted operators:
let sum = works()[3] + 40
check (sum == 42.success)
# catch
let x = parseInt("42").catch
check (x == 42.success)
let y = parseInt("XX").catch
check y.isFailure
# Conversion to Option
let converted = works().option
check (converted == @[1, 1, 2, 2, 2].some)
# Without statement
proc someProc(r: ?!int) =
without value =? r, error:
check error.msg == "some error"
return
check value == 42
someProc(42.success)
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
suite "result compatibility":
type R = Result[int, string]
let good = R.ok 42
let bad = R.err "some error"
test "|?, =? and .option work on other types of Result":
check bad |? 43 == 43
if value =? good:
check value == 42
else:
fail
check good.option == 42.some
test "=? works on other type of Result after without statement with error":
without a =? 42.success, error:
discard error # fixes warning about unused variable "error"
fail
without b =? good:
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"
license = "MIT"
requires "result"
requires "results"
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"
license = "MIT"
requires "stew"
when (NimMajor, NimMinor) >= (1, 6):
requires "stew"
task test, "Runs the test suite":
exec "nim c -f -r test.nim"
task test, "Runs the test suite":
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"