# SPDX-FileCopyrightText: ☭ Emery Hemingway # SPDX-License-Identifier: Unlicense import std/[options, sequtils, tables, typetraits] import preserves import ./protocols/dataspacePatterns from ./actors import Ref export dataspacePatterns.`$`, PatternKind, DCompoundKind, AnyAtomKind type AnyAtom = dataspacePatterns.AnyAtom[Ref] DBind = dataspacePatterns.DBind[Ref] DCompound = dataspacePatterns.DCompound[Ref] DCompoundArr = dataspacePatterns.DCompoundArr[Ref] DCompoundDict = dataspacePatterns.DCompoundDict[Ref] DCompoundRec = dataspacePatterns.DCompoundRec[Ref] DLit = dataspacePatterns.DLit[Ref] Pattern* = dataspacePatterns.Pattern[Ref] proc toPattern(d: sink DBind): Pattern = Pattern(orKind: PatternKind.DBind, dbind: d) proc toPattern(d: sink DLit): Pattern = Pattern(orKind: PatternKind.DLit, dlit: d) proc toPattern(aa: sink AnyAtom): Pattern = DLit(value: aa).toPattern proc toPattern(d: sink DCompound): Pattern = Pattern(orKind: PatternKind.DCompound, dcompound: d) proc toPattern(d: sink DCompoundRec): Pattern = DCompound(orKind: DCompoundKind.rec, rec: d).toPattern proc toPattern(d: sink DCompoundArr): Pattern = DCompound(orKind: DCompoundKind.arr, arr: d).toPattern proc toPattern(d: sink DCompoundDict): Pattern = DCompound(orKind: DCompoundKind.dict, dict: d).toPattern proc drop*(): Pattern {.inline.} = Pattern(orKind: PatternKind.DDiscard) ## Create a pattern to match any value without capture. proc grab*(): Pattern {.inline.} = DBind(pattern: drop()).toPattern ## Create a pattern to capture any value. proc grab*[T](pr: Preserve[T]): Pattern = ## Convert a `Preserve` value to a `Pattern`. runnableExamples: from std/unittest import check import preserves check: $(grab parsePreserves""">""") == """ ]> }> <_>]>""" assert not pr.embedded case pr.kind of pkBoolean: AnyAtom(orKind: AnyAtomKind.`bool`, bool: pr.bool).toPattern of pkFloat: AnyAtom(orKind: AnyAtomKind.`float`, float: pr.float).toPattern of pkDouble: AnyAtom(orKind: AnyAtomKind.`double`, double: pr.double).toPattern of pkSignedInteger: AnyAtom(orKind: AnyAtomKind.`int`, int: pr.int).toPattern of pkString: AnyAtom(orKind: AnyAtomKind.`string`, string: pr.string).toPattern of pkByteString: AnyAtom(orKind: AnyAtomKind.`bytes`, bytes: pr.bytes).toPattern of pkSymbol: AnyAtom(orKind: AnyAtomKind.`symbol`, symbol: pr.symbol).toPattern of pkRecord: if (pr.isRecord("_") and pr.arity == 0) or (pr.isRecord("bind") and pr.arity == 1): drop() else: DCompoundRec( label: cast[Preserve[Ref]](pr.label), # TODO: don't cast like this fields: map[Preserve[T], Pattern](pr.fields, grab)).toPattern of pkSequence: DCompoundArr(items: map(pr.sequence, grab)).toPattern of pkSet: raise newException( ValueError, "cannot construct a pattern over a set literal") of pkDictionary: var dict = DCompoundDict() for key, val in pr.pairs: dict.entries[cast[Preserve[Ref]](key)] = grab val dict.toPattern of pkEmbedded: # TODO: can patterns be constructed over embedded literals? drop() proc grab*[T](val: T): Pattern = ## Construct a `Pattern` from value of type `T`. runnableExamples: from std/unittest import check check: $grab(true) == "" $grab(3.14) == "" $grab([0, 1, 2, 3]) == " ]>" grab (toPreserve(val, Ref)) proc patternOfType(typ: static typedesc; `bind`: static bool): Pattern = when typ is ref: patternOfType(pointerBase(typ), `bind`) elif typ.hasPreservesRecordPragma: var rec = DCompoundRec(label: typ.recordLabel.tosymbol(Ref)) for _, f in fieldPairs(default typ): add(rec.fields, patternOfType(typeof f, `bind`)) result = rec.toPattern elif typ.hasPreservesDictionaryPragma: var dict = DCompoundDict() for key, val in fieldPairs(default typ): dict.entries[toSymbol(key, Ref)] = patternOfType(typeof val, `bind`) dict.toPattern elif typ is tuple: var arr = DCompoundArr() for _, f in fieldPairs(default typ): add(arr.items, patternOfType(typeof f, `bind`)) arr.toPattern elif typ is array: var arr = DCompoundArr() arr.items.setLen(len(typ)) for e in arr.items.mitems: e = grab() arr.toPattern else: if `bind`: grab() else: drop() proc grabType*(typ: static typedesc): Pattern = ## Derive a `Pattern` from type `typ`. ## This works for `tuple` and `object` types but in the ## general case will return a wildcard binding. runnableExamples: import preserves from std/unittest import check check: $grabType(array[3, int]) == """> > >]>""" type Point = tuple[x: int; y: int] Rect {.preservesRecord: "rect".} = tuple[a: Point; B: Point] ColoredRect {.preservesDictionary.} = tuple[color: string; rect: Rect] check: $(grabType Point) == "> >]>" $(grabType Rect) == "> >]> > >]>]>" $(grabType ColoredRect) == "> rect: > >]> > >]>]>}>" patternOfType(typ, true) proc dropType*(typ: static typedesc): Pattern = ## Derive a `Pattern` from type `typ` without any bindings. patternOfType(typ, false) proc fieldCount(T: typedesc): int = for _, _ in fieldPairs(default T): inc result proc match(bindings: sink openArray[(int, Pattern)]; i: int; pat: var Pattern): bool = for (j, b) in bindings: if i == j: pat = b return true proc grab*(typ: static typedesc; bindings: sink openArray[(int, Pattern)]): Pattern = ## Construct a `Pattern` from type `typ` that selectively captures fields. runnableExamples: import preserves from std/unittest import check type Point = tuple[x: int; y: int] Rect {.preservesRecord: "rect".} = tuple[a: Point; B: Point] ColoredRect {.preservesDictionary.} = tuple[color: string; rect: Rect] check: $grab(Point, { 1: grab() }) == " >]>" $grab(Rect, { 0: grab() }) == "> <_>]>]>" when typ is ref: grab(pointerBase(typ), bindings) elif typ.hasPreservesRecordPragma: var rec = DCompoundRec(label: typ.recordLabel.tosymbol(Ref)) rec.fields.setLen(fieldCount typ) var i: int for _, f in fieldPairs(default typ): if not match(bindings, i, rec.fields[i]): rec.fields[i] = dropType(typeof f) inc i result = rec.toPattern elif typ is tuple: var arr = DCompoundArr() arr.items.setLen(fieldCount typ) var i: int for _, f in fieldPairs(default typ): if not match(bindings, i, arr.items[i]): arr.items[i] = dropType(typeof f) inc i result = arr.toPattern else: {.error: "grab with bindings not implemented for " & $typ.} proc grabLit*(): Pattern = runnableExamples: from std/unittest import check check: $grabLit() == """>]>""" grabType(dataspacePatterns.DLit[void]) proc grabDict*(): Pattern = grabType(dataspacePatterns.DCompoundDict[void]) proc unpackLiterals*[E](pr: Preserve[E]): Preserve[E] = result = pr apply(result) do (pr: var Preserve[E]): if pr.isRecord("lit", 1) or pr.isRecord("dict", 1) or pr.isRecord("arr", 1) or pr.isRecord("set", 1): pr = pr.record[0] proc inject*(pat: Pattern; bindings: openArray[(int, Pattern)]): Pattern = ## Construct a `Pattern` from `pat` with injected overrides from `bindings`. ## Injects are made at offsets indexed by the discard (`<_>`) patterns in `pat`. proc inject(pat: Pattern; bindings: openArray[(int, Pattern)]; offset: var int): Pattern = case pat.orKind of PatternKind.DDiscard: result = pat for (off, injection) in bindings: if off == offset: result = injection break inc offset of PatternKind.DBind: let bindOff = offset result = pat result.dbind.pattern = inject(pat.dbind.pattern, bindings, offset) if result.orKind == PatternKind.DBind: for (off, injection) in bindings: if (off == bindOff) and (result.dbind.pattern == injection): result = result.dbind.pattern break # promote the injected pattern over the bind of PatternKind.DLit: result = pat of PatternKind.DCompound: result = pat case pat.dcompound.orKind of DCompoundKind.rec: var fields = mapIt(pat.dcompound.rec.fields): inject(it, bindings, offset) result = DCompoundRec(label: result.dcompound.rec.label, fields: fields).toPattern of DCompoundKind.arr: var items = mapIt(pat.dcompound.arr.items): inject(it, bindings, offset) result = DCompoundArr(items: items).toPattern of DCompoundKind.dict: var dict = DCompoundDict() for key, val in pat.dcompound.dict.entries: dict.entries[key] = inject(val, bindings, offset) result = toPattern(dict) var offset = 0 inject(pat, bindings, offset) proc recordPattern*(label: Preserve[Ref], fields: varargs[Pattern]): Pattern = runnableExamples: from std/unittest import check import syndicate/actors, preserves check: $recordPattern("Says".toSymbol(Ref), grab(), grab()) == """> >]>""" DCompoundRec(label: label, fields: fields.toSeq).toPattern type Value = Preserve[Ref] Path = seq[Value] Analysis* = tuple constPaths: seq[Path] constValues: seq[Value] capturePaths: seq[Path] func walk(result: var Analysis; path: var Path; p: Pattern) func walk(result: var Analysis; path: var Path; key: int|Value; pat: Pattern) = path.add(key.toPreserve(Ref)) walk(result, path, pat) discard path.pop func walk(result: var Analysis; path: var Path; p: Pattern) = case p.orKind of PatternKind.DCompound: case p.dcompound.orKind of DCompoundKind.rec: for k, e in p.dcompound.rec.fields: walk(result, path, k, e) of DCompoundKind.arr: for k, e in p.dcompound.arr.items: walk(result, path, k, e) of DCompoundKind.dict: for k, e in p.dcompound.dict.entries: walk(result, path, k, e) of PatternKind.DBind: result.capturePaths.add(path) walk(result, path, p.dbind.pattern) of PatternKind.DDiscard: discard of PatternKind.DLit: result.constPaths.add(path) result.constValues.add(p.dlit.value.toPreserve(Ref)) func analyse*(p: Pattern): Analysis = var path: Path walk(result, path, p) func projectPath*(v: Value; path: Path): Option[Value] = result = some(v) for index in path: result = preserves.step(result.get, index) if result.isNone: break func projectPaths*(v: Value; paths: seq[Path]): seq[Value] = result = newSeq[Value](paths.len) for i, path in paths: var vv = projectPath(v, path) if vv.isSome: result[i] = get(vv) func matches*(pat: Pattern; pr: Value): bool = let analysis = analyse(pat) assert analysis.constPaths.len == analysis.constValues.len for i, path in analysis.constPaths: let v = projectPath(pr, path) if v.isNone : return false if analysis.constValues[i] != v.get: return false for path in analysis.capturePaths: if isNone projectPath(pr, path): return false true func capture*(pat: Pattern; pr: Value): seq[Value] = let analysis = analyse(pat) assert analysis.constPaths.len == analysis.constValues.len for i, path in analysis.constPaths: let v = projectPath(pr, path) if v.isNone : return @[] if analysis.constValues[i] != v.get: return @[] for path in analysis.capturePaths: let v = projectPath(pr, path) if v.isNone: return @[] result.add(get v) when isMainModule: let txt = readAll stdin if txt != "": let v = parsePreserves(txt) pat = grab v stdout.writeLine(pat)