1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193
|
## This module allows evaluating expressions safely against the following conditions:
## * nil dereferences
## * field accesses with incorrect discriminant in case objects
##
## `default(T)` is returned in those cases when evaluating an expression of type `T`.
## This simplifies code by reducing need for if-else branches.
##
## Note: experimental module, unstable API.
#[
TODO:
consider handling indexing operations, eg:
doAssert ?.default(seq[int])[3] == default(int)
]#
import std/macros
runnableExamples:
type Foo = ref object
x1: string
x2: Foo
x3: ref int
var f: Foo
assert ?.f.x2.x1 == "" # returns default value since `f` is nil
var f2 = Foo(x1: "a")
f2.x2 = f2
assert ?.f2.x1 == "a" # same as f2.x1 (no nil LHS in this chain)
assert ?.Foo(x1: "a").x1 == "a" # can use constructor inside
# when you know a sub-expression doesn't involve a `nil` (e.g. `f2.x2.x2`),
# you can scope it as follows:
assert ?.(f2.x2.x2).x3[] == 0
assert (?.f2.x2.x2).x3 == nil # this terminates ?. early
runnableExamples:
# ?. also allows case object
type B = object
b0: int
case cond: bool
of false: discard
of true:
b1: float
var b = B(cond: false, b0: 3)
doAssertRaises(FieldDefect): discard b.b1 # wrong discriminant
doAssert ?.b.b1 == 0.0 # safe
b = B(cond: true, b1: 4.5)
doAssert ?.b.b1 == 4.5
# lvalue semantics are preserved:
if (let p = ?.b.b1.addr; p != nil): p[] = 4.7
doAssert b.b1 == 4.7
proc finalize(n: NimNode, lhs: NimNode, level: int): NimNode =
if level == 0:
result = quote: `lhs` = `n`
else:
result = quote: (let `lhs` = `n`)
proc process(n: NimNode, lhs: NimNode, label: NimNode, level: int): NimNode =
var n = n.copyNimTree
var it = n
let addr2 = bindSym"addr"
var old: tuple[n: NimNode, index: int]
while true:
if it.len == 0:
result = finalize(n, lhs, level)
break
elif it.kind == nnkCheckedFieldExpr:
let dot = it[0]
let obj = dot[0]
let objRef = quote do: `addr2`(`obj`)
# avoids a copy and preserves lvalue semantics, see tests
let check = it[1]
let okSet = check[1]
let kind1 = check[2]
let tmp = genSym(nskLet, "tmpCase")
let body = process(objRef, tmp, label, level + 1)
let tmp3 = nnkDerefExpr.newTree(tmp)
it[0][0] = tmp3
let dot2 = nnkDotExpr.newTree(@[tmp, dot[1]])
if old.n != nil: old.n[old.index] = dot2
else: n = dot2
let assgn = finalize(n, lhs, level)
result = quote do:
`body`
if `tmp3`.`kind1` notin `okSet`: break `label`
`assgn`
break
elif it.kind in {nnkHiddenDeref, nnkDerefExpr}:
let tmp = genSym(nskLet, "tmp")
let body = process(it[0], tmp, label, level + 1)
it[0] = tmp
let assgn = finalize(n, lhs, level)
result = quote do:
`body`
if `tmp` == nil: break `label`
`assgn`
break
elif it.kind == nnkCall: # consider extending to `nnkCallKinds`
# `copyNimTree` needed to avoid `typ = nil` issues
old = (it, 1)
it = it[1].copyNimTree
else:
old = (it, 0)
it = it[0]
macro `?.`*(a: typed): 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.
let lhs = genSym(nskVar, "lhs")
let label = genSym(nskLabel, "label")
let body = process(a, lhs, label, 0)
result = quote do:
var `lhs`: type(`a`)
block `label`:
`body`
`lhs`
# the code below is not needed for `?.`
from std/options import Option, isSome, get, option, unsafeGet, UnpackDefect
macro `??.`*(a: typed): Option =
## Same as `?.` but returns an `Option`.
runnableExamples:
import std/options
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
doAssertRaises(UnpackDefect): discard (??.f2.x1[]).get
doAssert ?.f2.x1[] == 0 # in contrast, this returns default(int)
let lhs = genSym(nskVar, "lhs")
let lhs2 = genSym(nskVar, "lhs")
let label = genSym(nskLabel, "label")
let body = process(a, lhs2, label, 0)
result = quote do:
var `lhs`: Option[type(`a`)]
block `label`:
var `lhs2`: type(`a`)
`body`
`lhs` = option(`lhs2`)
`lhs`
template fakeDot*(a: Option, b): untyped =
## See top-level example.
let a1 = a # to avoid double evaluations
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:
option(a2.b)
else:
option(a2.b)
else:
# nil is "sticky"; this is needed, see tests
default(T)
# xxx this should but doesn't work: func `[]`*[T, I](a: Option[T], i: I): Option {.inline.} =
func `[]`*[T, I](a: Option[T], i: I): auto {.inline.} =
## See top-level example.
if isSome(a):
# correctly will raise IndexDefect if a is valid but wraps an empty container
result = option(a.unsafeGet[i])
func `[]`*[U](a: Option[U]): auto {.inline.} =
## See top-level example.
if isSome(a):
let a2 = a.unsafeGet
if a2 != nil:
result = option(a2[])
when false:
# xxx: expose a way to do this directly in std/options, e.g.: `getAsIs`
proc safeGet[T](a: Option[T]): T {.inline.} =
get(a, default(T))
|