diff --git a/README.md b/README.md index 0d9d85b..987cef4 100644 --- a/README.md +++ b/README.md @@ -1,5 +1 @@ Nim implementation of the [Preserves data language](https://preserves.gitlab.io/preserves/preserves.html). - -Missing features: -* embedded values -* ordering of compound values diff --git a/preserves.nimble b/preserves.nimble index b5c693b..3f99911 100644 --- a/preserves.nimble +++ b/preserves.nimble @@ -1,6 +1,6 @@ # Package -version = "0.3.0" +version = "1.0.0" author = "Emery Hemingway" description = "data model and serialization format" license = "Unlicense" diff --git a/src/preserves.nim b/src/preserves.nim index 5ca36b9..659538e 100644 --- a/src/preserves.nim +++ b/src/preserves.nim @@ -9,9 +9,15 @@ from std/macros import hasCustomPragma, getCustomPragmaVal type PreserveKind* = enum - pkBoolean, pkFloat, pkDouble, pkSignedInteger, pkBigInteger, pkString, - pkByteString, pkSymbol, pkRecord, pkSequence, pkSet, pkDictionary, pkEmbedded + pkBoolean, pkFloat, pkDouble, pkSignedInteger, pkBigInteger, pkString, pkByteString, pkSymbol, + pkRecord, pkSequence, pkSet, pkDictionary, + pkEmbedded +const + atomKinds* = {pkBoolean, pkFloat, pkDouble, pkSignedInteger, pkBigInteger, pkString, pkByteString, pkSymbol} + compoundKinds* = {pkRecord, pkSequence, pkSet, pkDictionary} + +type DictEntry[EmbededType] = tuple[key: PreserveGen[EmbededType], val: PreserveGen[EmbededType]] PreserveGen*[EmbeddedType] {.acyclic.} = ref object @@ -67,6 +73,7 @@ proc `<`(x, y: string | seq[byte]): bool = x.len < y.len proc `<`*[E](x, y: PreserveGen[E]): bool = + ## Preserves have a total order over Values. Check if `x` is ordered before `y`. if x.kind != y.kind: if x.kind == pkSignedInteger and y.kind == pkBigInteger: result = x.int.initBigInt < y.bigint @@ -120,6 +127,7 @@ proc `<`*[E](x, y: PreserveGen[E]): bool = result = x.embedded < y.embedded proc `==`*[E](x, y: PreserveGen[E]): bool = + ## Check `x` and `y` for equivalence. # TODO: is this necessary to define? if x.isNil or y.isNil: result = x.isNil and y.isNil @@ -155,6 +163,7 @@ proc `==`*[E](x, y: PreserveGen[E]): bool = result = x.embedded == y.embedded proc hash*[E](prs: PreserveGen[E]): Hash = + ## Produce a `Hash` of `prs` for use with a `HashSet` or `Table`. type Value = PreserveGen[E] var h = hash(prs.kind.int) case prs.kind @@ -193,6 +202,8 @@ proc hash*[E](prs: PreserveGen[E]): Hash = !$h proc `[]`*(prs: Preserve; i: int): Preserve = + ## Select an indexed value from `prs`. + ## Only valid for records and sequences. case prs.kind of pkRecord: prs.record[i] of pkSequence: prs.sequence[i] @@ -200,6 +211,7 @@ proc `[]`*(prs: Preserve; i: int): Preserve = raise newException(ValueError, "`[]` is not valid for " & $prs.kind) proc incl*[E](prs: var PreserveGen[E]; key: PreserveGen[E]) = + ## Include `key` in the Preserves set `prs`. for i in 0..prs.set.high: if key < prs.set[i]: insert(prs.set, [key], i) @@ -207,37 +219,57 @@ proc incl*[E](prs: var PreserveGen[E]; key: PreserveGen[E]) = prs.set.add(key) proc excl*[E](prs: var PreserveGen[E]; key: PreserveGen[E]) = + ## Exclude `key` from the Preserves set `prs`. for i in 0..prs.set.high: if prs.set[i] == key: delete(prs.set, i, i) break proc `[]`*[E](prs: var PreserveGen[E]; key: PreserveGen[E]): PreserveGen[E] = + ## Select a value by `key` from the Preserves dictionary `prs`. for (k, v) in prs.dict.items: if k == key: return v raise newException(KeyError, "value not in Preserves dictionary") proc `[]=`*[E](prs: var PreserveGen[E]; key, val: PreserveGen[E]) = + ## Insert `val` by `key` in the Preserves dictionary `prs`. for i in 0..prs.dict.high: if key < prs.dict[i].key: - insert(prs.dict, [(key, val,)], i) + insert(prs.dict, [(key, val, )], i) return elif key == prs.dict[i].key: prs.dict[i].val = val return - prs.dict.add((key, val,)) + prs.dict.add((key, val, )) -proc initRecord*(label: Preserve; args: varargs[Preserve, toPreserve]): Preserve = - ## Record constructor. +proc symbol*(s: string; E = void): PreserveGen[E] {.inline.} = + ## Create a Preserves symbol value. + PreserveGen[E](kind: pkSymbol, symbol: s) + +proc initRecord*[E](label: PreserveGen[E]; args: varargs[PreserveGen[E]]): PreserveGen[E] = + ## Create a Preserves record value. result = Preserve(kind: pkRecord, record: newSeqOfCap[Preserve](1+args.len)) for arg in args: - #assertValid(arg) result.record.add(arg) result.record.add(label) +proc initRecord*(label: string; args: varargs[Preserve, toPreserve]): Preserve = + ## Convert ``label`` to a symbol and create a new record. + runnableExamples: + assert($initRecord("foo", 1, 2.0) == "") + initRecord(symbol(label), args) + +proc initSet*(E = void): PreserveGen[E] = PreserveGen[E](kind: pkSet) + ## Create a Preserves set value. + +proc initDictionary*(E = void): PreserveGen[E] = PreserveGen[E](kind: pkDictionary) + ## Create a Preserves dictionary value. + proc len*[E](prs: PreserveGen[E]): int = - ## Return the number of values one level below ``prs``. + ## Return the shallow count of values in ``prs``, 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 prs.kind of pkRecord: prs.record.len.pred of pkSequence: prs.sequence.len @@ -245,19 +277,10 @@ proc len*[E](prs: PreserveGen[E]): int = of pkDictionary: prs.dict.len else: 0 -proc symbol*(s: string; E = void): PreserveGen[E] {.inline.} = - ## Symbol constructor. - PreserveGen[E](kind: pkSymbol, symbol: s) - -proc initRecord*(label: string; args: varargs[Preserve, toPreserve]): Preserve {.inline.} = - ## Record constructor that converts ``label`` to a symbol. - initRecord(symbol(label), args) - -proc initSet*(E = void): PreserveGen[E] = PreserveGen[E](kind: pkSet) - -proc initDictionary*(E = void): PreserveGen[E] = PreserveGen[E](kind: pkDictionary) - iterator items*[E](prs: PreserveGen[E]): PreserveGen[E] = + ## Shallow iterator over `prs`, yield the fields in a record, + ## the items of a sequence, the items of a set, or the pairs + ## of a dictionary. case prs.kind of pkRecord: for i in 0..prs.record.high.pred: @@ -276,30 +299,33 @@ proc isFalse*[E](prs: PreserveGen[E]): bool = ## Check if ``prs`` is equivalent to the zero-initialized ``Preserve``. prs.kind == pkBoolean and prs.bool == false -proc isSymbol*(prs: Preserve; sym: string): bool = +proc isSymbol*[E](prs: PreserveGen[E]; sym: string): bool = + ## Check if `prs` is a Preserves symbol. (prs.kind == pkSymbol) and (prs.symbol == sym) -func isRecord*(prs: Preserve): bool = +proc isRecord*[E](prs: PreserveGen[E]): bool = + ## Check if `prs` is a Preserves record. if prs.kind == pkRecord: result = true assert(prs.record.len > 0) -func isDictionary*(prs: Preserve): bool = +proc isDictionary*[E](prs: PreserveGen[E]): bool = + ## Check if `prs` is a Preserves dictionary. prs.kind == pkDictionary -proc label*(prs: Preserve): Preserve {.inline.} = - ## Return the label of a record value. +proc label*[E](prs: PreserveGen[E]): PreserveGen[E] {.inline.} = + ## Return the label of record value. prs.record[prs.record.high] -proc arity*(prs: Preserve): int {.inline.} = - ## Return the number of fields in a record value. +proc arity*[E](prs: PreserveGen[E]): int {.inline.} = + ## Return the number of fields in record `prs`. pred(prs.record.len) -proc fields*(prs: Preserve): seq[Preserve] {.inline.} = +proc fields*[E](prs: PreserveGen[E]): seq[PreserveGen[E]] {.inline.} = ## Return the fields of a record value. prs.record[0..prs.record.high.pred] -iterator fields*(prs: Preserve): Preserve = +iterator fields*[E](prs: PreserveGen[E]): PreserveGen[E] = ## Iterate the fields of a record value. for i in 0..") - ## ``` + ## Serialize this object or tuple as a record. See ``toPreserve``. template unpreservable*() {.pragma.} - ## Pragma to forbid a type from being converted by `toPreserve`. + ## Pragma to forbid a type from being converted by ``toPreserve``. proc toPreserve*[T](x: T; E = void): PreserveGen[E] = - ## Serializes `x` to Preserves; uses `toPreserveHook(x: T)` if it's in scope to - ## customize serialization. + ## Serializes ``x`` to Preserves. Can be customized by defining + ## ``toPreserveHook(x: T)`` in the calling scope. + ## ``E`` is the embedded type where ``void`` returns embedded + ## values as Preserves. type Value = PreserveGen[E] when (T is Value): result = x elif T is E: result = Value(kind: pkEmbedded, embedded: x) @@ -579,26 +607,26 @@ proc toPreserve*[T](x: T; E = void): PreserveGen[E] = raiseAssert("unpreservable type" & $T) proc toPreserveHook*[T](set: HashSet[T]): Preserve = + ## Hook for preserving ``HashSet``. Preserve(kind: pkSet, set: set.map(toPreserve)) -proc toPreserveHook*[A,B](table: Table[A,B]|TableRef[A,B]): Preserve = +proc toPreserveHook*[A, B](table: Table[A, B]|TableRef[A, B]): Preserve = + ## Hook for preserving ``Table``. result = Preserve(kind: pkDictionary, dict: initTable[Preserve, Preserve](table.len)) - for k, v in table.pairs: result.dict.add((toPreserve(k), toPreserve(v),)) + for k, v in table.pairs: result.dict.add((toPreserve(k), toPreserve(v), )) -proc fromPreserve*[E,T](v: var T; prs: PreserveGen[E]): bool = - ## Inplace version of `preserveTo`. - ## Partial matches on compond values may leave artifacts in ``v``. +proc fromPreserve*[E, T](v: var T; prs: PreserveGen[E]): bool = + ## Inplace version of `preserveTo`. Returns ``true`` on + ## a complete match, otherwise returns ``false``. # TODO: {.raises: [].} runnableExamples: - import std/options, preserves, preserves/parse + import preserves, preserves/parse type Foo {.record: "foo".} = object - x, y, z: int - + x, y: int var foo: Foo - assert(fromPreserve(foo, parsePreserves(""""""))) + assert(fromPreserve(foo, parsePreserves(""""""))) assert(foo.x == 1) assert(foo.y == 2) - assert(foo.z == 3) type Value = PreserveGen[E] when T is Value: v = prs @@ -683,18 +711,20 @@ proc fromPreserve*[E,T](v: var T; prs: PreserveGen[E]): bool = result = fromPreserve(result.distinctBase, prs) else: raiseAssert("no conversion of type Preserve to " & $T) + if not result: reset v proc preserveTo*[E](prs: PreserveGen[E]; T: typedesc): Option[T] = ## Reverse of `toPreserve`. + ## # TODO: {.raises: [].} runnableExamples: import std/options, preserves, preserves/parse type Foo {.record: "foo".} = object - x, y, z: int + x, y: int assert(parsePreserves("""""").preserveTo(Foo).isNone) - assert(parsePreserves("""""").preserveTo(Foo).isNone) - assert(parsePreserves("""""").preserveTo(Foo).isSome) + assert(parsePreserves("""""").preserveTo(Foo).isNone) + assert(parsePreserves("""""").preserveTo(Foo).isSome) var v: T if fromPreserve(v, prs): result = some(move v) @@ -702,7 +732,7 @@ proc preserveTo*[E](prs: PreserveGen[E]; T: typedesc): Option[T] = proc fromPreserveHook*[A,B](t: var Table[A,B]|TableRef[A,B]; prs: Preserve): bool = if prs.isDictionary: for k, v in prs.pairs: - t[preserveTo(k,A)] = preserveTo(k,B) + t[preserveTo(k, A)] = preserveTo(k, B) result = true proc concat[E](result: var string; prs: PreserveGen[E]) = @@ -775,3 +805,4 @@ proc concat[E](result: var string; prs: PreserveGen[E]) = result.add($prs.embedded) proc `$`*[E](prs: PreserveGen[E]): string = concat(result, prs) + ## Generate the textual representation of ``prs``. diff --git a/src/preserves/private/preserves_schema_nim.nim b/src/preserves/private/preserves_schema_nim.nim index 229f361..5a4a5e4 100644 --- a/src/preserves/private/preserves_schema_nim.nim +++ b/src/preserves/private/preserves_schema_nim.nim @@ -39,7 +39,7 @@ proc ident(sn: SchemaNode): PNode = var s: string case sn.kind of snkAlt: - s = sn.altLabel.toLower.nimIdentNormalize + s = sn.altLabel.toLower.nimIdentNormalize of snkLiteral: s = $sn.value of snkRecord: s = $sn.nodes[0] @@ -245,7 +245,8 @@ proc toConst(name: string; def: SchemaNode): Pnode = of pkSymbol: discard result.add nn(nkCall, ident"symbol", - PNode(kind: nkStrLit, strVal: def.value.symbol)) + PNode(kind: nkStrLit, strVal: def.value.symbol), + ident"EmbeddedType") else: raiseAssert("cannot convert " & $def & " to a Nim literal") else: discard @@ -256,7 +257,8 @@ proc toNimLit(sn: SchemaNode): PNode = of pkSymbol: nkCall.newNode.add( ident"symbol", - PNode(kind: nkStrLit, strVal: sn.value.symbol)) + PNode(kind: nkStrLit, strVal: sn.value.symbol), + ident"EmbeddedType") else: raiseAssert("no Nim literal for " & $sn) @@ -338,7 +340,9 @@ proc generateProcs(result: var seq[PNode]; name: string; sn: SchemaNode) = of snkRecord: var params = nn(nkFormalParams, ident"Preserve") - initRecordCall = nn(nkCall, ident"initRecord", sn.nodes[0].toNimLit) + initRecordCall = nn(nkCall, + nn(nkBracketExpr, ident"initRecord", ident"EmbeddedType"), + sn.nodes[0].toNimLit) for i, field in sn.nodes: if i > 0: let id = field.ident @@ -349,7 +353,8 @@ proc generateProcs(result: var seq[PNode]; name: string; sn: SchemaNode) = ident"Preserve", field.typeIdent), newEmpty()) - initRecordCall.add(id) + initRecordCall.add( + nn(nkCall, ident"toPreserve", id, ident"EmbeddedType")) result.add nn(nkProcDef, exportIdent("prs" & name), newEmpty(), @@ -366,7 +371,16 @@ proc generateNimFile*(scm: Schema; path: string) = typeSection = newNode nkTypeSection constSection = newNode nkConstSection procs: seq[PNode] - if scm.embeddedType != "": + if scm.embeddedType == "": + typeSection.add nn(nkTypeDef, + ident"EmbeddedType", + newEmpty(), + ident"void") + else: + typeSection.add nn(nkTypeDef, + ident"EmbeddedType", + newEmpty(), + ident(scm.embeddedType)) typeSection.add nn(nkTypeDef, ident"Preserve", newEmpty(), @@ -384,15 +398,15 @@ proc generateNimFile*(scm: Schema; path: string) = name.ident.toExport, newEmpty(), t) else: if def.kind == snkRecord: - knownTypes[name] = nn(nkTypeDef, - nn(nkPragmaExpr, - name.ident.toExport, - nn(nkPragma, - nn(nkExprColonExpr, - ident"record", - PNode(kind: nkStrLit, strVal: $def.nodes[0])))), - newEmpty(), - t) + knownTypes[name] = nn(nkTypeDef, + nn(nkPragmaExpr, + name.ident.toExport, + nn(nkPragma, + nn(nkExprColonExpr, + ident"record", + PNode(kind: nkStrLit, strVal: $def.nodes[0])))), + newEmpty(), + t) else: case t.kind of nkEnumTy: diff --git a/tests/test_conversions.nim b/tests/test_conversions.nim index a8d81ef..d51b927 100644 --- a/tests/test_conversions.nim +++ b/tests/test_conversions.nim @@ -12,7 +12,7 @@ suite "conversions": a, b: int c: Bar let - c: Foobar = (a: 1, b: 2, c: Bar(s: "ku",)) + c: Foobar = (a: 1, b: 2, c: Bar(s: "ku", )) b = toPreserve(c) a = preserveTo(b, Foobar) check(a.isSome and (get(a) == c)) @@ -25,7 +25,7 @@ suite "conversions": a, b: int c: Bar let - tup: Foobar = (a: 1, b: 2, c: Bar(s: "ku",)) + tup: Foobar = (a: 1, b: 2, c: Bar(s: "ku", )) prs = toPreserve(tup) check(prs.kind == pkRecord) check($prs == """>""") diff --git a/tests/test_parser.nim b/tests/test_parser.nim index b8e9383..c34f872 100644 --- a/tests/test_parser.nim +++ b/tests/test_parser.nim @@ -30,7 +30,7 @@ suite "parse": checkpoint($test) block: let - a= test + a = test b = decodePreserves(bin) check(a == b) block: