syndicate-nim/src/syndicate/patterns.nim

443 lines
16 KiB
Nim

# SPDX-FileCopyrightText: ☭ Emery Hemingway
# SPDX-License-Identifier: Unlicense
import std/[assertions, options, tables, typetraits]
import preserves
import ./protocols/[dataspacePatterns, dataspace]
from ./actors import Cap
export dataspacePatterns.`$`, AnyAtomKind, GroupTypeKind, PatternKind
type
Pattern* = dataspacePatterns.Pattern
proc toPattern(b: sink PatternBind): Pattern =
Pattern(orKind: PatternKind.`bind`, `bind`: b)
proc toPattern(l: sink PatternLit): Pattern =
Pattern(orKind: PatternKind.`lit`, lit: l)
proc toPattern(g: sink PatternGroup): Pattern =
Pattern(orKind: PatternKind.`group`, group: g)
proc toPattern(a: sink AnyAtom): Pattern =
PatternLit(value: a).toPattern
proc grab*(p: sink Pattern): Pattern =
PatternBind(pattern: p).toPattern
proc drop*(): Pattern = Pattern(orKind: PatternKind.`discard`)
## Create a pattern to match any value without capture.
proc grab*(): Pattern = drop().grab()
## Create a pattern to capture any value.
proc drop*(pr: Value): Pattern =
## Convert a `Preserve` value to a `Pattern`.
runnableExamples:
from std/unittest import check
import preserves
check:
$("""<foo "bar" #"00" [0 1 2.0] {maybe: #t} <_>>""".parsePreserves.drop) ==
"""<group <rec foo> {0: <lit "bar"> 1: <lit #"00"> 2: <group <arr> {0: <lit 0> 1: <lit 1> 2: <lit 2.0>}> 3: <group <dict> {maybe: <lit #t>}> 4: <_>}>"""
case pr.kind
of pkBoolean:
AnyAtom(orKind: AnyAtomKind.`bool`, bool: pr.bool).toPattern
of pkFloat:
AnyAtom(orKind: AnyAtomKind.`double`, double: pr.float).toPattern
of pkRegister:
AnyAtom(orKind: AnyAtomKind.`int`, int: pr.register).toPattern
of pkBigInt:
raiseAssert "cannot make a pattern over a big integer"
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("_", 0):
drop()
elif pr.isRecord("bind", 1):
pr.fields[0].drop
else:
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.rec))
group.`type`.rec.label = pr.label
var i: int
for v in pr.fields:
group.entries[toPreserves i] = drop v
inc i
group.toPattern
of pkSequence:
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.arr))
for i, v in pr.sequence:
group.entries[toPreserves i] = drop v
group.toPattern
of pkSet:
raiseAssert "cannot construct a pattern over a set literal"
of pkDictionary:
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.dict))
for key, val in pr.pairs:
group.entries[key] = drop val
group.toPattern
of pkEmbedded:
if pr.embeddedRef.isNil: drop()
else:
AnyAtom(orKind: AnyAtomKind.`embedded`, embedded: pr.embeddedRef).toPattern
#else:
# raise newException(ValueError, "cannot generate a pattern for unhandled Value type")
proc drop*[T](x: T): Pattern =
## Construct a `Pattern` from value of type `T`.
## This proc is called `drop` because the value `x` is matched but discarded.
runnableExamples:
from std/unittest import check
check:
$drop(true) == "<lit #t>"
$drop(3.14) == "<lit 3.14>"
$drop([0, 1, 2, 3]) == "<group <arr> {0: <lit 0> 1: <lit 1> 2: <lit 2> 3: <lit 3>}>"
drop(x.toPreserves)
proc grab*[T](x: T): Pattern {.
deprecated: "use drop unless you wish to capture the provided value".} =
PatternBind(pattern: drop x).toPattern
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]) ==
"""<group <arr> {0: <bind <_>> 1: <bind <_>> 2: <bind <_>> 3: <bind <_>>}>"""
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) ==
"<group <arr> {0: <bind <_>> 1: <bind <_>>}>"
$(grabType Rect) ==
"<group <rec rect> {0: <group <arr> {0: <bind <_>> 1: <bind <_>>}> 1: <group <arr> {0: <bind <_>> 1: <bind <_>>}>}>"
$(grabType ColoredRect) ==
"<group <dict> {color: <bind <_>> rect: <group <rec rect> {0: <group <arr> {0: <bind <_>> 1: <bind <_>>}> 1: <group <arr> {0: <bind <_>> 1: <bind <_>>}>}>}>"
when typ is ref:
grabType(pointerBase(typ))
elif typ.hasPreservesRecordPragma:
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`rec`))
group.`type`.rec.label = typ.recordLabel.toSymbol
for _, f in fieldPairs(default typ):
group.entries[group.entries.len.toPreserves] = grabType(typeof f)
group.toPattern
elif typ.hasPreservesDictionaryPragma:
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`dict`))
for key, val in fieldPairs(default typ):
group.entries[key.toSymbol] = grabType(typeof val)
group.toPattern
elif typ is tuple:
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`arr`))
for _, f in fieldPairs(default typ):
group.entries[group.entries.len.toPreserves] = grabType(typeof f)
group.toPattern
elif typ is array:
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`arr`))
for i in 0..len(typ):
group.entries[toPreserves i] = grab()
group.toPattern
else:
grab()
proc fieldCount(T: typedesc): int =
for _, _ in fieldPairs(default T):
inc result
proc dropType*(typ: static typedesc): Pattern =
## Derive a `Pattern` from type `typ` without any bindings.
when typ is ref:
dropType(pointerBase(typ))
elif typ.hasPreservesRecordPragma:
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`rec`))
group.`type`.rec.label = typ.recordLabel.toSymbol
let high = typ.fieldCount.pred
if high >= 0: group.entries[high.toPreserves] = drop()
group.toPattern
elif typ.hasPreservesDictionaryPragma:
PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`dict`)).toPattern
elif typ is tuple or typ is array:
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`arr`))
let high = typ.fieldCount.pred
if high >= 0: group.entries[high.toPreserves] = drop()
group.toPattern
else:
drop()
proc bindEntries(group: var PatternGroup; bindings: openArray[(int, Pattern)]) =
## Set `bindings` for a `group`.
for (i, pat) in bindings: group.entries[toPreserves i] = pat
proc grab*(typ: static typedesc; bindings: sink openArray[(int, Pattern)]): Pattern =
## Construct a `Pattern` from type `typ` with pattern `bindings` by integer offset.
when typ is ptr | ref:
grab(pointerBase(typ), bindings)
elif typ.hasPreservesRecordPragma:
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`rec`))
group.`type`.rec.label = typ.recordLabel.toSymbol
bindEntries(group, bindings)
group.toPattern
elif typ is tuple:
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`arr`))
bindEntries(group, bindings)
group.toPattern
else:
{.error: "grab with indexed bindings not implemented for " & $typ.}
proc grab*(typ: static typedesc; bindings: sink openArray[(Value, Pattern)]): Pattern =
## Construct a `Pattern` from type `typ` with dictionary field `bindings`.
when typ.hasPreservesDictionaryPragma:
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`dict`))
for key, val in bindinds: group.entries[key] = val
group.toPattern
else:
{.error: "grab with dictionary bindings not implemented for " & $typ.}
proc grabLit*(): Pattern =
runnableExamples:
from std/unittest import check
check:
$grabLit() == """<group <rec lit> {0: <bind <_>>}>"""
grabType(dataspacePatterns.PatternLit)
proc grabDict*(): Pattern =
grabType(dataspacePatterns.GroupTypeDict)
proc unpackLiterals*(pr: Value): Value =
result = pr
apply(result) do (pr: var Value):
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*(pattern: sink Pattern; p: Pattern; path: varargs[Value, toPreserves]): Pattern =
## Inject `p` inside `pattern` at `path`.
## Injects are made at offsets indexed by the discard (`<_>`) patterns in `pat`.
proc inject(pat: var Pattern; path: openarray[Value]) =
if len(path) == 0:
pat = p
elif pat.orKind != PatternKind.`group`:
raise newException(ValueError, "cannot inject along specified path")
else:
inject(pat.group.entries[path[0]], path[1..path.high])
result = pattern
inject(result, path)
proc grabRecord*(label: Value, fields: varargs[Pattern]): Pattern =
runnableExamples:
from std/unittest import check
import preserves
check:
$grabRecord("Says".toSymbol, grab(), grab()) ==
"""<group <rec Says> {0: <bind <_>> 1: <bind <_>>}>"""
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`rec`))
group.`type`.rec.label = label
for i, f in fields: group.entries[toPreserves i] = f
group.toPattern
proc grabRecord*(label: Value, fields: sink openArray[(int, Pattern)]): Pattern =
runnableExamples:
from std/unittest import check
import preserves
check:
$grabRecord("Says".toSymbol, {3: grab(), 4: grab()}) ==
"""<group <rec Says> {3: <bind <_>> 4: <bind <_>>}>"""
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`rec`))
group.`type`.rec.label = label
for (i, p) in fields: group.entries[toPreserves i] = p
group.toPattern
proc grabRecord*(label: string, fields: varargs[Pattern]): Pattern =
## Sugar for creating record patterns.
## `label` is converted to a symbol value.
grabRecord(label.toSymbol, fields)
proc grabDictionary*(bindings: sink openArray[(Value, Pattern)]): Pattern =
## Construct a pattern that grabs some dictionary pairs.
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`dict`))
for (key, val) in bindings: group.entries[key] = val
group.toPattern
proc grabDictionary*(bindings: sink openArray[(string, Pattern)]): Pattern =
## Construct a pattern that grabs some dictionary pairs.
## Keys are converted from strings to symbols.
var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`dict`))
for (key, val) in bindings: group.entries[toSymbol key] = val
group.toPattern
proc depattern(group: PatternGroup; values: var seq[Value]; index: var int): Value
proc depattern(pat: Pattern; values: var seq[Value]; index: var int): Value =
case pat.orKind
of PatternKind.`discard`:
discard
of PatternKind.`bind`:
if index < values.len:
result = move values[index]
inc index
of PatternKind.`lit`:
result = pat.`lit`.value.toPreserves
of PatternKind.`group`:
result = depattern(pat.group, values, index)
proc depattern(group: PatternGroup; values: var seq[Value]; index: var int): Value =
case group.`type`.orKind
of GroupTypeKind.rec:
result = initRecord(group.`type`.rec.label, group.entries.len)
var i: int
for key, val in group.entries:
if i.fromPreserves key:
result[i] = depattern(val, values, index)
of GroupTypeKind.arr:
result = initSequence(group.entries.len)
var i: int
for key, val in group.entries:
if i.fromPreserves key:
result[i] = depattern(val, values, index)
of GroupTypeKind.dict:
result = initDictionary(Cap)
for key, val in group.entries:
result[key] = depattern(val, values, index)
proc depattern*(pat: Pattern; values: sink seq[Value]): Value =
## Convert a `Pattern` to a `Value` while replacing binds with `values`.
runnableExamples:
from std/unittest import check
import preserves
type Foo {.preservesRecord: "foo".} = object
a, b: int
let pat = grabType Foo
let val = depattern(pat, @[1.toPreserves, 5.toPreserves])
check $val == "<foo 1 5>"
var index: int
depattern(pat, values, index)
type Literal*[T] = object
## A wrapper type to deserialize patterns to native values.
value*: T
proc fromPreservesHook*[T](lit: var Literal[T]; pr: Value): bool =
var pat: Pattern
pat.fromPreserves(pr) and lit.value.fromPreserves(depattern(pat, @[]))
proc toPreservesHook*[T](lit: Literal[T]): Value =
lit.value.grab.toPreserves
func isGroup(pat: Pattern): bool =
pat.orKind == PatternKind.`group`
func isMetaDict(pat: Pattern): bool =
pat.orKind == PatternKind.`group` and
pat.group.type.orKind == GroupTypeKind.dict
proc metaApply(result: var Pattern; pat: Pattern; path: openarray[Value], offset: int) =
if offset == path.len:
result = pat
elif result.isGroup and result.group.entries[1.toPreserves].isMetaDict:
if offset == path.high:
result.group.entries[1.toPreserves].group.entries[path[offset]] = pat
else:
metaApply(result.group.entries[1.toPreserves].group.entries[path[offset]], pat, path, succ offset)
else:
assert result.isGroup, "non-group: " & $result
assert result.group.entries[1.toPreserves].isMetaDict, "non-meta-dict: " & $result.group.entries[1.toPreserves]
raise newException(ValueError, "cannot inject into non-group pattern " & $result)
proc observePattern*(pat: Pattern; injects: openarray[(seq[Value], Pattern)]): Pattern =
result = dropType Observe
var meta = pat.toPreserves.drop
for (path, pat) in injects:
metaApply(meta, pat, path, 0)
result.group.entries[0.toPreserves] = meta
type
Path* = seq[Value]
Paths* = seq[Path]
Captures* = seq[Value]
Analysis* = tuple
presentPaths: Paths
constPaths: Paths
constValues: seq[Value]
capturePaths: Paths
func walk(result: var Analysis; path: var Path; p: Pattern)
func walk(result: var Analysis; path: var Path; key: Value; pat: Pattern) =
path.add(key)
walk(result, path, pat)
discard path.pop
func walk(result: var Analysis; path: var Path; p: Pattern) =
case p.orKind
of PatternKind.group:
for k, v in p.group.entries: walk(result, path, k, v)
of PatternKind.`bind`:
result.capturePaths.add(path)
walk(result, path, p.`bind`.pattern)
of PatternKind.`discard`:
result.presentPaths.add(path)
of PatternKind.`lit`:
result.constPaths.add(path)
result.constValues.add(p.`lit`.value.toPreserves)
func analyse*(p: Pattern): Analysis =
var path: Path
walk(result, path, p)
func checkPresence*(v: Value; present: Paths): bool =
result = true
for path in present:
if not result: break
result = step(v, path).isSome
func projectPaths*(v: Value; paths: Paths): Option[Captures] =
var res = newSeq[Value](paths.len)
for i, path in paths:
var vv = step(v, path)
if vv.isSome: res[i] = get(vv)
else: return
some res
proc matches*(pat: Pattern; pr: Value): bool =
let analysis = analyse(pat)
assert analysis.constPaths.len == analysis.constValues.len
result = checkPresence(pr, analysis.presentPaths)
if result:
for i, path in analysis.constPaths:
let v = step(pr, path)
if v.isNone: return false
if analysis.constValues[i] != v.get: return false
for path in analysis.capturePaths:
if step(pr, path).isNone: return false
proc capture*(pat: Pattern; pr: Value): seq[Value] =
let analysis = analyse(pat)
assert analysis.constPaths.len == analysis.constValues.len
if checkPresence(pr, analysis.presentPaths):
for i, path in analysis.constPaths:
let v = step(pr, path)
if v.isNone : return @[]
if analysis.constValues[i] != v.get: return @[]
for path in analysis.capturePaths:
let v = step(pr, path)
if v.isNone: return @[]
result.add(get v)
when isMainModule:
stdout.writeLine stdin.readAll.parsePreserves.grab