# SPDX-FileCopyrightText: ☭ Emery Hemingway # SPDX-License-Identifier: Unlicense import std/[assertions, options, sets, sequtils, strutils, tables, typetraits] from std/algorithm import sort from std/json import escapeJson, escapeJsonUnquoted import bigints import ./preserves/private/[buffering, encoding, decoding, dot, macros, parsing, texts, values] export buffering, 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 type Preserve*[E] {.deprecated: "preserves.Preserve[E] is now preserves.Value".} = Value proc sortDict(pr: var Value) = sort(pr.dict) do (x, y: DictEntry) -> int: cmp(x.key, y.key) proc cannonicalize*(pr: var Value) = ## 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): cannonicalize(e.val) sortDict(pr) else: discard proc toInt*(pr: Value): Option[int] = case pr.kind of pkRegister: result = some pr.register of pkBigInt: result = toInt[int](pr.bigint) else: discard func isAtomic*(pr: Value): bool = pr.kind in atomKinds func isBoolean*(pr: Value): bool {.inline.} = pr.kind == pkBoolean ## Check if ``pr`` is a Preserve boolean. func isFalse*(pr: Value): bool {.inline.} = ## Check if ``pr`` is equivalent to the zero-initialized ``Preserve``. pr.kind == pkBoolean and pr.bool == false func isFloat*(pr: Value): bool {.inline.} = pr.kind == pkFloat ## Check if ``pr`` is a Preserve float. func isDouble*(pr: Value): bool {.deprecated: "use isFloat".} = pr.kind == pkFloat func isInteger*(pr: Value): bool {.inline.} = ## Check if ``pr`` is a Preserve integer. pr.kind == pkRegister or pr.kind == pkBigInt func isInteger*(pr: Value; 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: Value): bool {.inline.} = pr.kind == pkString ## Check if ``pr`` is a Preserve text string. func isString*(pr: Value; 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: Value): bool {.inline.} = pr.kind == pkByteString ## Check if ``pr`` is a Preserves byte string. func isSymbol*(pr: Value): bool {.inline.} = pr.kind == pkSymbol ## Check if `pr` is a Preserves symbol. func isSymbol*(pr: Value; 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: Value): Value {.inline.} = ## Return the label of record value. pr.record[pr.record.high] proc arity*(pr: Value): int {.inline.} = ## Return the number of fields in record `pr`. pred(pr.record.len) func isRecord*(pr: Value): bool {.inline.} = (pr.kind == pkRecord) and (pr.record.len > 0) ## Check if ``pr`` is a Preserves record. func isRecord*(pr: Value; 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: Value; 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: Value): bool {.inline.} = pr.kind == pkSequence ## Check if ``pr`` is a Preserves sequence. proc isSet*(pr: Value): bool {.inline.} = pr.kind == pkSet ## Check if ``pr`` is a Preserves set. proc isDictionary*(pr: Value): bool {.inline.} = pr.kind == pkDictionary ## Check if ``pr`` is a Preserves dictionary. func isEmbedded*(pr: Value): bool {.inline.} = ## Check if ``pr`` is an embedded value. pr.embedded or pr.kind == pkEmbedded proc `&`*(x, y: Value): Value = ## Concatenate operator. if x.kind != y.kind: raise newException(ValueError, "cannot concatenate heterogenous values") case x.kind of pkString: result = Value(kind: pkString, string: x.string & y.string) of pkByteString: result = Value(kind: pkByteString, bytes: x.bytes & y.bytes) of pkSequence: result = Value(kind: pkSequence, sequence: x.sequence & y.sequence) else: raise newException(ValueError, "cannot concatenate this value type") proc `&`*(x: Value; y: seq[Value]): Value = if x.kind != pkSequence: raise newException(ValueError, "cannot concatenate to non-sequence value") result = Value(kind: pkSequence, sequence: x.sequence & y) proc pop*(pr: var Value; key: Value; val: var Value): 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: Value): Value {.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") proc toSymbol*(s: sink string): Value {.inline.} = ## Create a Preserves symbol value. Value(kind: pkSymbol, symbol: Symbol s) proc toSymbol*(s: sink string; E: typedesc): Value {.deprecated.} = s.toSymbol proc initRecord*(label: Value; arity: Natural = 0): Value = ## Create a Preserves record value. result = Value(kind: pkRecord, record: newSeq[Value](arity.succ)) result.record[arity] = label proc initRecord*(label: Value; E: typedesc): Value {.deprecated.} = initRecord(label) proc initRecord*(label: Value; args: varargs[Value]): Value = ## Create a Preserves record value. result = Value(kind: pkRecord, record: newSeqOfCap[Value](1+args.len)) for arg in args: result.record.add(arg) result.record.add(label) proc initRecord*(label: string; args: varargs[Value]): Value {.inline.} = ## Create a Preserves record value. initRecord(toSymbol(label), args) proc toRecord*(items: varargs[Value, toPreserves]): Value = assert items.len > 0 result = initRecord(items[0], items.len.pred) for i in 0.. ", result proc toPreserve*[T](x: T; E: typedesc = void): Value {.deprecated.} = x.topreserves() proc toPreservesHook*(a: Atom): Value = result = Value(kind: a.kind) case a.kind of pkBoolean: result.bool = a.bool of pkFloat: result.float = a.float of pkRegister: result.register = a.register of pkBigInt: result.bigint = a.bigint of pkString: result.string = a.string of pkByteString: result.bytes = a.bytes of pkSymbol: result.symbol = a.symbol else: discard proc toPreservesHook*[T](set: HashSet[T]): Value = ## Hook for preserving ``HashSet``. result = Value(kind: pkSet, set: newSeqOfCap[Value](set.len)) for e in set: result.incl toPreserves(e) cannonicalize(result) proc toPreservesHook*[A, B](table: Table[A, B]|TableRef[A, B]): Value = ## Hook for preserving ``Table``. result = initDictionary() for k, v in table.pairs: result[toPreserves(k)] = toPreserves(v) sortDict(result) proc toPreservesHook*(o: Option): Value = o.get.toPreserves proc fromAtom*[T](v: var T; a: ATom): bool = if T is Atom: v = a result = true if T is Value: v = a.toPreservesHook result = true elif T is enum: if a.kind == pkSymbol: try: v = parseEnum[T](string a.symbol) result = true except ValueError: discard elif T is bool: if a.kind == pkBoolean: v = a.bool result = true elif T is SomeInteger: if a.kind == pkRegister: result = a.register.T < high(T) if result: v = T a.register elif a.kind == pkBigInt: var o = toInt[T](a.bigint) result = o.isSome if result: v = o.get elif T is seq[byte]: if a.kind == pkByteString: v = a.bytes result = true elif T is SomeFloat: case a.kind of pkFloat: v = T a.float result = true else: discard elif T is Ordinal | SomeInteger: if a.kind == pkRegister: v = T(a.register) result = int(v) == a.register elif a.kind == pkBigInt: var o = toInt[T](a.bigint) if o.isSome: v = get o result = true elif T is string: if a.kind == pkString: v = a.string result = true elif T is Symbol: if a.kind == pkSymbol: v = a.symbol result = true elif T is distinct: result = fromAtom(v.distinctBase, a) proc fromPreserves*[T](v: var T; pr: Value): bool = ## Inplace version of `preservesTo`. Returns ``true`` on ## a complete match, otherwise returns ``false``. ## Can be customized with `fromPreservesHook(x: T; var pr: Value): bool`. ## Any ``fromPreservesHook`` 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(fromPreserves(foo, parsePreserves(""""""))) assert(foo.x == 1) assert(foo.y == 2) when T is Value: v = pr result = true elif T is Value: v = pr result = true elif compiles(fromPreservesHook(v, pr)): result = fromPreservesHook(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 fromPreserves(v[i], pr.sequence[i]) if not result: v.setLen 0 break elif T is SomeFloat: if pr.kind == pkFloat: v = (T)pr.float result = true 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 fromPreserves(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)) result = if pv.isSome: fromPreserves(val, get pv) else: false if not result: break else: discard elif T is ref: when T.hasCustomPragma(preservesEmbedded): if (pr.kind == pkEmbedded) and (pr.embeddedRef of T): v = T(pr.embeddedRef) result = true else: if isNil(v): new(v) result = fromPreserves(v[], pr) elif T is object: template fieldFromPreserve[T](key: string; val: T; pr: Value): bool {.used.} = when v.dot(key).hasCustomPragma(preservesLiteral): const atom = v.dot(key).getCustomPragmaVal(preservesLiteral).parsePreservesAtom pr == atom.toPreservesHook() elif v.dot(key).hasCustomPragma(preservesEmbedded): when T is EmbeddedRef: if pr.kind == pkEmbedded and pr.embeddedRef of T: val = T(pr.embeddedRef) true else: false else: fromPreserves(val, pr) else: fromPreserves(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 fromPreserves(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, field in fieldPairs(v): if not result: break let val = step(pr, key.toSymbol) when field is Option: if val.isSome: discard fieldFromPreserve(key, v.dot(key), val.get) else: inc i result = result and val.isSome if result: var pr = val.get result = result and fieldFromPreserve(key, v.dot(key), pr) 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): if not result: break let val = step(pr, key.toSymbol) result = result and val.isSome if result: result = result and fieldFromPreserve(key, v.dot(key), val.get) inc i result = result and (i <= pr.len) else: result = fromPreservesHook(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 fromPreserve*[T](v: var T; pr: Value): bool {.deprecated.} = v.frompreserves(pr) proc preservesTo*(pr: Value; T: typedesc): Option[T] = ## Reverse of `toPreserve`. # TODO: {.raises: [].} runnableExamples: import std/options type Foo {.preservesRecord: "foo".} = object x, y: int assert(parsePreserves("""""").preservesTo(Foo).isNone) assert(parsePreserves("""""").preservesTo(Foo).isNone) assert(parsePreserves("""""").preservesTo(Foo).isSome) var v: T if fromPreserves(v, pr): result = some(move v) proc fromPreservesHook*[T, E](v: var set[T]; pr: Value): bool = ## Hook for unpreserving a `set`. if pr.kind == pkSet: reset v result = true var vv: T for e in pr.set: result = fromPreserves(vv, e) if result: v.incl vv else: reset v break proc fromPreservesHook*[T, E](set: var HashSet[T]; pr: Value): 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 = fromPreserves(e, pe) if not result: break set.incl(move e) proc fromPreservesHook*[A,B](t: var (Table[A,B]|TableRef[A,B]); pr: Value): 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 = fromPreserves(a, k) and fromPreserves(b, v) if not result: clear t break t[move a] = move b proc fromPreservesHook*[T](opt: var Option[T]; pr: Value): bool = opt = some(default T) result = opt.get.fromPreserves(pr) if not result: opt = none(T) when isMainModule: var t: Table[int, string] var pr = t.toPreservesHook() assert fromPreservesHook(t, pr) func step(pr, idx: Value): Option[Value] = if pr.isDictionary: for (k, v) in pr.dict.items: if k == idx: result = some(v) break elif (pr.isRecord or pr.isSequence): var o = idx.toInt if o.isSome: var i = get o if i < pr.len: result = some(pr[i]) func step*(pr: Value; path: varargs[Value, toPreserves]): Option[Value] = ## Step into `pr` by index `idx`. ## Works for sequences, records, and dictionaries. runnableExamples: import std/options assert step(parsePreserves(""""""), 1.toPreserves) == some(2.toPreserves) assert step(parsePreserves("""{ foo: 1 bar: 2}"""), "foo".toSymbol) == some(1.toPreserves) assert step(parsePreserves("""[ ]"""), 1.toPreserves) == none(Value) result = some(pr) for index in path: if result.isSome: result = step(result.get, index) func step*(pr: Value; key: Symbol): Option[Value] = ## 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 apply*(result: var Value; op: proc(_: var Value) {.gcsafe.}) {.gcsafe.} = proc recurse(result: var Value) = apply(result, op) op(result) case result.kind of pkBoolean, pkFloat, 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): recurse(e.key) recurse(e.val) cannonicalize(result) proc mapEmbeds*(pr: sink Value; op: proc (x: Value): Value): Value = ## Process all embeds in a `Value`. case pr.kind of pkBoolean, pkFloat, pkRegister, pkBigInt, pkString, pkByteString, pkSymbol, pkEmbedded: result = pr of pkRecord: result = Value(kind: pr.kind) result.record = map(pr.record) do (x: Value) -> Value: mapEmbeds(x, op) of pkSequence: result = Value(kind: pr.kind) result.sequence = map(pr.sequence) do (x: Value) -> Value: mapEmbeds(x, op) of pkSet: result = Value(kind: pr.kind) result.set = map(pr.set) do (x: Value) -> Value: mapEmbeds(x, op) of pkDictionary: result = Value(kind: pr.kind) result.dict = map(pr.dict) do (e: DictEntry) -> DictEntry: (mapEmbeds(e.key, op), mapEmbeds(e.val, op)) if pr.embedded or pr.kind == pkEmbedded: result = op(result) result.embedded = true cannonicalize(result) # TODO: is this necessary? proc getOrDefault*[T, V](pr: Value; 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 fromPreserves(result, v): return else: break default