From 06898e4ec1fc96bf673dec972315190d781e721e Mon Sep 17 00:00:00 2001 From: Emery Hemingway Date: Wed, 17 Apr 2024 16:39:28 +0200 Subject: [PATCH] Update dataspace patterns protocol Skeletons do not check for the presence of ignored entries in pattern matches, this will cause bugs! --- src/syndicate.nim | 5 +- src/syndicate/Tupfile | 1 + src/syndicate/drivers/timers.nim | 4 +- src/syndicate/patterns.nim | 453 +++++++++--------- src/syndicate/protocols/dataspacePatterns.nim | 74 +-- src/syndicate/relays.nim | 41 +- src/syndicate/skeletons.nim | 49 +- syndicate.nimble | 2 +- tests/test_patterns.nim | 164 +++---- tests/test_timers.nim | 14 +- 10 files changed, 394 insertions(+), 413 deletions(-) diff --git a/src/syndicate.nim b/src/syndicate.nim index e296e12..7293648 100644 --- a/src/syndicate.nim +++ b/src/syndicate.nim @@ -19,7 +19,7 @@ proc `!`*(typ: static typedesc): Pattern {.inline.} = patterns.dropType(typ) proc `?`*[T](val: T): Pattern {.inline.} = - patterns.grab[T](val) + patterns.drop[T](val) proc `?:`*(typ: static typedesc): Pattern {.inline.} = patterns.grabType(typ) @@ -30,9 +30,6 @@ proc `?:`*(typ: static typedesc; bindings: sink openArray[(int, Pattern)]): Patt proc `?:`*(typ: static typedesc; bindings: sink openArray[(Value, Pattern)]): Pattern {.inline.} = patterns.grab(typ, bindings) -proc `??`*(pat: Pattern; bindings: openArray[(int, Pattern)]): Pattern {.inline.} = - patterns.inject(pat, bindings) - type PublishProc = proc (turn: var Turn; v: Value; h: Handle) {.closure.} RetractProc = proc (turn: var Turn; h: Handle) {.closure.} diff --git a/src/syndicate/Tupfile b/src/syndicate/Tupfile index 50d81d2..db35de1 100644 --- a/src/syndicate/Tupfile +++ b/src/syndicate/Tupfile @@ -1,3 +1,4 @@ include_rules NIM_FLAGS += --path:$(TUP_CWD)/.. : foreach *.nim |> !nim_check |> +: patterns.nim |> !nim_bin |> diff --git a/src/syndicate/drivers/timers.nim b/src/syndicate/drivers/timers.nim index 1bea87c..2816eb5 100644 --- a/src/syndicate/drivers/timers.nim +++ b/src/syndicate/drivers/timers.nim @@ -134,7 +134,7 @@ proc spawnTimerDriver*(turn: var Turn; ds: Cap): Actor {.discardable.} = ## dataspace observations of timeouts on `ds`. linkActor(turn, "timers") do (turn: var Turn): let driver = spawnTimerDriver(turn.facet, ds) - let pat = inject(grab Observe(pattern: dropType LaterThan), {0: grabLit()}) + let pat = observePattern(!LaterThan, {@[0.toPreserves]: grabLit()}) during(turn, ds, pat) do (deadline: float): driver.deadlines[deadline] = turn.facet discard trampoline(whelp await(driver, deadline)) @@ -144,5 +144,5 @@ proc spawnTimerDriver*(turn: var Turn; ds: Cap): Actor {.discardable.} = proc after*(turn: var Turn; ds: Cap; dur: Duration; act: TurnAction) = ## Execute `act` after some duration of time. var later = wallFloat() + dur.inMilliseconds.float / 1_000.0 - onPublish(turn, ds, grab LaterThan(seconds: later)): + onPublish(turn, ds, ?LaterThan(seconds: later)): act(turn) diff --git a/src/syndicate/patterns.nim b/src/syndicate/patterns.nim index 7fa0f92..42a5d43 100644 --- a/src/syndicate/patterns.nim +++ b/src/syndicate/patterns.nim @@ -1,70 +1,46 @@ # SPDX-FileCopyrightText: ☭ Emery Hemingway # SPDX-License-Identifier: Unlicense -import std/[algorithm, assertions, options, sequtils, tables, typetraits] +import std/[assertions, options, tables, typetraits] import preserves -import ./protocols/dataspacePatterns +import ./protocols/[dataspacePatterns, dataspace] from ./actors import Cap -export dataspacePatterns.`$`, PatternKind, DCompoundKind, AnyAtomKind +export dataspacePatterns.`$`, AnyAtomKind, GroupTypeKind, PatternKind type - AnyAtom = dataspacePatterns.AnyAtom - DBind = dataspacePatterns.DBind - DCompound = dataspacePatterns.DCompound - DCompoundArr = dataspacePatterns.DCompoundArr - DCompoundDict = dataspacePatterns.DCompoundDict - DCompoundRec = dataspacePatterns.DCompoundRec - DLit = dataspacePatterns.DLit Pattern* = dataspacePatterns.Pattern -iterator orderedEntries*(dict: DCompoundDict): (Value, Pattern) = - ## Iterate a `DCompoundDict` in Preserves order. - ## Values captured from a dictionary are represented as an - ## array of values ordered by their former key, so using an - ## ordered iterator is sometimes essential. - var keys = dict.entries.keys.toSeq - sort(keys, preserves.cmp) - for k in keys: - yield(k, dict.entries.getOrDefault(k)) - # getOrDefault doesn't raise and we know the keys will match +proc toPattern(b: sink PatternBind): Pattern = + Pattern(orKind: PatternKind.`bind`, `bind`: b) -proc toPattern(d: sink DBind): Pattern = - Pattern(orKind: PatternKind.DBind, dbind: d) +proc toPattern(l: sink PatternLit): Pattern = + Pattern(orKind: PatternKind.`lit`, lit: l) -proc toPattern(d: sink DLit): Pattern = - Pattern(orKind: PatternKind.DLit, dlit: d) +proc toPattern(g: sink PatternGroup): Pattern = + Pattern(orKind: PatternKind.`group`, group: g) -proc toPattern(aa: sink AnyAtom): Pattern = - DLit(value: aa).toPattern +proc toPattern(a: sink AnyAtom): Pattern = + PatternLit(value: a).toPattern -proc toPattern(d: sink DCompound): Pattern = - Pattern(orKind: PatternKind.DCompound, dcompound: d) +proc grab*(p: sink Pattern): Pattern = + PatternBind(pattern: p).toPattern -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) +proc drop*(): Pattern = Pattern(orKind: PatternKind.`discard`) ## Create a pattern to match any value without capture. -proc grab*(): Pattern {.inline.} = DBind(pattern: drop()).toPattern +proc grab*(): Pattern = drop().grab() ## Create a pattern to capture any value. -proc grab*(pr: Value): Pattern = +proc drop*(pr: Value): Pattern = ## Convert a `Preserve` value to a `Pattern`. runnableExamples: from std/unittest import check import preserves check: - $(grab parsePreserves""">""") == - """ ]> }> <_>]>""" + $(""">""".parsePreserves.drop) == + """ {0: 1: 2: {0: 1: 2: }> 3: {maybe: }> 4: <_>}>""" case pr.kind of pkBoolean: @@ -73,43 +49,65 @@ proc grab*(pr: Value): Pattern = 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("_") and pr.arity == 0) or (pr.isRecord("bind") and pr.arity == 1): + if pr.isRecord("_", 0): drop() + elif pr.isRecord("bind", 1): + pr.fields[0].drop else: - DCompoundRec( - label: pr.label, - fields: map[Value, Pattern](pr.fields, grab)).toPattern + 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: - DCompoundArr(items: map(pr.sequence, grab)).toPattern + 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 dict = DCompoundDict() - for key, val in pr.pairs: dict.entries[key] = grab val - dict.toPattern + 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") + #else: + # raise newException(ValueError, "cannot generate a pattern for unhandled Value type") -proc grab*[T](x: T): Pattern = +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: - $grab(true) == "" - $grab(3.14) == "" - $grab([0, 1, 2, 3]) == " ]>" - grab(x.toPreserves) + $drop(true) == "" + $drop(3.14) == "" + $drop([0, 1, 2, 3]) == " {0: 1: 2: 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`. @@ -120,40 +118,41 @@ proc grabType*(typ: static typedesc): Pattern = from std/unittest import check check: $grabType(array[3, int]) == - """> > >]>""" + """ {0: > 1: > 2: > 3: >}>""" 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) == - "> >]>" + " {0: > 1: >}>" $(grabType Rect) == - "> >]> > >]>]>" + " {0: {0: > 1: >}> 1: {0: > 1: >}>}>" $(grabType ColoredRect) == - "> rect: > >]> > >]>]>}>" + " {color: > rect: {0: {0: > 1: >}> 1: {0: > 1: >}>}>}>" when typ is ref: grabType(pointerBase(typ)) elif typ.hasPreservesRecordPragma: - var rec = DCompoundRec(label: typ.recordLabel.toSymbol) + var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`rec`)) + group.`type`.rec.label = typ.recordLabel.toSymbol for _, f in fieldPairs(default typ): - add(rec.fields, grabType(typeof f)) - result = rec.toPattern + group.entries[group.entries.len.toPreserves] = grabType(typeof f) + group.toPattern elif typ.hasPreservesDictionaryPragma: - var dict = DCompoundDict() + var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`dict`)) for key, val in fieldPairs(default typ): - dict.entries[key.toSymbol] = grabType(typeof val) - dict.toPattern + group.entries[key.toSymbol] = grabType(typeof val) + group.toPattern elif typ is tuple: - var arr = DCompoundArr() + var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`arr`)) for _, f in fieldPairs(default typ): - add(arr.items, grabType(typeof f)) - arr.toPattern + group.entries[group.entries.len.toPreserves] = grabType(typeof f) + group.toPattern elif typ is array: - var arr = DCompoundArr() - arr.items.setLen(len(typ)) - for e in arr.items.mitems: e = grab() - arr.toPattern + var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`arr`)) + for i in 0..len(typ): + group.entries[toPreserves i] = grab() + group.toPattern else: grab() @@ -166,59 +165,47 @@ proc dropType*(typ: static typedesc): Pattern = when typ is ref: dropType(pointerBase(typ)) elif typ.hasPreservesRecordPragma: - var rec = DCompoundRec(label: typ.recordLabel.toSymbol) - rec.fields.setLen(fieldCount typ) - for i, _ in rec.fields: - rec.fields[i] = drop() - result = rec.toPattern + 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: - DCompoundDict().toPattern - elif typ is tuple: - var arr = DCompoundArr() - arr.items.setLen(len typ) - for i, _ in arr.items: - arr.items[i] = drop() - arr.toPattern - elif typ is array: - var arr = DCompoundArr() - arr.items.setLen(len(typ)) - for e in arr.items.mitems: e = drop() - arr.toPattern + 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 lookup(bindings: openArray[(int, Pattern)]; i: int): Pattern = - for (j, b) in bindings: - if i == j: return b - return 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 rec = DCompoundRec(label: typ.recordLabel.toSymbol) - rec.fields.setLen(fieldCount typ) - var i: int - for _, f in fieldPairs(default typ): - rec.fields[i] = lookup(bindings, i) - inc i - result = rec.toPattern + 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 arr = DCompoundArr() - arr.items.setLen(fieldCount typ) - var i: int - for _, f in fieldPairs(default typ): - arr.items[i] = lookup(bindings, i) - inc i - result = arr.toPattern + 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: - DCompoundDict(entries: bindings.toTable).toPattern + 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.} @@ -226,11 +213,11 @@ proc grabLit*(): Pattern = runnableExamples: from std/unittest import check check: - $grabLit() == """>]>""" - grabType(dataspacePatterns.DLit) + $grabLit() == """ {0: >}>""" + grabType(dataspacePatterns.PatternLit) proc grabDict*(): Pattern = - grabType(dataspacePatterns.DCompoundDict) + grabType(dataspacePatterns.GroupTypeDict) proc unpackLiterals*(pr: Value): Value = result = pr @@ -238,64 +225,42 @@ proc unpackLiterals*(pr: Value): 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*(pat: Pattern; bindings: openArray[(int, Pattern)]): Pattern = - ## Construct a `Pattern` from `pat` with injected overrides from `bindings`. +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: 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 inject*(pat: Pattern; bindings: openArray[(Value, Pattern)]): Pattern = - ## Inject `bindings` into a dictionary pattern. - assert pat.orKind == PatternKind.DCompound - assert pat.dcompound.orKind == DCompoundKind.dict - result = pat - for (key, val) in bindings: - result.dcompound.dict.entries[key] = val + 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 syndicate/actors, preserves + import preserves check: $grabRecord("Says".toSymbol, grab(), grab()) == - """> >]>""" - DCompoundRec(label: label, fields: fields.toSeq).toPattern + """ {0: > 1: >}>""" + 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()}) == + """ {3: > 4: >}>""" + 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. @@ -304,47 +269,61 @@ proc grabRecord*(label: string, fields: varargs[Pattern]): Pattern = proc grabDictionary*(bindings: sink openArray[(Value, Pattern)]): Pattern = ## Construct a pattern that grabs some dictionary pairs. - DCompoundDict(entries: bindings.toTable).toPattern + 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. - result = DCompoundDict().toPattern - for (key, val) in bindings.items: - result.dcompound.dict.entries[key.toSymbol] = val + var group = PatternGroup(`type`: GroupType(orKind: GroupTypeKind.`dict`)) + for (key, val) in bindings: group.entries[toSymbol key] = val + group.toPattern -proc depattern(comp: DCompound; values: var seq[Value]; index: var int): Value +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.DDiscard: + of PatternKind.`discard`: discard - of PatternKind.DBind: + of PatternKind.`bind`: if index < values.len: result = move values[index] inc index - of PatternKind.DLit: - result = pat.dlit.value.toPreserves - of PatternKind.DCompound: - result = depattern(pat.dcompound, values, index) + of PatternKind.`lit`: + result = pat.`lit`.value.toPreserves + of PatternKind.`group`: + result = depattern(pat.group, values, index) -proc depattern(comp: DCompound; values: var seq[Value]; index: var int): Value = - case comp.orKind - of DCompoundKind.rec: - result = initRecord(comp.rec.label, comp.rec.fields.len) - for i, f in comp.rec.fields: - result[i] = depattern(f, values, index) - of DCompoundKind.arr: - result = initSequence(comp.arr.items.len) - for i, e in comp.arr.items: - result[i] = depattern(e, values, index) - of DCompoundKind.dict: +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 comp.dict.entries: + 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 == "" var index: int depattern(pat, values, index) @@ -359,44 +338,73 @@ proc fromPreservesHook*[T](lit: var Literal[T]; pr: Value): bool = 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: int|Value; pat: Pattern) = - path.add(key.toPreserves) +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.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.orderedEntries: walk(result, path, k, e) - of PatternKind.DBind: + 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.dbind.pattern) - of PatternKind.DDiscard: discard - of PatternKind.DLit: + walk(result, path, p.`bind`.pattern) + of PatternKind.`discard`: + result.presentPaths.add(path) + of PatternKind.`lit`: result.constPaths.add(path) - result.constValues.add(p.dlit.value.toPreserves) + 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: @@ -408,30 +416,27 @@ func projectPaths*(v: Value; paths: Paths): Option[Captures] = proc 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 = step(pr, path) - if v.isNone: return false - if analysis.constValues[i] != v.get: return false - for path in analysis.capturePaths: - if isNone step(pr, path): return false - true + 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 -func capture*(pat: Pattern; pr: Value): seq[Value] = +proc 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 = 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) + 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: - let txt = readAll stdin - if txt != "": - let - v = parsePreserves(txt) - pat = grab v - stdout.writeLine(pat) + stdout.writeLine stdin.readAll.parsePreserves.grab diff --git a/src/syndicate/protocols/dataspacePatterns.nim b/src/syndicate/protocols/dataspacePatterns.nim index 9c8a94b..2aae0d9 100644 --- a/src/syndicate/protocols/dataspacePatterns.nim +++ b/src/syndicate/protocols/dataspacePatterns.nim @@ -29,58 +29,58 @@ type `embedded`* {.preservesEmbedded.}: EmbeddedRef - DLit* {.preservesRecord: "lit".} = object - `value`*: AnyAtom - - DBind* {.preservesRecord: "bind".} = object - `pattern`*: Pattern - - DDiscard* {.preservesRecord: "_".} = object - - DCompoundKind* {.pure.} = enum + GroupTypeKind* {.pure.} = enum `rec`, `arr`, `dict` - DCompoundRec* {.preservesRecord: "rec".} = object + GroupTypeRec* {.preservesRecord: "rec".} = object `label`*: Value - `fields`*: seq[Pattern] - DCompoundArr* {.preservesRecord: "arr".} = object - `items`*: seq[Pattern] + GroupTypeArr* {.preservesRecord: "arr".} = object + + GroupTypeDict* {.preservesRecord: "dict".} = object + + `GroupType`* {.preservesOr.} = object + case orKind*: GroupTypeKind + of GroupTypeKind.`rec`: + `rec`*: GroupTypeRec - DCompoundDict* {.preservesRecord: "dict".} = object - `entries`*: Table[Value, Pattern] + of GroupTypeKind.`arr`: + `arr`*: GroupTypeArr - `DCompound`* {.preservesOr.} = object - case orKind*: DCompoundKind - of DCompoundKind.`rec`: - `rec`* {.preservesEmbedded.}: DCompoundRec - - of DCompoundKind.`arr`: - `arr`* {.preservesEmbedded.}: DCompoundArr - - of DCompoundKind.`dict`: - `dict`* {.preservesEmbedded.}: DCompoundDict + of GroupTypeKind.`dict`: + `dict`*: GroupTypeDict PatternKind* {.pure.} = enum - `DDiscard`, `DBind`, `DLit`, `DCompound` + `discard`, `bind`, `lit`, `group` + PatternDiscard* {.preservesRecord: "_".} = object + + PatternBind* {.preservesRecord: "bind".} = object + `pattern`*: Pattern + + PatternLit* {.preservesRecord: "lit".} = object + `value`*: AnyAtom + + PatternGroup* {.preservesRecord: "group".} = object + `type`*: GroupType + `entries`*: Table[Value, Pattern] + `Pattern`* {.acyclic, preservesOr.} = ref object case orKind*: PatternKind - of PatternKind.`DDiscard`: - `ddiscard`*: DDiscard + of PatternKind.`discard`: + `discard`*: PatternDiscard - of PatternKind.`DBind`: - `dbind`* {.preservesEmbedded.}: DBind + of PatternKind.`bind`: + `bind`* {.preservesEmbedded.}: PatternBind - of PatternKind.`DLit`: - `dlit`* {.preservesEmbedded.}: DLit + of PatternKind.`lit`: + `lit`* {.preservesEmbedded.}: PatternLit - of PatternKind.`DCompound`: - `dcompound`* {.preservesEmbedded.}: DCompound + of PatternKind.`group`: + `group`* {.preservesEmbedded.}: PatternGroup -proc `$`*(x: AnyAtom | DLit | DBind | DDiscard | DCompound | Pattern): string = +proc `$`*(x: AnyAtom | GroupType | Pattern): string = `$`(toPreserves(x)) -proc encode*(x: AnyAtom | DLit | DBind | DDiscard | DCompound | Pattern): seq[ - byte] = +proc encode*(x: AnyAtom | GroupType | Pattern): seq[byte] = encode(toPreserves(x)) diff --git a/src/syndicate/relays.nim b/src/syndicate/relays.nim index f631366..1b01bfb 100644 --- a/src/syndicate/relays.nim +++ b/src/syndicate/relays.nim @@ -251,7 +251,7 @@ proc recv(relay: Relay; buf: openarray[byte]; slice: Slice[int]) = var pr = decode(relay.wireBuf) if pr.isSome: dispatch(relay, pr.get) -proc recv(relay: Relay; buf: openarray[byte]) = +proc recv(relay: Relay; buf: openarray[byte]) {.used.} = feed(relay.wireBuf, buf) var pr = decode(relay.wireBuf) if pr.isSome: dispatch(relay, pr.get) @@ -393,11 +393,12 @@ when defined(posix): entity.alive = false close(entity.sock) onStop(turn, kill) - publish(turn, ds, TransportConnection( + var ass = TransportConnection( `addr`: ta.toPreserves, control: newCap(entity, turn), resolved: entity.relay.peer.accepted, - )) + ) + publish(turn, ds, ass) run(entity.relay.facet, setup) let buf = new seq[byte] entity.alive = true @@ -549,31 +550,28 @@ proc connectRoute(turn: var Turn; ds: Cap; route: Route; transOff: int) = onPublish(turn, ds, acceptPat) do (origin: Cap): walk(turn, ds, origin, route, transOff, 0) -type StepCallback = proc (turn: var Turn; step: Value; origin, next: Cap) {.closure.} +type StepCallback = proc (turn: var Turn; step: Value; origin: Cap; res: Resolved) {.closure.} proc spawnStepResolver(turn: var Turn; ds: Cap; stepType: Value; cb: StepCallback) = - let stepPat = grabRecord(stepType, grab()) - let pat = ?Observe(pattern: ResolvedPathStep?:{1: stepPat}) ?? {0: grabLit(), 1: grab()} + let pat = observePattern( + ResolvedPathStep?:{1: grabRecord(stepType)}, + { @[0.toPreserve]: grabLit(), @[1.toPreserve]: grab() }, + ) during(turn, ds, pat) do (origin: Cap; stepDetail: Literal[Value]): - let step = toRecord(stepType, stepDetail.value) proc duringCallback(turn: var Turn; ass: Value; h: Handle): TurnAction = - var res = ass.preservesTo Resolved - if res.isSome: - if res.get.orKind == ResolvedKind.accepted and - res.get.accepted.responderSession of Cap: - cb(turn, step, origin, res.get.accepted.responderSession.Cap) - else: - publish(turn, ds, ResolvedPathStep( - origin: origin, pathStep: step, resolved: res.get)) + var res: Resolved + if res.fromPreserves ass: + cb(turn, stepDetail.value, origin, res) proc action(turn: var Turn) = stop(turn) result = action publish(turn, origin, Resolve( - step: step, observer: newCap(turn, during(duringCallback)))) + step: stepDetail.value, observer: newCap(turn, during(duringCallback)))) proc spawnRelays*(turn: var Turn; ds: Cap) = - ## Spawn actors that manage routes and appeasing gatekeepers. - let transPat = ?Observe(pattern: !TransportConnection) ?? { 0: grab() } + ## Spawn actors that manage routes and appease gatekeepers. + + let transPat = observePattern(!TransportConnection, { @[0.toPreserves]: grab() }) # Use a generic pattern and type matching # in the during handler because it is easy. @@ -600,20 +598,21 @@ proc spawnRelays*(turn: var Turn; ds: Cap) = resolved: rejected(embed e), )) - let resolvePat = ?Observe(pattern: !ResolvePath) ?? {0: grab()} + let resolvePat = observePattern(!ResolvePath, {@[0.toPreserves]: grab()}) during(turn, ds, resolvePat) do (route: Literal[Route]): for i, transAddr in route.value.transports: connectRoute(turn, ds, route.value, i) spawnStepResolver(turn, ds, "ref".toSymbol) do ( - turn: var Turn, step: Value, origin: Cap, next: Cap): + turn: var Turn, step: Value, origin: Cap, res: Resolved): publish(turn, ds, ResolvedPathStep( - origin: origin, pathStep: step, resolved: next.accepted)) + origin: origin, pathStep: step, resolved: res)) type BootProc* = proc (turn: var Turn; ds: Cap) {.closure.} proc resolve*(turn: var Turn; ds: Cap; route: Route; bootProc: BootProc) = ## Resolve `route` within `ds` and call `bootProc` with resolved capabilities. + let pat = ResolvePath ?: {0: ?route, 3: ?:ResolvedAccepted} during(turn, ds, ResolvePath ?: {0: ?route, 3: ?:ResolvedAccepted}) do (dst: Cap): bootProc(turn, dst) diff --git a/src/syndicate/skeletons.nim b/src/syndicate/skeletons.nim index 2b3c583..32b9041 100644 --- a/src/syndicate/skeletons.nim +++ b/src/syndicate/skeletons.nim @@ -9,32 +9,28 @@ import ./actors, ./bags, ./patterns import ./protocols/dataspacePatterns type - DCompound = dataspacePatterns.DCompound Pattern = dataspacePatterns.Pattern Path = seq[Value] ClassKind = enum classNone, classRecord, classSequence, classDictionary Class = object kind: ClassKind label: Value - arity: int func classOf(v: Value): Class = case v.kind - of pkRecord: Class(kind: classRecord, label: v.label, arity: v.arity) - of pkSequence: Class(kind: classSequence, arity: v.len) + of pkRecord: Class(kind: classRecord, label: v.label) + of pkSequence: Class(kind: classSequence) of pkDictionary: Class(kind: classDictionary) else: Class(kind: classNone) proc classOf(p: Pattern): Class = - if p.orKind == PatternKind.DCompound: - case p.dcompound.orKind - of DCompoundKind.rec: - Class(kind: classRecord, - label: p.dcompound.rec.label, - arity: p.dcompound.rec.fields.len) - of DCompoundKind.arr: - Class(kind: classSequence, arity: p.dcompound.arr.items.len) - of DCompoundKind.dict: + if p.orKind == PatternKind.group: + case p.group.type.orKind + of GroupTypeKind.rec: + Class(kind: classRecord, label: p.group.`type`.rec.label) + of GroupTypeKind.arr: + Class(kind: classSequence) + of GroupTypeKind.dict: Class(kind: classDictionary) else: Class(kind: classNone) @@ -69,13 +65,14 @@ type LeafProc = proc (l: Leaf; v: Value) {.closure.} ObserverProc = proc (turn: var Turn; group: ObserverGroup; vs: seq[Value]) {.closure.} -proc getLeaves(cont: Continuation; constPaths: Paths): LeafMap = +proc getLeaves(cont: Continuation; presentPaths, constPaths: Paths): LeafMap = result = cont.leafMap.getOrDefault(constPaths) if result.isNil: new result cont.leafMap[constPaths] = result assert not cont.isEmpty for ass in cont.cache: + # TODO: check presence let key = projectPaths(ass, constPaths) if key.isSome: var leaf = result.getOrDefault(get key) @@ -165,25 +162,13 @@ proc getOrNew[A, B, C](t: var Table[A, TableRef[B, C]], k: A): TableRef[B, C] = result = newTable[B, C]() t[k] = result -iterator pairs(dc: DCompound): (Value, Pattern) = - case dc.orKind - of DCompoundKind.rec: - for i, p in dc.rec.fields: - yield (i.toPreserves, p,) - of DCompoundKind.arr: - for i, p in dc.arr.items: - yield (i.toPreserves, p,) - of DCompoundKind.dict: - for pair in dc.dict.entries.pairs: - yield pair - proc extendWalk(node: Node; popCount: Natural; stepIndex: Value; pat: Pattern; path: var Path): tuple[popCount: Natural, nextNode: Node] = case pat.orKind - of PatternKind.DDiscard, PatternKind.DLit: + of PatternKind.`discard`, PatternKind.lit: result = (popCount, node) - of PatternKind.DBind: - result = extendWalk(node, popCount, stepIndex, pat.dbind.pattern, path) - of PatternKind.DCompound: + of PatternKind.`bind`: + result = extendWalk(node, popCount, stepIndex, pat.`bind`.pattern, path) + of PatternKind.`group`: let selector: Selector = (popCount, stepIndex,) table = node.edges.getOrNew(selector) @@ -198,7 +183,7 @@ proc extendWalk(node: Node; popCount: Natural; stepIndex: Value; pat: Pattern; p if v.isSome and class == classOf(get v): result.nextNode.continuation.cache.incl a result.popCount = 0 - for step, p in pat.dcompound.pairs: + for step, p in pat.group.entries: add(path, step) result = extendWalk(result.nextNode, result.popCount, step, p, path) discard pop(path) @@ -231,7 +216,7 @@ proc add*(index: var Index; turn: var Turn; pattern: Pattern; observer: Cap) = let cont = index.root.extend(pattern) analysis = analyse pattern - constValMap = cont.getLeaves(analysis.constPaths) + constValMap = cont.getLeaves(analysis.presentPaths, analysis.constPaths) leaf = constValMap.getLeaf(analysis.constValues) endpoints = leaf.getEndpoints(analysis.capturePaths) # TODO if endpoints.cachedCaptures.len > 0: diff --git a/syndicate.nimble b/syndicate.nimble index 8d37e65..bafb831 100644 --- a/syndicate.nimble +++ b/syndicate.nimble @@ -1,6 +1,6 @@ # Package -version = "20240408" +version = "20240421" author = "Emery Hemingway" description = "Syndicated actors for conversational concurrency" license = "Unlicense" diff --git a/tests/test_patterns.nim b/tests/test_patterns.nim index 9a557d0..18f3103 100644 --- a/tests/test_patterns.nim +++ b/tests/test_patterns.nim @@ -1,103 +1,87 @@ # SPDX-FileCopyrightText: ☭ Emery Hemingway # SPDX-License-Identifier: Unlicense -import std/[options, tables, unittest] +import std/[options, sequtils, tables, unittest] -import preserves, syndicate, syndicate/protocols/gatekeeper +import preserves, syndicate, syndicate/protocols/[gatekeeper, timer] import ./test_schema -test "patterns": - let - pat = ?Observe(pattern: !Foo) ?? {0: grab()} - text = """ > <_> <_>]>]> <_>]>""" - check($pat == text) +suite "example": + var pat: Pattern + check pat.fromPreserves parsePreserves""" + { + 0: + 1: { + 0: > + 1: <_> + }>> + 2: <_> + }> + """ - let - worte = @["alles", "in", "ordnung"] - observer = Observe(pattern: inject(?:Foo, { 0: ?worte })).toPreserves - have = capture(pat, observer).toPreserves.unpackLiterals - want = [worte.toPreserves].toPreserves - check(have == want) + const A = "[1 2 3]" + test A: + let v = parsePreserves A + check: + not pat.matches(v) -type Obj {.preservesDictionary.} = object - a, b, c: int - -test "dictionaries": - let pat = ?:Obj - var source = initDictionary(Cap) - source["b".toSymbol] = 2.toPreserves - source["c".toSymbol] = 3.toPreserves - source["a".toSymbol] = 1.toPreserves - - let values = capture(pat, source) - check values.len == 3 - check values[0] == 1.toPreserves - check values[1] == 2.toPreserves - check values[2] == 3.toPreserves - -type - File {.preservesDictionary.} = object - name: string - path: string - size: BiggestInt - `type`: string - Files = Table[Symbol, File] - Fields = Table[Symbol, string] - - Request {.preservesRecord: "request".} = object - seq: BiggestInt - fields: Fields - files: Files - -test "literals": - const txt = """ date: notes: title: }> path: size: type: }>}>]>""" - var pr = parsePreserves(txt) - - var capture: Literal[Request] - check capture.fromPreserves(pr) - -suite "captures": - for txt in [ - "#f", - "#t", - "0", - "-1", - "foo", - "", - "[0, 1, 2]", - ]: - test txt: - let - pr = parsePreserves txt - pat = grab pr - checkpoint $pat - check pat.matches pr - -suite "protocol": - test "Observe": - let pat = ?:Observe - const text = """> >]>""" - check $pat == text - - test "later-than": + const B = "[1 [2 3] 4]" + test B: let - obsA = parsePreserves"""]> #f>""" - obsB = parsePreserves""" >]>]>]> <_>]> #f>""" - patA = """]>""".parsePreserves.preservesTo(Pattern).get - patB = """ >]>]>]> <_>]>""".parsePreserves.preservesTo(Pattern).get + v = parsePreserves B + c = parsePreserves "[[2 3] 2]" + check pat.matches(v) + check pat.capture(v).toPreserves == c - patC = grab obsA + const C = "[1 [2] 5]" + test C: + let v = parsePreserves C + check: + not pat.matches(v) - test $patC: - check patC.matches obsA - - test $patB: - checkpoint $obsA - check patB.matches obsA - - test "TransportConnection": + const D = "[1 [2 3 4] 5]" + test D: let - pat = TransportConnection ?: { 2: ?:Rejected} - text = """ <_> >]>]>""" - check $pat == text + v = parsePreserves D + c = parsePreserves "[[2 3 4] 2]" + check pat.matches(v) + check pat.capture(v).toPreserves == c + + const E = "[1 [ ] []]" + test E: + let + v = parsePreserves E + c = parsePreserves "[[ ] ]" + check pat.matches(v) + check pat.capture(v).toPreserves == c + +suite "meta": + + test "pattern-of-pattern": + let + pat = grabRecord("foo".toSymbol, {666: drop()}) + meta = pat.toPreserves.drop() + check $meta == " {0: {0: }> 1: {666: <_>}>}>" + + test "observe": + let + val = Observe(pattern: LaterThan ?: {0: drop 12.24}).toPreserves + pat = grab(val) + check pat.matches(val) + check pat.capture(val) == @[val] + let + meta = observePattern(!LaterThan, {@[0.toPreserves]: grabLit()}) + res = parsePreserves "[12.24]" + check meta.matches(val) + check meta.capture(val).toPreserves == res + + test "connect-transport": + let pat = parsePreserves""" + {0: {0: }> 2: {0: >}>}> + """.preservesTo(Pattern).get + let val = parsePreserves""" + #:#f > + """ + check pat.matches(val) + check pat.capture(val).toPreserves == parsePreserves "[#:#f]" diff --git a/tests/test_timers.nim b/tests/test_timers.nim index fc8f8e7..5184a7e 100644 --- a/tests/test_timers.nim +++ b/tests/test_timers.nim @@ -2,7 +2,9 @@ # SPDX-License-Identifier: Unlicense import std/times -import syndicate, syndicate/drivers/timers +import syndicate, syndicate/drivers/timers, preserves + +var passCount = 0 runActor("timer-test") do (turn: var Turn): let timers = newDataspace(turn) @@ -10,13 +12,21 @@ runActor("timer-test") do (turn: var Turn): onPublish(turn, timers, ?LaterThan(seconds: 1356100000)): echo "now in 13th bʼakʼtun" + inc passCount after(turn, timers, initDuration(seconds = 3)) do (turn: var Turn): echo "third timer expired" - stopActor(turn) + assert passCount == 3 + inc passCount after(turn, timers, initDuration(seconds = 1)) do (turn: var Turn): echo "first timer expired" + assert passCount == 1 + inc passCount after(turn, timers, initDuration(seconds = 2)) do (turn: var Turn): echo "second timer expired" + assert passCount == 2 + inc passCount + +doAssert passCount == 4, $passCount