diff options
-rw-r--r-- | changelog.md | 4 | ||||
-rw-r--r-- | lib/std/wrapnils.nim | 99 | ||||
-rw-r--r-- | tests/stdlib/twrapnils.nim | 37 |
3 files changed, 73 insertions, 67 deletions
diff --git a/changelog.md b/changelog.md index dd92c32ee..8c6d2b3c4 100644 --- a/changelog.md +++ b/changelog.md @@ -149,9 +149,11 @@ provided by the operating system. - Added experimental `linenoise.readLineStatus` to get line and status (e.g. ctrl-D or ctrl-C). - Added `compilesettings.SingleValueSetting.libPath` + - `std/wrapnils` doesn't use `experimental:dotOperators` anymore, avoiding issues like https://github.com/nim-lang/Nim/issues/13063 (which affected error messages) for modules importing `std/wrapnils`. + Added `??.` macro which returns an `Option`. - Added `math.frexp` overload procs. Deprecated `c_frexp`, use `frexp` instead. @@ -174,6 +176,8 @@ provided by the operating system. - Added `strip` and `setSlice` to `std/strbasics`. +- Added to `wrapnils` an option-like API via `??.`, `isSome`, `get`. + ## Language changes - `nimscript` now handles `except Exception as e`. diff --git a/lib/std/wrapnils.nim b/lib/std/wrapnils.nim index 7c0672176..a469b736d 100644 --- a/lib/std/wrapnils.nim +++ b/lib/std/wrapnils.nim @@ -1,6 +1,6 @@ ## This module allows chains of field-access and indexing where the LHS can be nil. ## This simplifies code by reducing need for if-else branches around intermediate values -## that maybe be nil. +## that may be nil. ## ## Note: experimental module, unstable API. @@ -23,89 +23,88 @@ runnableExamples: assert ?.(f2.x2.x2).x3[] == 0 assert (?.f2.x2.x2).x3 == nil # this terminates ?. early - import segfaults # enable `NilAccessDefect` exceptions + import std/segfaults # enable `NilAccessDefect` exceptions doAssertRaises(NilAccessDefect): echo (?.f2.x2.x2).x3[] -type Wrapnil[T] = object - valueImpl: T - validImpl: bool +from std/options import Option, isSome, get, option, unsafeGet, UnpackDefect +export options.get, options.isSome, options.isNone -proc wrapnil[T](a: T): Wrapnil[T] = - ## See top-level example. - Wrapnil[T](valueImpl: a, validImpl: true) - -template unwrap(a: Wrapnil): untyped = - ## See top-level example. - a.valueImpl - -template fakeDot*(a: Wrapnil, b): untyped = +template fakeDot*(a: Option, b): untyped = ## See top-level example. let a1 = a # to avoid double evaluations - let a2 = a1.valueImpl - type T = Wrapnil[typeof(a2.b)] - if a1.validImpl: + type T = Option[typeof(unsafeGet(a1).b)] + if isSome(a1): + let a2 = unsafeGet(a1) when typeof(a2) is ref|ptr: if a2 == nil: default(T) else: - wrapnil(a2.b) + option(a2.b) else: - wrapnil(a2.b) + option(a2.b) else: # nil is "sticky"; this is needed, see tests default(T) -proc isValid(a: Wrapnil): bool = - ## Returns true if `a` didn't contain intermediate `nil` values (note that - ## `a.valueImpl` itself can be nil even in that case) - a.validImpl +# xxx this should but doesn't work: func `[]`*[T, I](a: Option[T], i: I): Option {.inline.} = -template `[]`*[I](a: Wrapnil, i: I): untyped = +func `[]`*[T, I](a: Option[T], i: I): auto {.inline.} = ## See top-level example. - let a1 = a # to avoid double evaluations - if a1.validImpl: + if isSome(a): # correctly will raise IndexDefect if a is valid but wraps an empty container - wrapnil(a1.valueImpl[i]) - else: - default(Wrapnil[typeof(a1.valueImpl[i])]) + result = option(a.unsafeGet[i]) -template `[]`*(a: Wrapnil): untyped = +func `[]`*[U](a: Option[U]): auto {.inline.} = ## See top-level example. - let a1 = a # to avoid double evaluations - let a2 = a1.valueImpl - type T = Wrapnil[typeof(a2[])] - if a1.validImpl: - if a2 == nil: - default(T) - else: - wrapnil(a2[]) - else: - default(T) + if isSome(a): + let a2 = a.unsafeGet + if a2 != nil: + result = option(a2[]) import std/macros -proc replace(n: NimNode): NimNode = +func replace(n: NimNode): NimNode = if n.kind == nnkDotExpr: result = newCall(bindSym"fakeDot", replace(n[0]), n[1]) elif n.kind == nnkPar: doAssert n.len == 1 - result = newCall(bindSym"wrapnil", n[0]) + result = newCall(bindSym"option", n[0]) elif n.kind in {nnkCall, nnkObjConstr}: - result = newCall(bindSym"wrapnil", n) + result = newCall(bindSym"option", n) elif n.len == 0: - result = newCall(bindSym"wrapnil", n) + result = newCall(bindSym"option", n) else: n[0] = replace(n[0]) result = n -macro `?.`*(a: untyped): untyped = +proc safeGet[T](a: Option[T]): T {.inline.} = + get(a, default(T)) + +macro `?.`*(a: untyped): auto = ## Transforms `a` into an expression that can be safely evaluated even in ## presence of intermediate nil pointers/references, in which case a default ## value is produced. - #[ - Using a template like this wouldn't work: - template `?.`*(a: untyped): untyped = wrapnil(a)[] - ]# result = replace(a) result = quote do: - `result`.valueImpl + # `result`.val # TODO: expose a way to do this directly in std/options, e.g.: `getAsIs` + safeGet(`result`) + +macro `??.`*(a: untyped): Option = + ## Same as `?.` but returns an `Option`. + runnableExamples: + type Foo = ref object + x1: ref int + x2: int + # `?.` can't distinguish between a valid vs invalid default value, but `??.` can: + var f1 = Foo(x1: int.new, x2: 2) + doAssert (??.f1.x1[]).get == 0 # not enough to tell when the chain was valid. + doAssert (??.f1.x1[]).isSome # a nil didn't occur in the chain + doAssert (??.f1.x2).get == 2 + + var f2: Foo + doAssert not (??.f2.x1[]).isSome # f2 was nil + from std/options import UnpackDefect + doAssertRaises(UnpackDefect): discard (??.f2.x1[]).get + doAssert ?.f2.x1[] == 0 # in contrast, this returns default(int) + + result = replace(a) diff --git a/tests/stdlib/twrapnils.nim b/tests/stdlib/twrapnils.nim index c56a65d11..af0978762 100644 --- a/tests/stdlib/twrapnils.nim +++ b/tests/stdlib/twrapnils.nim @@ -1,15 +1,11 @@ import std/wrapnils -const wrapnilExtendedExports = declared(wrapnil) - # for now, wrapnil, isValid, unwrap are not exported - proc checkNotZero(x: float): float = doAssert x != 0 x -var witness = 0 - proc main() = + var witness = 0 type Bar = object b1: int b2: ptr string @@ -31,8 +27,12 @@ proc main() = proc fun(a: Bar): auto = a.b2 var a: Foo - var x6 = create(Bar) - x6.b1 = 42 + + var x6: ptr Bar + when nimvm: discard # pending https://github.com/timotheecour/Nim/issues/568 + else: + x6 = create(Bar) + x6.b1 = 42 var a2 = Foo(x1: 1.0, x5: @[10, 11], x6: x6) var a3 = Foo(x1: 1.2, x3: "abc") a3.x2 = a3 @@ -50,26 +50,23 @@ proc main() = doAssert ?.a3.x2.x2.x5.len == 0 doAssert a3.x2.x2.x3.len == 3 - when wrapnilExtendedExports: - # example calling wrapnil directly, with and without unwrap - doAssert a3.wrapnil.x2.x2.x3.len == wrapnil(3) - doAssert a3.wrapnil.x2.x2.x3.len.unwrap == 3 - doAssert a2.wrapnil.x4.isValid - doAssert not a.wrapnil.x4.isValid - doAssert ?.a.x2.x2.x3[1] == default(char) # here we only apply wrapnil around gook.foo, not gook (and assume gook is not nil) doAssert ?.(gook.foo).x2.x2.x1 == 0.0 - doAssert ?.a2.x6[] == Bar(b1: 42) # deref for ptr Bar + when nimvm: discard + else: + doAssert ?.a2.x6[] == Bar(b1: 42) # deref for ptr Bar doAssert ?.a2.x1.checkNotZero == 1.0 doAssert a == nil # shows that checkNotZero won't be called if a nil is found earlier in chain doAssert ?.a.x1.checkNotZero == 0.0 - # checks that a chain without nil but with an empty seq still throws IndexDefect - doAssertRaises(IndexDefect): discard ?.a2.x8[3] + when nimvm: discard + else: + # checks that a chain without nil but with an empty seq still raises + doAssertRaises(IndexDefect): discard ?.a2.x8[3] # make sure no double evaluation bug doAssert witness == 0 @@ -79,4 +76,10 @@ proc main() = # here, it's used twice, to deref `ref Bar` and then `ptr string` doAssert ?.a.x9[].fun[] == "" + block: # `??.` + doAssert (??.a3.x2.x2.x3.len).get == 3 + doAssert (??.a2.x4).isSome + doAssert not (??.a.x4).isSome + main() +static: main() |