# SPDX-FileCopyrightText: ☭ Emery Hemingway # SPDX-License-Identifier: Unlicense import std/[options, sets, sequtils, strutils, tables, typetraits] from std/algorithm import sort from std/json import escapeJson, escapeJsonUnquoted import bigints import ./preserves/private/[encoding, decoding, dot, macros, parsing, texts, values] export encoding, decoding, parsing, texts, values when defined(tracePreserves): when defined(posix): template trace(args: varargs[untyped]) = {.cast(noSideEffect).}: stderr.writeLine(args) else: template trace(args: varargs[untyped]) = {.cast(noSideEffect).}: echo(args) else: template trace(args: varargs[untyped]) = discard proc sortDict[E](pr: var Preserve[E]) = sort(pr.dict) do (x, y: DictEntry[E]) -> int: cmp(x.key, y.key) proc cannonicalize*[E](pr: var Preserve[E]) = ## Cannonicalize a compound Preserves value by total ordering. case pr.kind of pkSequence: apply(pr.sequence, cannonicalize) of pkSet: apply(pr.set, cannonicalize) sort(pr.set) of pkDictionary: apply(pr.dict) do (e: var DictEntry[E]): cannonicalize(e.val) sortDict(pr) else: discard proc toInt*(pr: Preserve): Option[int] = case pr.kind of pkRegister: result = some pr.register of pkBigInt: result = toInt[int](pr.bigint) else: discard proc getOrDefault(pr: Preserve; key: Preserve): Preserve = ## Retrieves the value of `pr[key]` if `pr` is a dictionary containing `key` ## or returns the `#f` Preserves value. if pr.kind == pkDictionary: for (k, v) in pr.dict: if key == k: result = v break proc pop*(pr: var Preserve; key: Preserve; val: var Preserve): bool = ## Deletes the `key` from a Preserves dictionary. ## Returns true, if the key existed, and sets `val` to the mapping ## of the key. Otherwise, returns false, and the `val` is unchanged. if pr.kind == pkDictionary: var i = 0 while i < pr.dict.len: if pr.dict[i].key == key: val = move pr.dict[i].val delete(pr.dict, i, i) return true proc `[]`*(pr, key: Preserve): Preserve {.deprecated: "use step instead".} = ## Select a value by `key` from `pr`. ## Works for sequences, records, and dictionaries. case pr.kind of pkDictionary: for (k, v) in pr.dict.items: if k == key: return v raise newException(KeyError, "value not in Preserves dictionary") of pkRecord, pkSequence: let idx = key.toInt if idx.isSome: result = pr[get idx] else: raise newException(ValueError, "invalid Preserves index") else: raise newException(ValueError, "invalid Preserves indexing") func step*(pr, idx: Preserve): Option[Preserve] = ## Step into `pr` by index `idx`. ## Works for sequences, records, and dictionaries. runnableExamples: import std/options assert step(parsePreserves(""""""), 1.toPreserve) == some(2.toPreserve) assert step(parsePreserves("""{ foo: 1 bar: 2}"""), "foo".toSymbol) == some(1.toPreserve) assert step(parsePreserves("""[ ]"""), 1.toPreserve) == none(Preserve[void]) case pr.kind of pkDictionary: for (k, v) in pr.dict.items: if k == idx: result = some(v) break of pkRecord, pkSequence: var i = idx.toInt if i.isSome: var i = get i if i < pr.len: result = some(pr[i]) else: discard func step*(pr: Preserve; path: varargs[Preserve]): Option[Preserve] = ## Step into `pr` by indexes at `path`. result = some(pr) for index in path: if result.isSome: result = step(result.get, index) func step*[E](pr: Preserve[E]; key: Symbol): Option[Preserve[E]] = ## Step into dictionary by a `Symbol` key. if pr.isDictionary: for (k, v) in pr.dict.items: if k.isSymbol and k.symbol == key: result = some(v) break proc mget*(pr: var Preserve; key: Preserve): var Preserve = ## Select a value by `key` from the Preserves dictionary `pr`. if pr.isDictionary: for (k, v) in pr.dict.items: if k == key: return v raise newException(KeyError, "key not in Preserves dictionary") raise newException(ValueError, "not a Preserves dictionary") proc toSymbol*(s: sink string; E = void): Preserve[E] {.inline.} = ## Create a Preserves symbol value. Preserve[E](kind: pkSymbol, symbol: Symbol s) proc initRecord*[E](label: Preserve[E]; arity: Natural = 0): Preserve[E] = ## Create a Preserves record value. result = Preserve[E](kind: pkRecord, record: newSeq[Preserve[E]](arity.succ)) result.record[arity] = label proc initRecord*[E](label: Preserve[E]; args: varargs[Preserve[E]]): Preserve[E] = ## Create a Preserves record value. result = Preserve[E](kind: pkRecord, record: newSeqOfCap[Preserve[E]](1+args.len)) for arg in args: result.record.add(arg) result.record.add(label) proc initRecord*[E](label: string; args: varargs[Preserve[E]]): Preserve[E] {.inline.} = ## Create a Preserves record value. initRecord(toSymbol(label, E), args) proc initSequence*(len: Natural = 0; E = void): Preserve[E] = ## Create a Preserves sequence value. Preserve[E](kind: pkSequence, sequence: newSeq[Preserve[E]](len)) proc initSequenceOfCap*(cap: Natural; E = void): Preserve[E] = ## Create a Preserves sequence value. Preserve[E](kind: pkSequence, sequence: newSeqOfCap[Preserve[E]](cap)) proc initSet*(E = void): Preserve[E] = Preserve[E](kind: pkSet) ## Create a Preserves set value. proc initDictionary*(E = void): Preserve[E] = Preserve[E](kind: pkDictionary) ## Create a Preserves dictionary value. proc toDictionary*[E](pairs: openArray[(string, Preserve[E])]): Preserve[E] = ## Create a Preserves dictionary value. result = Preserve[E](kind: pkDictionary) for (key, val) in pairs: result[toSymbol(key, E)] = val proc embed*[E](pr: sink Preserve[E]): Preserve[E] = ## Create a Preserves value that embeds ``e``. result = pr result.embedded = true proc embed*[E](e: sink E): Preserve[E] = ## Create a Preserves value that embeds ``e``. Preserve[E](kind: pkEmbedded, embed: e) proc len*(pr: Preserve): int = ## Return the shallow count of values in ``pr``, that is the number of ## fields in a record, items in a sequence, items in a set, or key-value pairs ## in a dictionary. case pr.kind of pkRecord: pr.record.len.pred of pkSequence: pr.sequence.len of pkSet: pr.set.len of pkDictionary: pr.dict.len else: 0 iterator items*(pr: Preserve): Preserve = ## Shallow iterator over `pr`, yield the fields in a record, ## the items of a sequence, the items of a set, or the pairs ## of a dictionary. case pr.kind of pkRecord: for i in 0..pr.record.high.pred: yield pr.record[i] of pkSequence: for e in pr.sequence.items: yield e of pkSet: for e in pr.set.items: yield e of pkDictionary: for (k, v) in pr.dict.items: yield k # key can be an arbitrary Preserve yield v else: discard iterator pairs*[E](pr: Preserve[E]): DictEntry[E] = assert(pr.kind == pkDictionary, "not a dictionary") for i in 0..pr.dict.high: yield pr.dict[i] func isBoolean*(pr: Preserve): bool {.inline.} = pr.kind == pkBoolean ## Check if ``pr`` is a Preserve boolean. func isFalse*(pr: Preserve): bool {.inline.} = ## Check if ``pr`` is equivalent to the zero-initialized ``Preserve``. pr.kind == pkBoolean and pr.bool == false func isFloat*(pr: Preserve): bool {.inline.} = pr.kind == pkFloat ## Check if ``pr`` is a Preserve float. func isDouble*(pr: Preserve): bool {.inline.} = pr.kind == pkDouble ## Check if ``pr`` is a Preserve double. func isInteger*(pr: Preserve): bool {.inline.} = ## Check if ``pr`` is a Preserve integer. pr.kind == pkRegister or pr.kind == pkBigInt func isInteger*(pr: Preserve; i: SomeInteger): bool {.inline.} = ## Check if ``pr`` is a Preserve integer equivalent to `i`. case pr.kind of pkRegister: pr.register == i.int of pkBigInt: pr.int == i.initBigInt else: false func isString*(pr: Preserve): bool {.inline.} = pr.kind == pkString ## Check if ``pr`` is a Preserve text string. func isString*(pr: Preserve; s: string): bool {.inline.} = ## Check if ``pr`` is a Preserve text string equivalent to `s`. pr.kind == pkString and pr.string == s func isByteString*(pr: Preserve): bool {.inline.} = pr.kind == pkByteString ## Check if ``pr`` is a Preserves byte string. func isSymbol*(pr: Preserve): bool {.inline.} = pr.kind == pkSymbol ## Check if `pr` is a Preserves symbol. func isSymbol*(pr: Preserve; sym: string|Symbol): bool {.inline.} = ## Check if ``pr`` is a Preserves symbol of ``sym``. (pr.kind == pkSymbol) and (pr.symbol == Symbol(sym)) proc label*(pr: Preserve): Preserve {.inline.} = ## Return the label of record value. pr.record[pr.record.high] proc arity*(pr: Preserve): int {.inline.} = ## Return the number of fields in record `pr`. pred(pr.record.len) func isRecord*(pr: Preserve): bool {.inline.} = (pr.kind == pkRecord) and (pr.record.len > 0) ## Check if ``pr`` is a Preserves record. func isRecord*(pr: Preserve; label: string): bool {.inline.} = ## Check if ``pr`` is a Preserves record with the given label symbol. pr.kind == pkRecord and pr.record.len > 0 and pr.label.isSymbol(label) func isRecord*(pr: Preserve; label: string; arity: Natural): bool {.inline.} = ## Check if ``pr`` is a Preserves record with the given label symbol and field arity. pr.kind == pkRecord and pr.record.len == succ(arity) and pr.label.isSymbol(label) proc isSequence*(pr: Preserve): bool {.inline.} = pr.kind == pkSequence ## Check if ``pr`` is a Preserves sequence. proc isSet*(pr: Preserve): bool {.inline.} = pr.kind == pkSet ## Check if ``pr`` is a Preserves set. proc isDictionary*(pr: Preserve): bool {.inline.} = pr.kind == pkDictionary ## Check if ``pr`` is a Preserves dictionary. func isEmbedded*[E](pr: Preserve[E]): bool {.inline.} = ## Check if ``pr`` is an embedded value. when E is void: pr.embedded # embedded Preserves else: pr.kind == pkEmbedded # embedded Nim proc fields*(pr: Preserve): seq[Preserve] {.inline.} = ## Return the fields of a record value. pr.record[0..pr.record.high.pred] iterator fields*(pr: Preserve): Preserve = ## Iterate the fields of a record value. for i in 0.. ", result proc toPreserveHook*[T](set: HashSet[T]; E: typedesc): Preserve[E] = ## Hook for preserving ``HashSet``. result = Preserve[E](kind: pkSet, set: newSeqOfCap[Preserve[E]](set.len)) for e in set: result.incl toPreserve(e, E) cannonicalize(result) proc toPreserveHook*[A, B](table: Table[A, B]|TableRef[A, B], E: typedesc): Preserve[E] = ## Hook for preserving ``Table``. result = initDictionary(E) for k, v in table.pairs: result[toPreserve(k, E)] = toPreserve(v, E) sortDict(result) func containsNativeEmbeds[E](pr: Preserve[E]): bool = ## Check if a `Preserve[E]` is convertible to `Preserve[void]`. when not E is void: if pr.kind in compoundKinds: for e in pr.items: if e.containsNativeEmbeds: result = true break elif pr.kind == pkEmbedded: result = true proc fromPreserve*[T, E](v: var T; pr: Preserve[E]): bool {.gcsafe.} = ## Inplace version of `preserveTo`. Returns ``true`` on ## a complete match, otherwise returns ``false``. ## Can be customized with `fromPreserveHook[E](x: T; var pr: Preserve[E]): bool`. ## Any ``fromPreserveHook`` that does not compile will be discarded; ## *Write tests for your hooks!* ## ## When `tracePreserves` is defined (`-d:tracePreserves`) a diagnostic ## trace is printing during `fromPreserve`. # TODO: {.raises: [].} runnableExamples: type Foo {.preservesRecord: "foo".} = object x, y: int var foo: Foo assert(fromPreserve(foo, parsePreserves(""""""))) assert(foo.x == 1) assert(foo.y == 2) when T is E: if not pr.embedded and pr.kind == pkEmbedded: v = pr.embed result = true elif T is Preserve[E]: v = pr result = true elif T is Preserve[void]: if pr.containsNativeEmbeds: raise newException(ValueError, "cannot convert Preserve value with embedded " & $E & " values") v = cast[T](pr) result = true elif T is Preserve: {.error: "cannot convert " & $T & " from " & $Preserve[E].} elif compiles(fromPreserveHook(v, pr)): result = fromPreserveHook(v, pr) elif T is enum: if pr.isSymbol: try: v = parseEnum[T](string pr.symbol) result = true except ValueError: discard elif T is bool: if pr.kind == pkBoolean: v = pr.bool result = true elif T is SomeInteger: if pr.kind == pkRegister: v = T(pr.register) result = true elif T is seq[byte]: if pr.kind == pkByteString: v = pr.bytes result = true elif T is seq: if pr.kind == pkSequence: v.setLen(pr.len) result = true for i, e in pr.sequence: result = result and fromPreserve(v[i], e) if not result: v.setLen 0 break elif T is float32: if pr.kind == pkFloat: v = pr.float result = true elif T is float64: case pr.kind of pkFloat: v = pr.float result = true of pkDouble: v = pr.double result = true else: discard elif T is Ordinal | SomeInteger: case pr.kind of pkRegister: v = (T)pr.register result = true of pkBigInt: var o = toInt[T](pr.bigint) result = o.isSome if result: v = get o else: discard elif T is string: if pr.kind == pkString: v = pr.string result = true elif T is Symbol: if pr.kind == pkSymbol: v = pr.symbol result = true elif T is distinct: result = fromPreserve(v.distinctBase, pr) elif T is tuple: case pr.kind of pkRecord, pkSequence: if pr.len <= tupleLen(T): result = true var i {.used.}: int for f in fields(v): if result and i < pr.len: result = result and fromPreserve(f, pr[i]) inc i of pkDictionary: if tupleLen(T) <= pr.len: result = true for key, val in fieldPairs(v): let pv = step(pr, toSymbol(key, E)) result = if pv.isSome: fromPreserve(val, get pv) else: false if not result: break else: discard elif T is ref: if isNil(v): new(v) result = fromPreserve(v[], pr) elif T is object: template fieldFromPreserve(key: string; val: typed; pr: Preserve[E]): bool {.used.} = when v.dot(key).hasCustomPragma(preservesLiteral): const lit = parsePreserves(v.dot(key).getCustomPragmaVal(preservesLiteral)) pr == lit else: fromPreserve(val, pr) when T.hasCustomPragma(unpreservable): raiseAssert($T & " is unpreservable") elif T.hasCustomPragma(preservesRecord): if pr.isRecord and pr.label.isSymbol(T.getCustomPragmaVal(preservesRecord)): result = true var i: int for name, field in fieldPairs(v): when v.dot(name).hasCustomPragma(preservesTupleTail): v.dot(name).setLen(pr.record.len.pred - i) var j: int while result and i < pr.record.high: result = result and fromPreserve(v.dot(name)[j], pr.record[i]) inc i inc j break else: if result and i <= pr.len: result = result and fieldFromPreserve(name, field, pr.record[i]) inc i result = result and (i <= pr.len) elif T.hasCustomPragma(preservesTuple): if pr.isSequence: result = true var i: int for name, field in fieldPairs(v): when v.dot(name).hasCustomPragma(preservesTupleTail): if pr.len >= i: setLen(v.dot(name), pr.len - i) var j: int while result and i < pr.len: result = result and fieldFromPreserve(name, v.dot(name)[j], pr.sequence[i]) inc i inc j else: if result and i < pr.len: result = result and fieldFromPreserve(name, field, pr.sequence[i]) inc i result = result and (i == pr.len) elif T.hasCustomPragma(preservesDictionary): if pr.isDictionary: result = true var i: int for key, _ in fieldPairs(v): let val = pr.getOrDefault(toSymbol(key, E)) result = result and fieldFromPreserve( key, v.dot(key), val) if not result: break inc i result = result and (i <= pr.len) elif T.hasCustomPragma(preservesOr): for kind in typeof(T.orKind): v = T(orKind: kind) var fieldWasFound = false for key, val in fieldPairs(v): when key != "orKind": # fieldPairs unwraps early result = fieldFromPreserve(key, v.dot(key), pr) fieldWasFound = true break if not fieldWasFound: # hopefully a `discard` of-branch, so discard `pr` result = true if result: break else: if pr.isDictionary: result = true var i: int for key, _ in fieldPairs(v): let val = pr.getOrDefault(toSymbol(key, E)) result = result and fieldFromPreserve( key, v.dot(key), val) if not result: break inc i result = result and (i <= pr.len) else: result = fromPreserveHook(v, pr) # a previous branch determined that the hook does not compile but # calling it here explicitly produces a reasonable compiler error when defined(tracePreserves): if not result: trace T, " !- ", pr else: trace T, " <- ", pr proc preserveTo*(pr: Preserve; T: typedesc): Option[T] = ## Reverse of `toPreserve`. # TODO: {.raises: [].} runnableExamples: import std/options type Foo {.preservesRecord: "foo".} = object x, y: int assert(parsePreserves("""""").preserveTo(Foo).isNone) assert(parsePreserves("""""").preserveTo(Foo).isNone) assert(parsePreserves("""""").preserveTo(Foo).isSome) var v: T if fromPreserve(v, pr): result = some(move v) proc fromPreserveHook*[T, E](v: var set[T]; pr: Preserve[E]): bool = ## Hook for unpreserving a `set`. if pr.kind == pkSet: reset v result = true var vv: T for e in pr.set: result = fromPreserve(vv, e) if result: v.incl vv else: reset v break proc fromPreserveHook*[T, E](set: var HashSet[T]; pr: Preserve[E]): bool = ## Hook for preserving ``HashSet``. if pr.kind == pkSet: result = true set.init(pr.set.len) var e: T for pe in pr.set: result = fromPreserve(e, pe) if not result: break set.incl(move e) proc fromPreserveHook*[A,B,E](t: var (Table[A,B]|TableRef[A,B]); pr: Preserve[E]): bool = if pr.isDictionary: when t is TableRef[A,B]: if t.isNil: new t result = true var a: A var b: B for (k, v) in pr.dict.items: result = fromPreserve(a, k) and fromPreserve(b, v) if not result: clear t break t[move a] = move b when isMainModule: var t: Table[int, string] var pr = t.toPreserveHook(void) assert fromPreserveHook(t, pr) proc apply*[E](result: var Preserve[E]; op: proc(_: var Preserve[E]) {.gcsafe.}) {.gcsafe.} = proc recurse(result: var Preserve[E]) = apply(result, op) op(result) case result.kind of pkBoolean, pkFloat, pkDouble, pkRegister, pkBigInt, pkString, pkByteString, pkSymbol, pkEmbedded: discard of pkRecord: apply(result.record, recurse) of pkSequence: apply(result.sequence, recurse) of pkSet: apply(result.set, recurse) of pkDictionary: apply(result.dict) do (e: var DictEntry[E]): recurse(e.key) recurse(e.val) cannonicalize(result) proc mapEmbeds*(pr: sink Preserve[void]; E: typedesc): Preserve[E] = ## Convert `Preserve[void]` to `Preserve[E]` using `fromPreserve` for `E`. when E is void: {.error: "E cannot be void".} if pr.embedded: pr.embedded = false result = Preserve[E](kind: pkEmbedded) if not fromPreserve(result.embed, pr): raise newException(ValueError, "failed to convert " & $E & " from " & $pr) else: case pr.kind of pkBoolean, pkFloat, pkDouble, pkRegister, pkBigInt, pkString, pkByteString, pkSymbol: result = cast[Preserve[E]](pr) of pkRecord: result = Preserve[E](kind: pr.kind) result.record = map(pr.record) do (x: Preserve[void]) -> Preserve[E]: mapEmbeds(x, E) of pkSequence: result = Preserve[E](kind: pr.kind) result.sequence = map(pr.sequence) do (x: Preserve[void]) -> Preserve[E]: mapEmbeds(x, E) of pkSet: result = Preserve[E](kind: pr.kind) result.set = map(pr.set) do (x: Preserve[void]) -> Preserve[E]: mapEmbeds(x, E) of pkDictionary: result = Preserve[E](kind: pr.kind) result.dict = map(pr.dict) do (e: DictEntry[void]) -> DictEntry[E]: (mapEmbeds(e.key, E), mapEmbeds(e.val, E)) of pkEmbedded: result = Preserve[E](kind: pkEmbedded) if not fromPreserve(result.embed, pr): raise newException(ValueError, "failed to convert embedded " & $E) proc mapEmbeds*[A, B](pr: sink Preserve[A]; op: proc (v: A): B): Preserve[B] = ## Convert `Preserve[A]` to `Preserve[B]` using an `A → B` procedure. runnableExamples: import std/tables type MacGuffin = ref object stuff: void var registry = {20: new MacGuffin}.toTable let a = [ 20.embed ].toPreserve(int) b = mapEmbeds(a) do (i: int) -> MacGuffin: registry[i] assert typeof(b[0].unembed) is MacGuffin when A is Preserve: {.error: "cannot mapEmbeds from Preserve[Preserve[…]]".} when B is Preserve: {.error: "cannot mapEmbeds to Preserve[Preserve[…]]".} if pr.embedded: var e: A pr = pr # TODO: avoid copy pr.embedded = false if not fromPreserve(e, pr): raise newException(ValueError, "failed to map across embedded types") result = embed op(e) else: case pr.kind of pkBoolean, pkFloat, pkDouble, pkRegister, pkBigInt, pkString, pkByteString, pkSymbol: result = cast[Preserve[B]](pr) of pkRecord: result = Preserve[B](kind: pr.kind) result.record = map(pr.record) do (x: Preserve[A]) -> Preserve[B]: mapEmbeds(x, op) of pkSequence: result = Preserve[B](kind: pr.kind) result.sequence = map(pr.sequence) do (x: Preserve[A]) -> Preserve[B]: mapEmbeds(x, op) of pkSet: result = Preserve[B](kind: pr.kind) result.set = map(pr.set) do (x: Preserve[A]) -> Preserve[B]: mapEmbeds(x, op) of pkDictionary: result = Preserve[B](kind: pr.kind) result.dict = map(pr.dict) do (e: DictEntry[A]) -> DictEntry[B]: (mapEmbeds(e.key, op), mapEmbeds(e.val, op)) of pkEmbedded: result = embed op(pr.embed) cannonicalize(result) proc contract*[E](pr: sink Preserve[E]; op: proc (v: E): Preserve[void] {.gcsafe.}): Preserve[void] {.gcsafe.} = ## Convert `Preserve[E]` to `Preserve[void]` using an `E → Preserve[void]` procedure. if not pr.embedded: case pr.kind of pkBoolean, pkFloat, pkDouble, pkRegister, pkBigInt, pkString, pkByteString, pkSymbol: result = cast[Preserve[void]](pr) of pkRecord: result = Preserve[void](kind: pr.kind) result.record = map(pr.record) do (x: Preserve[E]) -> Preserve[void]: contract(x, op) of pkSequence: result = Preserve[void](kind: pr.kind) result.sequence = map(pr.sequence) do (x: Preserve[E]) -> Preserve[void]: contract(x, op) of pkSet: result = Preserve[void](kind: pr.kind) result.set = map(pr.set) do (x: Preserve[E]) -> Preserve[void]: contract(x, op) of pkDictionary: result = Preserve[void](kind: pr.kind) result.dict = map(pr.dict) do (e: DictEntry[E]) -> DictEntry[void]: (contract(e.key, op), contract(e.val, op)) of pkEmbedded: result = embed op(pr.embed) cannonicalize(result) proc expand*[E](pr: sink Preserve[void]; op: proc (v: Preserve[void]): Preserve[E] {.gcsafe.}): Preserve[E] {.gcsafe.} = ## Convert `Preserve[void]` to `Preserve[E]` using an `Preserve[void] → Preserve[E]` procedure. if pr.embedded: result = op(pr) else: case pr.kind of pkBoolean, pkFloat, pkDouble, pkRegister, pkBigInt, pkString, pkByteString, pkSymbol: result = cast[Preserve[E]](pr) of pkRecord: result = Preserve[E](kind: pr.kind) result.record = map(pr.record) do (x: Preserve[void]) -> Preserve[E]: expand(x, op) of pkSequence: result = Preserve[E](kind: pr.kind) result.sequence = map(pr.sequence) do (x: Preserve[void]) -> Preserve[E]: expand(x, op) of pkSet: result = Preserve[E](kind: pr.kind) result.set = map(pr.set) do (x: Preserve[void]) -> Preserve[E]: expand(x, op) of pkDictionary: result = Preserve[E](kind: pr.kind) result.dict = map(pr.dict) do (e: DictEntry[void]) -> DictEntry[E]: (expand(e.key, op), expand(e.val, op)) of pkEmbedded: result = op(pr.embed) cannonicalize(result) proc getOrDefault*[T, V](pr: Preserve[T]; key: string; default: V): V = ## Retrieves the value of `pr[key]` if `pr` is a dictionary containing `key` ## or returns the `default` value. var sym = toSymbol(key, T) if pr.kind == pkDictionary: for (k, v) in pr.dict: if sym == k: if fromPreserve(result, v): return else: break default