diff --git a/lib/pure/matching.nim b/lib/pure/matching.nim new file mode 100644 index 0000000000000..949a3932058e9 --- /dev/null +++ b/lib/pure/matching.nim @@ -0,0 +1,2578 @@ +import std/[ + sequtils, macros, tables, options, strformat, strutils, + parseutils, algorithm, hashes +] + +export options + +runnableExamples: + {.experimental: "caseStmtMacros".} + + case [(1, 3), (3, 4)]: + of [(1, @a), _]: + echo a + + else: + echo "Match failed" + + +## .. include:: matching.rst + +const + nnkStrKinds* = { + nnkStrLit .. nnkTripleStrLit + } ## Set of all nim node kinds for string nodes + + nnkIntKinds* = { + nnkCharLit .. nnkUInt64Lit + } ## Set of all nim node kinds for integer literal nodes + + nnkFloatKinds* = { + nnkFloatLit .. nnkFloat128Lit + } ## Set of all nim node kinds for float literal nodes + + nnkIdentKinds* = { + nnkIdent, nnkSym, nnkOpenSymChoice + } ## Set of all nim node kinds for identifier-like nodes + + nnkTokenKinds* = + nnkStrKinds + nnkIntKinds + nnkFloatKinds + + nnkIdentKinds + {nnkEmpty} + ## Set of all token-like nodes (primitive type literals or + ## identifiers) + + +const debugWIP = false + +template echov(arg: untyped): untyped {.used.} = + {.noSideEffect.}: + when debugWIP: + let val = $arg + if split(val).len > 1: + echo instantiationInfo().line, " \e[32m", astToStr(arg), "\e[39m " + echo val + + else: + echo instantiationInfo().line, " \e[32m", astToStr(arg), "\e[39m ", val + +template varOfIteration*(arg: untyped): untyped = + when compiles( + for item in items(arg): + discard + ): + (( + # Hack around `{.requiresinit.}` + block: + var tmp2: ref typeof(items(arg), typeOfIter) + let tmp = tmp2[] + tmp + )) + + else: + proc aux(): auto = + for val in arg: + return val + + var tmp2: ref typeof(aux()) + let tmp1 = tmp2[] + tmp1 + +func codeFmt(str: string): string {.inline.} = + &"\e[4m{str}\e[24m" + +func codeFmt(node: NimNode): NimNode {.inline.} = + node.strVal().codeFmt().newLit() + +func toPattStr(node: NimNode): NimNode = + var tmp = node.toStrLit().strVal() + if split(tmp, '\n').len > 1: + tmp = "\n" & tmp.split('\n').mapIt(" " & it).join("\n") & "\n\n" + + else: + tmp = codeFmt(tmp) & ". " + + newLit(tmp) + + +func nodeStr(n: NimNode): string = + ## Get nim node string value from any identifier or string literal node + case n.kind: + of nnkIdent: n.strVal() + of nnkOpenSymChoice: n[0].strVal() + of nnkSym: n.strVal() + of nnkStrKinds: n.strVal() + else: raiseAssert(&"Cannot get string value from node kind {n.kind}") + +func lineIInfo(node: NimNode): NimNode = + ## Create tuple literal for `{.line: .}` pragma + let iinfo = node.lineInfoObj() + newLit((filename: iinfo.filename, line: iinfo.line)) + + + +func idxTreeRepr(inputNode: NimNode, maxLevel: int = 120): string = + func aux(node: NimNode, parent: seq[int]): seq[string] = + result.add parent.mapIt(&"[{it}]").join("") & + " ".repeat(6) & + ($node.kind)[3..^1] & + (if node.len == 0: " " & node.toStrLit().nodeStr() else: "") + + for idx, subn in node: + if parent.len + 1 < maxLevel: + result &= aux(subn, parent & @[idx]) + else: + result &= (parent & @[idx]).mapIt(&"[{it}]").join("") & + " ".repeat(6 + 3 + 3) & "[...] " & ($subn.kind)[3..^1] + + return aux(inputNode, @[]).join("\n") + + + + +template getSome[T](opt: Option[T], injected: untyped): bool = + opt.isSome() and ((let injected {.inject.} = opt.get(); true)) + +func splitDots(n: NimNode): seq[NimNode] = + ## Split nested `DotExpr` node into sequence of nodes. `a.b.c -> @[a, b, c]` + result = case n.kind: + of nnkDotExpr: + if n[0].kind == nnkDotExpr: + splitDots(n[0]) & @[n[1]] + elif n[0].kind == nnkBracketExpr: + splitDots(n[0]) & splitDots(n[1]) + else: + @[n[0], n[1]] + of nnkBracketExpr: + if n[0].kind in nnkIdentKinds: + @[n[0]] & splitDots(n[1]).mapIt(nnkBracket.newTree(it)) + else: + n[0][0].splitDots() & ( + n[0][1].splitDots() & n[1].splitDots() + ).mapIt(nnkBracket.newTree(it)) + else: + @[n] + +func firstDot(n: NimNode): NimNode {.inline.} = + splitDots(n)[0] + +template assertKind(node: NimNode, kindSet: set[NimNodeKind]): untyped = + if node.kind notin kindSet: + raiseAssert("Expected one of " & $kindSet & " but node has kind " & + $node.kind & " (assertion on " & $instantiationInfo() & ")") + +func startsWith(n: NimNode, str: string): bool = + n.nodeStr().startsWith(str) + + + +func parseEnumField(fld: NimNode): string = + ## Get name of enum field from nim node + case fld.kind: + of nnkEnumFieldDef: + fld[0].nodeStr + of nnkSym: + fld.nodeStr + else: + raiseAssert(&"Cannot parse enum field for kind: {fld.kind}") + +func parseEnumImpl(en: NimNode): seq[string] = + ## Get sequence of enum value names + case en.kind: + of nnkSym: + let impl = en.getTypeImpl() + case impl.kind: + of nnkBracketExpr: + return parseEnumImpl(impl.getTypeInst()[1].getImpl()) + of nnkEnumTy: + result = parseEnumImpl(impl) + else: + assertKind(impl, {nnkBracketExpr, nnkEnumTy}) + # raiseAssert(&"#[ IMPLEMENT {impl.kind} ]#") + of nnkTypeDef: + result = parseEnumImpl(en[2]) + of nnkEnumTy: + for fld in en[1..^1]: + result.add parseEnumField(fld) + of nnkTypeSection: + result = parseEnumImpl(en[0]) + else: + raiseAssert(&"Cannot parse enum element for kind {en.kind}") + + +func pref(name: string): string = + discard name.parseUntil(result, {'A' .. 'Z', '0' .. '9'}) + +func foldInfix( + s: seq[NimNode], + inf: string, start: seq[NimNode] = @[]): NimNode = + + if inf == "or" and s.len > 0 and ( + s[0].eqIdent("true") or s[0] == newLit(true) + ): + result = newLit(true) + + else: + result = ( start & s ).mapIt(it.newPar().newPar()).foldl( + nnkInfix.newTree(ident inf, a, b)) + + +func commonPrefix(strs: seq[string]): string = + ## Find common prefix for seq of strings + if strs.len == 0: + return "" + else: + let strs = strs.sorted() + for i in 0 ..< min(strs[0].len, strs[^1].len): + if strs[0][i] == strs[^1][i]: + result.add strs[0][i] + else: + return + + +func dropPrefix(str: string, alt: string): string = + if str.startsWith(alt): + return str[min(alt.len, str.len)..^1] + return str + +func dropPrefix(ss: seq[string], patt: string): seq[string] = + for s in ss: + result.add s.dropPrefix(patt) + + +template findItFirstOpt*(s: typed, op: untyped): untyped = + var res: Option[typeof(s[0])] + for it {.inject.} in s: + if op: + res = some(it) + break + + res + + +func addPrefix*(str, pref: string): string = + if not str.startsWith(pref): pref & str else: str + +func hash(iinfo: LineInfo): Hash = + !$(iinfo.line.hash !& iinfo.column.hash !& iinfo.filename.hash) + +proc getKindNames*(head: NimNode): (string, seq[string]) = + var + pref: string + names: seq[string] + cache {.global.}: Table[LineInfo, (string, seq[string])] + + block: + let + impl = head.getTypeImpl() + iinfo = impl.lineInfoObj() + + if iinfo notin cache: + let + decl = impl.parseEnumImpl() + pref = decl.commonPrefix().pref() + + cache[iinfo] = (pref, decl.dropPrefix(pref)) + + pref = cache[iinfo][0] + names = cache[iinfo][1] + + return (pref, names) + + + +macro hasKindImpl*(head: typed, kind: untyped): untyped = + let (pref, names) = getKindNames(head) + kind.assertKind({nnkIdent}) + let str = kind.toStrLit().nodeStr().addPrefix(pref) + let kind = ident(str) + if not names.anyIt(eqIdent(it.addPrefix(pref), str)): + error("Invalid kind name - " & kind.toStrLit().strVal(), kind) + + result = nnkInfix.newTree(ident "==", head, kind) + +template hasKind*(head, kindExpr: untyped): untyped = + ## Determine if `head` has `kind` value. Either function/procedure + ## `kind` or field with the same name is expected to be declared. + ## Type of `kind` must be an enum. Kind expression is a pattern + ## describing expected values. Possible examples of pattern + ## (assuming value of type `NimNode` is used as `head`) + ## + ## - `nnkIntLit` - match integer literal + ## - `IntLit` - alternative (preferred) syntax for matching enum values + ## `nnk` prefix can be omitted. + when compiles(head.kind): + hasKindImpl(head.kind, kindExpr) + + elif compiles(head is kindExpr): + true + + else: + static: error "No `kind` defined for " & $typeof(head) + +when (NimMajor, NimMinor, NimPatch) >= (1, 4, 2): + type FieldIndex* = distinct int + func `==`*(idx: FieldIndex, i: SomeInteger): bool = idx.int == i + template `[]`*(t: tuple, idx: static[FieldIndex]): untyped = + t[idx.int] + +else: + type FieldIndex* = int + + +type + MatchKind* = enum + ## Different kinds of matching patterns + kItem ## Match single element + kSeq ## Match sequence of elements + kTuple ## Mach tuple (anonymous or named) + kPairs ## Match key-value pairs + kObject ## Match object, named tuple or object-like value + kSet ## Match set of elements + kAlt ## Ordered choice - mactch any of patterns. + + SeqKeyword* = enum + ## Possible special words for seq pattern matching + lkAny = "any" ## Any element from seq + lkAll = "all" ## All elements from seq + lkNone = "none" ## None of the elements from seq + lkOpt = "opt" ## Optionaly match element in seq + lkUntil = "until" ## All elements until + lkPref = "pref" ## All elements while + lkPos ## Exact position + lkSlice ## Subrange slice + lkTrail ## Variadic placeholder `.._` + + SeqStructure* = object + decl: NimNode ## Original declaration of the node + bindVar*: Option[NimNode] ## Optional bound variable + patt*: Match ## Patterh for element matching + case kind*: SeqKeyword + of lkSlice: + slice*: NimNode + else: + discard + + ItemMatchKind* = enum + ## Type of item pattern match + imkInfixEq ## Match item using infix operator + imkSubpatt ## Match item by checking it agains subpattern + imkPredicate ## Execute custom predicate to determine if element + ## matches pattern. + + KVPair* = object + key: NimNode + patt: Match + + MatchError* = ref object of CatchableError ## Exception indicating match failure + + + Match* = ref object + ## Object describing single match for element + bindVar*: Option[NimNode] ## Bound variable (if any) + declNode*: NimNode ## Original declaration of match + isOptional*: bool + fallback*: Option[NimNode] ## Default value in case match fails + case kind*: MatchKind + of kItem: + case itemMatch: ItemMatchKind + of imkInfixEq: + infix*: string ## Infix operator used for comparison + rhsNode*: NimNode ## Rhs expression to compare against + isPlaceholder*: bool ## Always true? `_` pattern is an + ## infix expression with `isPlaceholder` equal to true + of imkSubpatt: + rhsPatt*: Match ## Subpattern to compare value against + of imkPredicate: + isCall*: bool ## Predicate is a call expression + ## (`@val.matches()`) or a free-standing expression + ## (`@val(it.len < 100)`) + predBody*: NimNode ## Body of the expression + + of kAlt: + altElems*: seq[Match] ## Alternatives for matching + of kSeq: + seqElems*: seq[SeqStructure] ## Sequence subpatterns + of kTuple: + tupleElems*: seq[Match] ## Tuple elements + of kPairs: + pairElems*: seq[KVPair] + nocheck*: bool + + of kSet: + setElems*: seq[Match] + + of kObject: + kindCall*: Option[NimNode] ## Optional node with kind + ## expression pattern (see `hasKind`) + isRefKind*: bool + fldElems*: seq[tuple[ + name: string, + patt: Match + ]] + + kvMatches*: Option[Match] ## Optional key-value matches for + ## expressions like `JObject({"key": @val})` + seqMatches*: Option[Match] ## Optional indexed matches for + ## subelement access using `Infix([@op, @lhs, @rhs])` pattern. + + AccsElem = object + isVariadic: bool + case inStruct: MatchKind + of kSeq: + pos*: NimNode ## Expressions for accessing seq element + of kTuple: + idx*: int ## Tuple field index + of kObject: + fld*: string ## Object field name + of kPairs: + key*: NimNode ## Expression for key-value pair + nocheck*: bool + of kSet: + discard + of kAlt: + altIdx*: int + altMax*: int + of kItem: + isOpt*: bool ## Is match optional + + Path = seq[AccsElem] + + VarKind* = enum + ## Kind of matched variables + vkRegular ## Regular variable, assigned once + vkSequence + vkOption + vkSet + vkAlt + + AltSpec = object + altMax: int16 ## Max alterantive index + altPositions: set[int16] ## Previous positions for index + completed: bool ## Completed index + + VarSpec* = object + decl*: NimNode ## First time variable has been declared + case varKind*: VarKind ## Type of the variable + of vkAlt: + prefixMap*: Table[Path, AltSpec] + else: + nil + + typePath*: Path ## Whole path for expression that can be used to + ## determine type of the variable. + cnt*: int ## Number of variable occurencies in expression + + VarTable = Table[string, VarSpec] + +func hash(accs: AccsElem): Hash = + var h: Hash = 0 + h = h !& hash(accs.isVariadic) + case accs.inStruct: + of kSeq: + h = h !& hash(accs.pos.repr) + of kTuple: + h = h !& hash(accs.idx) + of kObject: + h = h !& hash(accs.fld) + of kPairs: + h = h !& hash(accs.key.repr) !& hash(accs.nocheck) + of kAlt: + h = h !& hash(accs.altIdx) !& hash(accs.altMax) + of kItem: + h = h !& hash(accs.isOpt) + of kSet: + discard + + result = !$h + +func `==`(a, b: AccsElem): bool = + a.isVariadic == b.isVariadic and + a.inStruct == b.inStruct and + ( + case a.inStruct: + of kSeq: + a.pos == b.pos + of kTuple: + a.idx == b.idx + of kObject: + a.fld == b.fld + of kPairs: + a.key == b.key and a.nocheck == b.nocheck + of kItem: + a.isOpt == b.isOpt + of kSet: + true + of kAlt: + a.altIdx == b.altIdx and a.altMax == b.altMax + ) + + +func `$`(path: Path): string = + for elem in path: + case elem.inStruct: + of kTuple: + result &= &"({elem.idx})" + of kSeq: + result &= "[pos]" + of kAlt: + result &= &"|{elem.altIdx}/{elem.altMax}|" + of kPairs: + result &= &"[{elem.key.repr}]" + of kSet: + result &= "{}" + of kObject: + result &= &".{elem.fld}" + of kItem: + result &= "#" + + result = "--- " & result + + +func `$`(match: Match): string + +func `$`(kvp: KVPair): string = + &"{kvp.key.repr}: {kvp.patt}" + + +func `$`(ss: SeqStructure): string = + if ss.kind == lkSlice: + result = &"{ss.repr}" + else: + result = $ss.kind + + + if ss.bindVar.getSome(bv): + result &= " " & bv.repr + result &= " " & $ss.patt + + + +func `$`(match: Match): string = + case match.kind: + of kAlt: + result = match.altElems.mapIt($it).join(" | ") + + of kSeq: + result = "[" & match.seqElems.mapIt($it).join(", ") & "]" + + of kTuple: + result = "(" & match.tupleElems.mapIt($it).join(", ") & ")" + + of kPairs: + result = "{" & match.pairElems.mapIt($it).join(", ") & "}" + + of kItem: + case match.itemMatch: + of imkInfixEq: + if match.isPlaceholder: + if match.bindVar.getSome(vn): + result = &"@{vn.repr}" + else: + result = "_" + else: + result = &"{match.infix} {match.rhsNode.repr}" + of imkSubpatt: + result = $match.rhsPatt + of imkPredicate: + result = match.predBody.repr + + of kSet: + result = "{" & match.setElems.mapIt($it).join(", ") & "}" + + of kObject: + var kk: string + if match.kindCall.getSome(kkn): + kk = kkn.repr + + result = &"{kk}(" & match.fldElems.mapIt( + &"{it.name}: {it.patt}").join(", ") + + if match.kvMatches.getSome(kvm): + result &= $kvm + + if match.seqMatches.getSome(sm): + result &= $sm + + result &= ")" + + +func isNamedTuple(node: NimNode): bool = + template implies(a, b: bool): bool = (if a: b else: true) + node.allIt(it.kind in { + nnkExprColonExpr, # `(fld: )` + nnkBracket, # `([])` + nnkTableConstr # `{key: val}` + }) and + node.allIt((it.kind == nnkIdent) .implies (it.nodeStr == "_")) + +func makeVarSet( + varn: NimNode, expr: NimNode, vtable: VarTable, doRaise: bool): NimNode = + varn.assertKind({nnkIdent}) + case vtable[varn.nodeStr()].varKind: + of vkSequence: + return quote do: + `varn`.add `expr` ## Append item to sequence + true + + of vkOption: + return quote do: + `varn` = some(`expr`) ## Set optional value + true + + of vkSet: + return quote do: + `varn`.incl some(`expr`) ## Add element to set + true + + of vkRegular: + let wasSet = ident(varn.nodeStr() & "WasSet") + let varStr = varn.toStrLit() + let ln = lineIInfo(vtable[varn.nodeStr()].decl) + let matchError = + if doRaise and not debugWIP: + quote do: + when compiles($(`varn`)): + {.line: `ln`.}: + raise MatchError( + msg: "Match failure: variable '" & `varStr` & + "' is already set to " & $(`varn`) & + ", and does not match with " & $(`expr`) & "." + ) + else: + {.line: `ln`.}: + raise MatchError( + msg: "Match failure: variable '" & `varStr` & + "' is already set and new value does not match." + ) + else: + quote do: + discard false + + + if vtable[varn.nodeStr()].cnt > 1: + return quote do: + if `wasSet`: + if `varn` == `expr`: + true + else: + if true: + `matchError` + false + else: + `varn` = `expr` + `wasSet` = true + true + else: + return quote do: + `varn` = `expr` + true + + of vkAlt: + return quote do: # WARNING - for now I assume unification with + # alternative variables is not supported, but + # this might be either declared as invalid + # (most likely) or handled via some kind of + # convoluted logic that is yet to be + # determined. + `varn` = some(`expr`) + true + +func toAccs*(path: Path, name: NimNode, pathForType: bool): NimNode = + ## Convert path in object to expression for getting element at path. + func aux(prefix: NimNode, top: Path): NimNode = + let head = top[0] + result = case head.inStruct: + of kSeq: + if pathForType: + newCall("varOfIteration", prefix) + else: + nnkBracketExpr.newTree(prefix, top[0].pos) + + of kTuple: + nnkBracketExpr.newTree( + prefix, newCall("FieldIndex", newLit(top[0].idx))) + + of kObject: + nnkDotExpr.newTree(prefix, ident head.fld) + + of kPairs: + nnkBracketExpr.newTree(prefix, head.key) + + of kItem, kAlt: + prefix + + of kSet: + raiseAssert( + "Invalid access path: cannot create explicit access for set") + + if top.len > 1: + result = result.aux(top[1 ..^ 1]) + + + result = + if path.len > 0: + name.aux(path) + else: + name + + +func parseMatchExpr*(n: NimNode): Match + +func parseNestedKey(n: NimNode): Match = + ## Unparse key-value pair with nested fields. `fld: ` and + ## `fld1.subfield.subsubfield: `. Lattern one is just + ## shorthand for `(fld1: (subfield: (subsubfield: )))`. + ## This function returns `(subfield: (subsubfield: ))` part + ## - first key should be handled by caller. + n.assertKind({nnkExprColonExpr}) + func aux(spl: seq[NimNode]): Match = + case spl[0].kind: + of nnkIdentKinds: + if spl.len == 1: + return n[1].parseMatchExpr() + else: + if spl[1].kind in nnkIdentKinds: + return Match( + kind: kObject, + declNode: spl[0], + fldElems: @[ + (name: spl[1].nodeStr(), patt: aux(spl[1 ..^ 1])) + ]) + else: + return Match( + kind: kPairs, + declNode: spl[0], + pairElems: @[KvPair( + key: spl[1][0], patt: aux(spl[1 ..^ 1]))], + nocheck: true + ) + of nnkBracket: + if spl.len == 1: + return n[1].parseMatchExpr() + else: + if spl[1].kind in nnkIdentKinds: + return Match( + kind: kObject, + declNode: spl[0], + fldElems: @[ + (name: spl[1].nodeStr(), patt: aux(spl[1 ..^ 1])) + ]) + else: + return Match( + kind: kPairs, + declNode: spl[1], + pairElems: @[KvPair( + key: spl[1][0], patt: aux(spl[1 ..^ 1]))], + nocheck: true + ) + else: + error( + "Malformed path access - expected either field name, " & + "or bracket access ['key'], but found " & + spl[0].toStrLit().strVal() & + " of kind " & $spl[0].kind, + spl[0] + ) + + return aux(n[0].splitDots()) + + + +func parseKVTuple(n: NimNode): Match = + ## Parse key-value tuple for object access - object or tuple fields. + if n[0].eqIdent("Some"): + # Special case for `Some(@var)` - expanded into `isSome` check and some + # additional cruft + if not (n.len <= 2): + error("Expected `Some()`", n) + + # n[1].assertKind({nnkPrefix}) + + result = Match(kind: kObject, declNode: n, fldElems: @{ + "isSome": Match(kind: kItem, itemMatch: imkInfixEq, declNode: n[0], + rhsNode: newLit(true), infix: "==") + }) + + if n.len > 1: + result.fldElems.add ("get", parseMatchExpr(n[1])) + + return + + elif n[0].eqIdent("None"): + return Match(kind: kObject, declNode: n, fldElems: @{ + "isNone": Match(kind: kItem, itemMatch: imkInfixEq, declNode: n[0], + rhsNode: newLit(true), infix: "==") + }) + + result = Match(kind: kObject, declNode: n) + var start = 0 # Starting subnode for actual object fields + if n.kind in {nnkCall, nnkObjConstr}: + start = 1 + result.kindCall = some(n[0]) + + for elem in n[start .. ^1]: + case elem.kind: + of nnkExprColonExpr: + var str: string + case elem[0].kind: + of nnkIdentKinds, nnkDotExpr, nnkBracketExpr: + let first = elem[0].firstDot() + if first.kind == nnkIdent: + str = first.nodeStr() + + else: + error( + "First field access element must be an identifier. " & + "For accessing int-indexable objects use [] subscript " & + "directly, like (" & first.repr & ")", + first + ) + + else: + error( + "Malformed path access - expected either field name, " & + "or bracket access, but found '" & + elem[0].toStrLit().strVal() & "'" & + " of kind " & $elem[0].kind, + elem[0] + ) + + result.fldElems.add((str, elem.parseNestedKey())) + + of nnkBracket, nnkStmtList: + # `Bracket` - Special case for object access - allow omission of + # parentesis, so you can write `ForStmt[@a, @b]` (which is very + # useful when working with AST types) + # + # `StmtList` - second special case for writing list patterns, + # allows to use treeRepr-like code. + result.seqMatches = some(elem.parseMatchExpr()) + + of nnkTableConstr: + # Special case for matching key-value pairs (tables and other + # objects implementing `contains` and `[]` operator) + result.kvMatches = some(elem.parseMatchExpr()) + + else: + elem.assertKind({nnkExprColonExpr}) + +func contains(kwds: openarray[SeqKeyword], str: string): bool = + for kwd in kwds: + if eqIdent($kwd, str): + return true + +func parseSeqMatch(n: NimNode): seq[SeqStructure] = + for elem in n: + if elem.kind == nnkPrefix and elem[0].eqIdent(".."): + elem[1].assertKind({nnkIdent}) + result.add SeqStructure(kind: lkTrail, patt: Match( + declNode: elem, + ), decl: elem) + + elif + # `^1 is ` + elem.kind == nnkInfix and elem[1].kind == nnkPrefix and + elem[0].eqIdent("is") and elem[1][0].eqIdent("^") and + elem[1][1].kind in nnkIntKinds + : + + var res = SeqStructure( + kind: lkSlice, slice: elem[1], decl: elem, + patt: parseMatchExpr(elem[2]), + ) + + res.bindVar = res.patt.bindVar + res.patt.bindVar = none(NimNode) + result.add res + + + elif + # `1 is ` + elem.kind == nnkInfix and + elem[1].kind in nnkIntKinds and + elem[0].eqIdent("is") + : + + var res = SeqStructure( + kind: lkSlice, slice: elem[1], decl: elem, + patt: parseMatchExpr(elem[2]), + ) + + res.bindVar = res.patt.bindVar + res.patt.bindVar = none(NimNode) + result.add res + + elif + # `[0 .. 3 @head is Jstring()]` + (elem.kind == nnkInfix and (elem[0].startsWith(".."))) or + # `[(0 .. 3) @head is Jstring()]` + (elem.kind == nnkCommand and elem[0].kind == nnkPar) or + # `[0 .. 2 is 12]` + (elem.kind == nnkInfix and + elem[1].kind == nnkInfix and + elem[1][0].startsWith("..") + ): + + var dotInfix, rangeStart, rangeEnd, body: NimNode + + if elem.kind == nnkInfix: + if elem.kind == nnkInfix and elem[1].kind == nnkInfix: + # `0 .. 2 is 12` + # Infix + # [0] Ident is + # [1] Infix + # [1][0] [...] Ident + # [1][1] [...] IntLit + # [1][2] [...] IntLit + # [2] IntLit 12 + dotInfix = elem[1][0] + rangeStart = elem[1][1] + rangeEnd = elem[1][2] + body = elem[2] + else: + # `0 .. 2 @a is 12` + # Infix + # [0] Ident .. + # [1] IntLit 0 + # [2] Command + # [2][0] IntLit 2 + # [2][1] Infix + # [2][1][0] [...] Ident + # [2][1][1] [...] Prefix + # [2][1][2] [...] IntLit + dotInfix = ident elem[0].nodeStr() + rangeStart = elem[1] + rangeEnd = elem[2][0] + body = elem[2][1] + + elif elem.kind == nnkCommand: + # I wonder, why do we need pattern matching in stdlib? + dotInfix = ident elem[0][0][0].nodeStr() + rangeStart = elem[0][0][1] + rangeEnd = elem[0][0][1] + body = elem[1] + # elif elem.kind == nnkInfix and : + + var res = SeqStructure( + kind: lkSlice, slice: nnkInfix.newTree( + dotInfix, + rangeStart, + rangeEnd + ), + patt: parseMatchExpr(body), + decl: elem + ) + + res.bindVar = res.patt.bindVar + res.patt.bindVar = none(NimNode) + result.add res + + else: + func toKwd(node: NimNode): SeqKeyword = + for (key, val) in { + "any" : lkAny, + "all" : lkAll, + "opt" : lkOpt, + "until" : lkUntil, + "none" : lkNone, + "pref" : lkPref + }: + if node.eqIdent(key): + result = val + break + + + let topElem = elem + var (elem, opKind) = (elem, lkPos) + let seqKwds = [lkAny, lkAll, lkNone, lkOpt, lkUntil, lkPref] + if elem.kind in {nnkCall, nnkCommand} and + elem[0].kind in {nnkSym, nnkIdent} and + elem[0].nodeStr() in seqKwds: + opKind = toKwd(elem[0]) + elem = elem[1] + elif elem.kind in {nnkInfix} and + elem[1].kind in {nnkIdent}: + + if elem[1].nodeStr() in seqKwds: + opKind = toKwd(elem[1]) + elem = nnkInfix.newTree(elem[0], ident "_", elem[2]) + + else: + if not elem[1].eqIdent("_"): + error("Invalid node match keyword - " & + elem[1].repr.codeFmt() & "",elem[1]) + + var + match = parseMatchExpr(elem) + bindv = match.bindVar + + if opKind != lkPos: + match.bindVar = none(NimNode) + + match.isOptional = opKind in {lkOpt} + + var it = SeqStructure(bindVar: bindv, kind: opKind, decl: topElem) + it.patt = match + result.add(it) + +func parseTableMatch(n: NimNode): seq[KVPair] = + for elem in n: + result.add(KVPair( + key: elem[0], + patt: elem[1].parseMatchExpr() + )) + +func parseAltMatch(n: NimNode): Match = + let + lhs = n[1].parseMatchExpr() + rhs = n[2].parseMatchExpr() + + var alts: seq[Match] + if lhs.kind == kAlt: alts.add lhs.altElems else: alts.add lhs + if rhs.kind == kAlt: alts.add rhs.altElems else: alts.add rhs + result = Match(kind: kAlt, altElems: alts, declNode: n) + +func splitOpt(n: NimNode): tuple[ + lhs: NimNode, rhs: Option[NimNode]] = + + n[0].assertKind({nnkIdent}) + if not n[0].eqIdent("opt"): + error("Only `opt` is supported for standalone item matches", n[0]) + + if not n.len == 2: + error("Expected exactly one parameter for `opt`", n) + + if n[1].kind == nnkInfix: + result.lhs = n[1][1] + result.rhs = some n[1][2] + else: + result.lhs = n[1] + +func isBrokenBracket(n: NimNode): bool = + result = ( + n.kind == nnkCommand and + n[1].kind == nnkBracket + ) or + ( + n.kind == nnkCommand and + n[1].kind == nnkInfix and + n[1][1].kind == nnkBracket and + n[1][2].kind == nnkCommand + ) + +func fixBrokenBracket(inNode: NimNode): NimNode = + + func aux(n: NimNode): NimNode = + if n.kind == nnkCommand and n[1].kind == nnkBracket: + # `A [1] -> A[1]` + result = nnkBracketExpr.newTree(n[0]) + for arg in n[1]: + result.add arg + else: + # It is possible to have something else ony if second part is + # infix, + + # `Par [_] | Par [_]` gives ast like this (paste below). It ts + # necessary to transform it into `Par[_] | Par[_]` - move infix + # up in the AST and convert all brackets into bracket + # expressions. + + #``` + # Command + # [0] Ident Par + # [1] Infix + # [1][0] Ident | + # [1][1] Bracket + # [1][1][0] Ident _ + # [1][2] Command + # [1][2][0] Ident Par + # [1][2][1] Bracket + # [1][2][1][0] Ident _ + #``` + var brac = nnkBracketExpr.newTree(n[0]) # First bracket head + + for arg in n[1][1]: + brac.add arg + + result = nnkInfix.newTree( + n[1][0], # Infix indentifier + brac, + aux(n[1][2]) # Everything else is handled recursively + ) + + + result = aux(inNode) + +func isBrokenPar(n: NimNode): bool = + result = ( + n.kind == nnkCommand and + n[1].kind == nnkPar + ) + + + +func fixBrokenPar(inNode: NimNode): NimNode = + func aux(n: NimNode): NimNode = + result = nnkCall.newTree(n[0]) + + for arg in n[1]: + result.add arg + + + result = aux(inNode) + +macro dumpIdxTree(n: untyped) {.used.} = + echo n.idxTreeRepr() + +func parseMatchExpr*(n: NimNode): Match = + ## Parse match expression from nim node + + # echov "Parse match expression" + # echov n.idxTreeRepr() + case n.kind: + of nnkIdent, nnkSym, nnkIntKinds, nnkStrKinds, nnkFloatKinds: + result = Match(kind: kItem, itemMatch: imkInfixEq, declNode: n) + if n == ident "_": + result.isPlaceholder = true + + else: + result.rhsNode = n + result.infix = "==" + + of nnkPar: # Named or unnamed tuple + if n.isNamedTuple(): # `(fld1: ...)` + result = parseKVTuple(n) + + elif n[0].kind == nnkInfix and n[0][0].eqIdent("|"): + result = parseAltMatch(n[0]) + + else: # Unnamed tuple `( , , , , )` + if n.len == 1: # Tuple with single argument is most likely used as + # regular parens in order to change operator + # precendence. + + result = parseMatchExpr(n[0]) + else: + result = Match(kind: kTuple, declNode: n) + for elem in n: + result.tupleElems.add parseMatchExpr(elem) + + of nnkPrefix: # `is Patt()`, `@capture` or other prefix expression + if n[0].nodeStr() in ["is", "of"]: # `is Patt()` + result = Match( + kind: kItem, itemMatch: imkSubpatt, + rhsPatt: parseMatchExpr(n[1]), declNode: n) + + if n[0].nodeStr() == "of" and result.rhsPatt.kind == kObject: + result.rhsPatt.isRefKind = true + + elif n[0].nodeStr() == "@": # `@capture` + n[1].assertKind({nnkIdent}) + result = Match( + kind: kItem, + itemMatch: imkInfixEq, + isPlaceholder: true, + bindVar: some(n[1]), + declNode: n + ) + + else: # Other prefix expression, for example `== 12` + result = Match( + kind: kItem, itemMatch: imkInfixEq, infix: n[0].nodeStr(), + rhsNode: n[1], declNode: n + ) + + of nnkBracket, nnkStmtList: + # `[1,2,3]` - seq pattern in inline form or as seq of elements + # (stmt list) + result = Match( + kind: kSeq, seqElems: parseSeqMatch(n), declNode: n) + + of nnkTableConstr: # `{"key": "val"}` - key-value matches + result = Match( + kind: kPairs, pairElems: parseTableMatch(n), declNode: n) + + of nnkCurly: # `{1, 2}` - set pattern + result = Match(kind: kSet, declNode: n) + for node in n: + if node.kind in {nnkExprColonExpr}: + error("Unexpected colon", node) + + case node.kind: + of nnkIntKinds, nnkIdent, nnkSym: + # Regular set element, `{1, 2}`, possibly with enum idents + result.setElems.add Match( + kind: kItem, + itemMatch: imkInfixEq, + rhsNode: node, + declNode: node + ) + + of nnkInfix: + if not node[0].eqIdent(".."): + error( + "Set patter expects infix `..`, but found " & node[0].repr, node) + + else: + result.setElems.add Match( + kind: kItem, + itemMatch: imkInfixEq, + rhsNode: node, + declNode: node + ) + + else: + error( + &"Unexpected node kind in set pattern - {node.kind}", node) + + + of nnkBracketExpr: + result = Match( + kindCall: some n[0], + kind: kObject, + declNode: n, + seqMatches: some parseMatchExpr( + nnkBracket.newTree(n[1..^1]) + ) + ) + + elif n.kind in {nnkObjConstr, nnkCall, nnkCommand} and + not n[0].eqIdent("opt"): + # echov n.idxTreeRepr() + if n.isBrokenBracket(): + # Broken bracket expression that was written as `A [1]` and + # subsequently parsed into + # `(Command (Ident "A") (Bracket (IntLit 1)))` + # when actually it was ment to be used as `A[1]` + # `(BracketExpr (Ident "A") (IntLit 1))` + result = parseMatchExpr(n.fixBrokenBracket()) + + elif n.isBrokenPar(): + result = parseMatchExpr(n.fixBrokenPar()) + + else: + if n[0].kind == nnkPrefix: + n[0][1].assertKind({nnkIdent}) # `@capture()` + result = Match( + kind: kItem, + itemMatch: imkPredicate, + bindVar: some(n[0][1]), + declNode: n, + predBody: n[1] + ) + + elif n[0].kind == nnkDotExpr: + var body = n + var bindVar: Option[NimNode] + if n[0][0].kind == nnkPrefix: + n[0][0][1].assertKind({nnkIdent}) + bindVar = some(n[0][0][1]) + + # Store name of the bound variable and then replace `_` with + # `it` to make `it.call("arguments")` + body[0][0] = ident("it") + + else: # `_.call("Arguments")` + # `(DotExpr (Ident "_") (Ident ""))` + n[0][1].assertKind({nnkIdent, nnkOpenSymChoice}) + n[0][0].assertKind({nnkIdent, nnkOpenSymChoice}) + + # Replace `_` with `it` to make `it.call("arguments")` + body[0][0] = ident("it") + + result = Match( + kind: kItem, + itemMatch: imkPredicate, + declNode: n, + predBody: body, + bindVar: bindVar + + ) + elif n.kind == nnkCall and n[0].eqIdent("_"): + # `_(some < expression)`. NOTE - this is probably a + # not-that-common use case, but I don't think explicitly + # disallowing it will make things more intuitive. + result = Match( + kind: kItem, + itemMatch: imkPredicate, + declNode: n[1], + predBody: n[1] + ) + + elif n.kind == nnkCall and + n.len > 1 and + n[1].kind == nnkStmtList: + + if n[0].kind == nnkIdent: + result = parseKVTuple(n) + + else: + result = parseMatchExpr(n[0]) + result.seqMatches = some(parseMatchExpr(n[1])) + + else: + result = parseKVTuple(n) + + elif (n.kind in {nnkCommand, nnkCall}) and n[0].eqIdent("opt"): + let (lhs, rhs) = splitOpt(n) + result = lhs.parseMatchExpr() + result.isOptional = true + result.fallback = rhs + + elif n.kind == nnkInfix and n[0].eqIdent("|"): + # `(true, true) | (false, false)` + result = parseAltMatch(n) + + elif n.kind in {nnkInfix, nnkPragmaExpr}: + n[1].assertKind({nnkPrefix, nnkIdent, nnkPragma}) + if n[1].kind in {nnkPrefix}: + n[1][1].assertKind({nnkIdent}) + + if n[0].nodeStr() == "is": + # `@patt is JString()` + # `@head is 'd'` + result = Match( + kind: kItem, itemMatch: imkSubpatt, + rhsPatt: parseMatchExpr(n[2]), declNode: n) + + elif n[0].nodeStr() == "of": + result = Match( + kind: kItem, itemMatch: imkSubpatt, + rhsPatt: parseMatchExpr(n[2]), declNode: n) + + if n[0].nodeStr() == "of" and result.rhsPatt.kind == kObject: + result.rhsPatt.isRefKind = true + + else: + # `@a | @b`, `@a == 6` + result = Match( + kind: kItem, itemMatch: imkInfixEq, + rhsNode: n[2], + infix: n[0].nodeStr(), declNode: n) + + if result.infix == "or": + result.isOptional = true + result.fallback = some n[2] + + if n[1].kind == nnkPrefix: # WARNING + result.bindVar = some(n[1][1]) + else: + error( + "Malformed DSL - found " & n.toStrLit().strVal() & + " of kind " & $n.kind & ".", n) + +func isVariadic(p: Path): bool = p.anyIt(it.isVariadic) + +func isAlt(p: Path): bool = + result = p.anyIt(it.inStruct == kAlt) + +iterator altPrefixes(p: Path): Path = + var idx = p.len - 1 + while idx >= 0: + if p[idx].inStruct == kAlt: + yield p[0 .. idx] + dec idx + +func isOption(p: Path): bool = + p.anyIt(it.inStruct == kItem and it.isOpt) + +func classifyPath(path: Path): VarKind = + if path.isVariadic: + vkSequence + elif path.isOption(): + vkOption + elif path.isAlt(): + vkAlt + else: + vkRegular + + + +func addvar(tbl: var VarTable, vsym: NimNode, path: Path): void = + let vs = vsym.nodeStr() + let class = path.classifyPath() + + if vs notin tbl: + tbl[vs] = VarSpec(decl: vsym, varKind: class, typePath: path) + + else: + var doUpdate = + (class == vkSequence) or + (class == vkOption and tbl[vs].varKind in {vkRegular}) + + if doUpdate: + tbl[vs].varKind = class + tbl[vs].typePath = path + + if class == vkAlt and tbl[vs].varKind == vkAlt: + for prefix in path.altPrefixes(): + let noalt = prefix[0 .. ^2] + if noalt notin tbl[vs].prefixMap: + tbl[vs].prefixMap[noalt] = AltSpec(altMax: prefix[^1].altMax.int16) + + var spec = tbl[vs].prefixMap[noalt] + spec.altPositions.incl prefix[^1].altIdx.int16 + if spec.altPositions.len == spec.altMax + 1: + spec.completed = true + + if spec.completed: + tbl[vs] = VarSpec( + decl: vsym, + varKind: vkRegular, + typePath: path + ) + + else: + tbl[vs].prefixMap[noalt] = spec + + + inc tbl[vs].cnt + +func makeVarTable(m: Match): tuple[table: VarTable, + mixident: seq[string]] = + + func aux(sub: Match, vt: var VarTable, path: Path): seq[string] = + if sub.bindVar.getSome(bindv): + if sub.isOptional and sub.fallback.isNone(): + vt.addvar(bindv, path & @[ + AccsElem(inStruct: kItem, isOpt: true) + ]) + + else: + vt.addVar(bindv, path) + + case sub.kind: + of kItem: + if sub.itemMatch == imkInfixEq and + sub.isPlaceholder and + sub.bindVar.isNone() + : + result &= "_" + if sub.itemMatch == imkSubpatt: + if sub.rhsPatt.kind == kObject and + sub.rhsPatt.isRefKind and + sub.rhsPatt.kindCall.getSome(kk) + : + result &= aux(sub.rhsPatt, vt, path & @[ + AccsElem(inStruct: kObject, fld: kk.repr)]) + else: + result &= aux(sub.rhsPatt, vt, path) + + of kSet: + discard + of kAlt: + for idx, alt in sub.altElems: + result &= aux(alt, vt, path & @[AccsElem( + inStruct: kAlt, + altIdx: idx, + altMax: sub.altElems.len - 1 + )]) + of kSeq: + for elem in sub.seqElems: + let parent = path & @[AccsElem( + inStruct: kSeq, pos: newLit(0), + isVariadic: elem.kind notin {lkPos, lkOpt})] + + if elem.bindVar.getSome(bindv): + if elem.patt.isOptional and elem.patt.fallback.isNone(): + vt.addVar(bindv, parent & @[ + AccsElem(inStruct: kItem, isOpt: true) + ]) + else: + vt.addVar(bindv, parent) + + result &= aux(elem.patt, vt, parent) + + of kTuple: + for idx, it in sub.tupleElems: + result &= aux(it, vt, path & @[ + AccsElem(inStruct: kTuple, idx: idx)]) + of kPairs: + for pair in sub.pairElems: + result &= aux(pair.patt, vt, path & @[ + AccsElem(inStruct: kPairs, key: pair.key)]) + of kObject: + for (fld, patt) in sub.fldElems: + result &= aux(patt, vt, path & @[ + AccsElem(inStruct: kObject, fld: fld)]) + + if sub.seqMatches.getSome(seqm): + result &= aux(seqm, vt, path) + + if sub.kvMatches.getSome(kv): + result &= aux(kv, vt, path) + + + result.mixident = aux(m, result.table, @[]).deduplicate() + + +func makeMatchExpr( + m: Match, + vtable: VarTable; + path: Path, + typePath: Path, + mainExpr: NimNode, + doRaise: bool, + originalMainExpr: NimNode + ): NimNode + +proc makeElemMatch( + elem: SeqStructure, # Pattern for element match + minLen: var int, # Required min len for object + maxLen: var int, # Required max len for object + doRaise: bool, # Raise exception on failed match? + failBreak: var NimNode, # Break of match chec loop + posid: NimNode, # Identifier for current position in sequence match + vtable: VarTable, # Table of variables + parent: Path, # Path to parent node + expr: NimNode, # Expression to check for pattern match + getLen: NimNode, # Get expression len + elemId: NimNode, # Main loop variable + idx: int, + counter: NimNode, + seqm: Match + ): tuple[ + body: NimNode, # Body of matching block + statevars: seq[tuple[varid, init: NimNode]], # Additional state variables + defaults: seq[NimNode] # Default `opt` setters + ] = + + result.body = newStmtList() + + + result.body.add newCommentStmtNode( + $elem.kind & " " & elem.patt.declNode.repr) + + let parent: Path = @[] + let mainExpr = elemId + + let pattStr = newLit(elem.decl.toStrLit().strVal()) + case elem.kind: + of lkPos: + inc minLen + inc maxLen + let ln = elem.decl.lineIInfo() + if doRaise and not debugWIP: + var str = newNimNode(nnkRStrLit) + str.strVal = "Match failure for pattern '" & pattStr.strVal() & + "'. Item at index " + + failBreak = quote do: + {.line: `ln`.}: + raise MatchError(msg: `str` & $(`posid` - 1) & " failed") + + if elem.bindVar.getSome(bindv): + result.body.add newCommentStmtNode( + "Set variable " & bindv.nodeStr() & " " & + $vtable[bindv.nodeStr()].varKind) + + + let vars = makeVarSet( + bindv, parent.toAccs(mainExpr, false), vtable, doRaise) + + + result.body.add quote do: + if not `vars`: + `failBreak` + + if elem.patt.kind == kItem and + elem.patt.itemMatch == imkInfixEq and + elem.patt.isPlaceholder: + result.body.add quote do: + inc `counter` + inc `posid` + continue + else: + result.body.add quote do: + if `expr`: + # debugecho "Match ok" + inc `counter` + inc `posid` + continue + + else: + # debugecho "Fail break" + `failBreak` + + else: + if elem.kind == lkSlice: + case elem.slice.kind: + of nnkIntKinds: + maxLen = max(maxLen, elem.slice.intVal.int + 1) + minLen = max(minLen, elem.slice.intVal.int + 1) + + of nnkInfix: + if elem.slice[1].kind in nnkIntKinds and + elem.slice[2].kind in nnkIntKinds: + + let diff = if elem.slice[0].strVal == "..": +1 else: 0 + + + maxLen = max([ + maxLen, + elem.slice[1].intVal.int, + elem.slice[2].intVal.int + diff + ]) + + minLen = max(minLen, elem.slice[1].intVal.int) + + else: + maxLen = 5000 + + else: + maxLen = 5000 + + # echov elem.kind + # echov maxLen + # echov minLen + + else: + maxLen = 5000 + + var varset = newEmptyNode() + + if elem.bindVar.getSome(bindv): + varset = makeVarSet( + bindv, parent.toAccs(mainExpr, false), vtable, doRaise) + # vtable.addvar(bindv, parent) # XXXX + + let ln = elem.decl.lineIInfo() + if doRaise and not debugWIP: + case elem.kind: + of lkAll: + failBreak = quote do: + {.line: `ln`.}: + raise MatchError( + msg: "Match failure for pattern '" & `pattStr` & + "' expected all elements to match, but item at index " & + $(`posid` - 1) & " failed" + ) + + of lkAny: + failBreak = quote do: + {.line: `ln`.}: + raise MatchError( + msg: "Match failure for pattern '" & `pattStr` & + "'. Expected at least one elemen to match, but got none" + ) + + of lkNone: + failBreak = quote do: + {.line: `ln`.}: + raise MatchError( + msg: "Match failure for pattern '" & `pattStr` & + "'. Expected no elements to match, but index " & + $(`posid` - 1) & " matched." + ) + + of lkSlice: + let positions = elem.slice.toStrLit().codeFmt() + failBreak = quote do: + {.line: `ln`.}: + raise MatchError( + msg: "Match failure for pattern '" & `pattStr` & + "'. Elements for positions " & `positions` & + " were expected to match no elements to match" + ) + + else: + discard + + if varset.kind == nnkEmpty: + varset = newLit(true) + + case elem.kind: + of lkAll: + # let allOk = genSym(nskVar, "allOk") + # result.statevars.add (allOk, newLit(true)) + result.body.add quote do: + block: + # If iteration value matches pattern, and all variables can + # be set, continue iteration. Otherwise fail (`all`) is a + # greedy patter - mismatch on single element would mean + # failure for whole sequence pattern + if `expr` and `varset`: + discard + + else: + `failBreak` + + of lkSlice: + var rangeExpr = elem.slice + # echov rangeExpr.idxTreeRepr() + if rangeExpr.kind in {nnkPrefix} + nnkIntKinds: + result.body.add quote do: + if `expr` and `varset`: + discard + + else: + `failBreak` + + inc `counter` + + else: + result.body.add quote do: + if `posid` in `rangeExpr`: + + if `expr` and `varset`: + discard + + else: + `failBreak` + + else: + inc `counter` + + of lkUntil: + result.body.add quote do: + if `expr`: + # After finishing `until` increment counter + inc `counter` + else: + discard `varset` + + if idx == seqm.seqElems.len - 1: + # If `until` is a last element we need to match if fully + result.body.add quote do: + if (`posid` < `getLen`): ## Not full match + `failBreak` + + of lkAny: + let state = genSym(nskVar, "anyState") + result.statevars.add (state, newLit(false)) + + result.body.add quote do: + block: + if `expr` and `varset`: + `state` = true + + else: + if (`posid` == `getLen` - 1): + if not `state`: + `failBreak` + + of lkPref: + result.body.add quote do: + if `expr`: + discard `varset` + else: + inc `counter` + + of lkOpt: + let state = genSym(nskVar, "opt" & $idx & "State") + result.statevars.add (state, newLit(false)) + + if elem.patt.isOptional and + elem.bindVar.getSome(bindv) and + elem.patt.fallback.getSome(fallback): + + let default = makeVarSet(bindv, fallback, vtable, doRaise) + + result.defaults.add quote do: + if not `state`: + discard `default` + + result.body.add quote do: + discard `varset` + `state` = true + + inc `counter` + inc `posid` + continue + + of lkNone: + result.body.add quote do: + block: + if (not `expr`) and `varset`: + discard + else: + `failBreak` + + of lkTrail, lkPos: + discard + + + + + +func makeSeqMatch( + seqm: Match, vtable: VarTable; path: Path, + mainExpr: NimNode, + doRaise: bool, + originalMainExpr: NimNode + ): NimNode = + + var idx = 1 + while idx < seqm.seqElems.len: + if seqm.seqElems[idx - 1].kind notin { + lkUntil, lkPos, lkOpt, lkPref, lkSlice}: + error("Greedy seq match must be last element in pattern", + seqm.seqElems[idx].decl) + + inc idx + + let + posid = genSym(nskVar, "pos") + matched = genSym(nskVar, "matched") + failBlock = ident("failBlock") + getLen = newCall("len", path.toAccs(mainExpr, false)) + + var failBreak = nnkBreakStmt.newTree(failBlock) + + + result = newStmtList() + var minLen = 0 + var maxLen = 0 + + let counter = genSym(nskVar, "counter") # Current pattern counter + let elemId = genSym(nskForVar, "elemId") # Main loop variable + + var loopBody = newStmtList() + + # Out-of-loop state variables + var statevars = newStmtList() + + # Default valudes for `opt` matches + var defaults = newStmtList() + + let successBlock = ident("successBlock") + + # Find necessary element size (fail-fast on `len` mismatch) + for idx, elem in seqm.seqElems: + let idLit = newLit(idx) + if elem.kind == lkTrail: + maxLen = 5000 + loopBody.add quote do: + if `counter` == `idLit`: + break `successBlock` + + else: + var + elemMainExpr = elemId + parent: Path = path & @[AccsElem( + inStruct: kSeq, pos: posid, + isVariadic: elem.kind notin {lkPos, lkOpt} + )] + + if elem.kind == lkSlice and + elem.slice.kind in (nnkIntKinds + {nnkPrefix}): + parent[^1].isVariadic = false + parent[^1].pos = elem.slice + elemMainExpr = nnkBracketExpr.newTree(mainExpr, elem.slice) + + let + expr: NimNode = elem.patt.makeMatchExpr( + vtable, + # Passing empty path and overriding `mainExpr` for elemen access + # in order to make all nested matches use loop variable + path = @[], + mainExpr = elemMainExpr, + # Expression for type path must be unchanged + typePath = parent, + doRaise = false, + # WARNING no detailed reporting for subpattern matching failure. + # In order to get these I need to return failure string from each + # element, which makes thing more complicated. It is not that + # hard to just return `(bool, string)` or something similar, but + # it would require quite annoying (albeit simple) redesign of the + # whole code, to account for new expression types. Not impossible + # though. + + # doRaise and (elem.kind notin {lkUntil, lkAny, lkNone}) + originalMainExpr = originalMainExpr + ) + + let (body, state, defsets) = makeElemMatch( + elem = elem, + elemId = elemId, + minLen = minLen, + maxLen = maxLen, + doRaise = doRaise, + failBreak = failBreak, + posid = posid, + vtable = vtable, + parent = parent, + expr = expr, + getLen = getLen, + idx = idx, + seqm = seqm, + counter = counter + ) + + defaults.add defsets + + loopBody.add quote do: + if `counter` == `idLit`: + `body` + + for (varname, init) in state: + statevars.add nnkVarSection.newTree( + nnkIdentDefs.newTree(varname, newEmptyNode(), init)) + + + result.add loopBody + + + let + comment = newCommentStmtNode(seqm.declNode.repr) + minNode = newLit(minLen) + maxNode = newLit(maxLen) + + var setCheck: NimNode + if maxLen >= 5000: + setCheck = quote do: + ## Check required len + `getLen` < `minNode` + + else: + setCheck = quote do: + ## Check required len + `getLen` notin {`minNode` .. `maxNode`} + + if doRaise and not debugWIP: + var pattStr = seqm.declNode.toPattStr() + let ln = seqm.declNode.lineIInfo() + let lenObj = path.toAccs(originalMainExpr, false).toStrLit().codeFmt() + + if maxLen >= 5000: + failBreak = quote do: + {.line: `ln`.}: + raise MatchError( + msg: "Match failure for pattern " & `pattStr` & + "Expected at least " & $(`minNode`) & + " elements, but " & (`lenObj`) & + " has .len of " & $(`getLen`) & "." + ) + + else: + failBreak = quote do: + {.line: `ln`.}: + raise MatchError( + msg: "Match failure for pattern " & `pattStr` & + "Expected length in range '" & $(`minNode`) & " .. " & + $(`maxNode`) & "', but `" & (`lenObj`) & + "` has .len of " & $(`getLen`) & "." + ) + + let tmpExpr = path.toAccs(mainExpr, false) + + result = quote do: + # Main match loop + for `elemId` in `tmpExpr`: + `result` + inc `posid` + + + var str = seqm.declNode.toStrLit().strVal() + if split(str, '\n').len > 1: + str = "\n" & str + + let patternLiteral = newLit(str).codeFmt() + + let compileCheck = + if debugWIP: + newStmtList() + + else: + quote do: + when not compiles(((discard `tmpExpr`.len()))): + static: + error " no `len` defined for " & $typeof(`tmpExpr`) & + " - needed to find number of elements for pattern " & + `patternLiteral` + + when not compiles((( + for item in items(`tmpExpr`): + discard + ))): + static: + error " no `items` defined for " & $typeof(`tmpExpr`) & + " - iteration is require for pattern " & + `patternLiteral` + + + + result = quote do: + `comment` + `compileCheck` + var `matched` = false + # Main expression of match + + # Failure block + block `failBlock`: + var `posid` = 0 ## Start seq match + var `counter` = 0 + + # Check for `len` first + if `setCheck`: + ## fail on seq len + `failBreak` + + # State variable initalization + `statevars` + + # Block for early successfuly return from iteration + block `successBlock`: + `result` + + `defaults` + + `matched` = true ## Seq match ok + + `matched` + + result = result.newPar().newPar() + # echov result.repr + + + + +func makeMatchExpr( + m: Match, + vtable: VarTable; + path: Path, + typePath: Path, + mainExpr: NimNode, + doRaise: bool, + originalMainExpr: NimNode + ): NimNode = + + case m.kind: + of kItem: + let parent = path.toAccs(mainExpr, false) + case m.itemMatch: + of imkInfixEq, imkSubpatt: + if m.itemMatch == imkInfixEq: + if m.isPlaceholder: + result = newLit(true) + else: + result = nnkInfix.newTree(ident m.infix, parent, m.rhsNode) + else: + result = makeMatchExpr( + m.rhsPatt, vtable, + path, path, mainExpr, # Type path and access path are the same + doRaise, originalMainExpr + ) + + if m.bindVar.getSome(vname): + # vtable.addvar(vname, path) # XXXX + let bindVar = makeVarSet(vname, parent, vtable, doRaise) + if result == newLit(true): + result = bindVar + else: + result = quote do: + block: + if `result`: + `bindVar` + else: + false + + + of imkPredicate: + let pred = m.predBody + var bindVar = newEmptyNode() + if m.bindVar.getSome(vname): + # vtable.addvar(vname, path) # XXXX + bindVar = makeVarSet(vname, parent, vtable, doRaise) + else: + bindVar = newLit(true) + + result = quote do: + let it {.inject.} = `parent` + if `pred`: + `bindVar` + else: + false + + of kSeq: + return makeSeqMatch( + m, vtable, path, mainExpr, doRaise, originalMainExpr) + + of kTuple: + var conds: seq[NimNode] + for idx, it in m.tupleElems: + let path = path & @[AccsElem(inStruct: kTuple, idx: idx)] + conds.add it.makeMatchExpr( + vtable, path, path, mainExpr, doRaise, originalMainExpr) + + result = conds.foldInfix("and") + + of kObject: + var conds: seq[NimNode] + var refCast: seq[AccsElem] + if m.kindCall.getSome(kc): + if m.isRefKind: + conds.add newCall( + "not", + newCall(ident "isNil", path.toAccs(mainExpr, false))) + + conds.add newCall(ident "of", path.toAccs(mainExpr, false), kc) + refCast.add AccsElem( + inStruct: kObject, fld: kc.repr) + else: + conds.add newCall(ident "hasKind", path.toAccs(mainExpr, false), kc) + + for (fld, patt) in m.fldElems: + let path = path & refCast & @[AccsElem(inStruct: kObject, fld: fld)] + + conds.add patt.makeMatchExpr( + vtable, path, path, mainExpr, doRaise, originalMainExpr) + + if m.seqMatches.getSome(seqm): + conds.add seqm.makeMatchExpr( + vtable, path, path, mainExpr, doRaise, originalMainExpr) + + if m.kvMatches.getSome(kv): + conds.add kv.makeMatchExpr( + vtable, path, path, mainExpr, doRaise, originalMainExpr) + + result = conds.foldInfix("and") + + of kPairs: + var conds: seq[NimNode] + for pair in m.pairElems: + let + accs = path.toAccs(mainExpr, false) + valPath = path & @[AccsElem( + inStruct: kPairs, key: pair.key, nocheck: m.nocheck)] + + valGet = valPath.toAccs(mainExpr, false) + + if m.nocheck: + conds.add pair.patt.makeMatchExpr( + vtable, valPath, valPath, mainExpr, doRaise, + originalMainExpr + ) + + else: + let + incheck = nnkInfix.newTree(ident "in", pair.key, accs) + + if not pair.patt.isOptional: + conds.add nnkInfix.newTree( + ident "and", incheck, + pair.patt.makeMatchExpr( + vtable, valPath, valPath, + mainExpr, doRaise, originalMainExpr, + ) + ) + + else: + let varn = pair.patt.bindVar.get + let varsetOk = makeVarSet(varn, valGet, vtable, doRaise) + if pair.patt.fallback.getSome(fallback): + let varsetFail = makeVarSet( + varn, fallback, vtable, doRaise) + + conds.add quote do: + block: + if `incheck`: + `varsetOk` + else: + `varsetFail` + else: + conds.add quote do: + if `incheck`: + `varsetOk` + else: + true + + result = conds.foldInfix("and") + + of kAlt: + var conds: seq[NimNode] + for idx, alt in m.altElems: + let path = path & @[AccsElem( + inStruct: kAlt, + altIdx: idx, + altMax: m.altElems.len - 1 + )] + + conds.add alt.makeMatchExpr( + vtable, path, path, mainExpr, false, originalMainExpr) + + let res = conds.foldInfix("or") + if not doRaise: + return res + + else: + let pattStr = m.declNode.toStrLit() + return quote do: + `res` or (block: raise MatchError( + msg: "Match failure for pattern '" & `pattStr` & + "' - None of the alternatives matched." + ); true) + + of kSet: + var testSet = nnkCurly.newTree() + let setPath = path.toAccs(mainExpr, false) + for elem in m.setElems: + if elem.kind == kItem: + testSet.add elem.rhsNode + + result = quote do: + `setPath` in `testSet` + + if doRaise: + let msgLit = newLit( + "Pattern match failed: element does not match " & + m.declNode.toPattStr().strVal()) + + result = quote do: + `result` or ((block: raise MatchError(msg: `msgLit`) ; false)) + + +func makeMatchExpr*( + m: Match, mainExpr: NimNode, + doRaise: bool, originalMainExpr: NimNode + ): tuple[ + expr: NimNode, vtable: VarTable, mixident: seq[string] + ] = + + ## Create NimNode for checking whether or not item referred to by + ## `mainExpr` matches pattern described by `Match` + + (result.vtable, result.mixident) = makeVarTable(m) + result.expr = makeMatchExpr( + m, result.vtable, @[], @[], mainExpr, doRaise, originalMainExpr) + +func toNode( + expr: NimNode, vtable: VarTable, mainExpr: NimNode + ): NimNode = + + var exprNew = nnkStmtList.newTree() + var hasOption: bool = false + for name, spec in vtable: + echov name & " -> " & $spec + let vname = ident(name) + var typeExpr = toAccs(spec.typePath, mainExpr, true) + typeExpr = quote do: + ((let tmp = `typeExpr`; tmp)) + + var wasSet = newEmptyNode() + if vtable[name].cnt > 1: + let varn = ident(name & "WasSet") + wasSet = quote do: + var `varn`: bool = false + + exprNew.add wasSet + + + case spec.varKind: + of vkSequence: + block: + let varExpr = toAccs(spec.typePath[0 .. ^2], mainExpr, false) + exprNew.add quote do: + when not compiles(((discard `typeExpr`))): + static: error $typeof(`varExpr`) & + " does not support iteration via `items`" + + exprNew.add quote do: + var `vname`: seq[typeof(`typeExpr`)] + + + of vkOption, vkAlt: + hasOption = true + exprNew.add quote do: + var `vname`: Option[typeof(`typeExpr`)] + + of vkSet: + exprNew.add quote do: + var `vname`: typeof(`typeExpr`) + + of vkRegular: + exprNew.add quote do: + var `vname`: typeof(`typeExpr`) + + result = quote do: + `exprNew` + `expr` + +macro expand*(body: typed): untyped = body + +macro match*(n: untyped): untyped = + var matchcase = nnkIfStmt.newTree() + var mixidents: seq[string] + let mainExpr = genSym(nskLet, "expr") + for elem in n[1 .. ^1]: + case elem.kind: + of nnkOfBranch: + if elem[0] == ident "_": + error("To create catch-all match use `else` clause", elem[0]) + + # echo elem[0].repr + let (expr, vtable, mixid) = + toSeq(elem[0 .. ^2]).foldl( + nnkInfix.newTree(ident "|", a, b) + ).parseMatchExpr().makeMatchExpr(mainExpr, false, n) + + mixidents.add mixid + + matchcase.add nnkElifBranch.newTree( + toNode(expr, vtable, mainExpr).newPar().newPar(), + elem[^1] + ) + + of nnkElifBranch, nnkElse: + matchcase.add elem + else: + discard + + let head = n[0] + let pos = newCommentStmtNode($n.lineInfoObj()) + var mixinList = newStmtList nnkMixinStmt.newTree( + mixidents.deduplicate.mapIt( + ident it + ) + ) + + if mixidents.len == 0: + mixinList = newEmptyNode() + + let ln = lineIInfo(n[0]) + let posId = genSym(nskLet, "pos") + result = quote do: + block: + # `mixinList` + `pos` + {.line: `ln`.}: + let `mainExpr` {.used.} = `head` + + let `posId` {.used.}: int = 0 + discard `posId` + `matchcase` + +macro `case`*(n: untyped): untyped = newCall("match", n) + + +macro assertMatch*(input, pattern: untyped): untyped = + ## Try to match `input` using `pattern` and raise `MatchError` on + ## failure. For DSL syntax details see start of the document. + let pattern = + if pattern.kind == nnkStmtList and pattern.len == 1: + pattern[0] + else: + pattern + + let + expr = genSym(nskLet, "expr") + (mexpr, vtable, _) = pattern.parseMatchExpr().makeMatchExpr( + expr, true, input# .toStrLit().strVal() + ) + + matched = toNode(mexpr, vtable, expr) + + + result = quote do: + let `expr` = `input` + let ok = `matched` + discard ok + +macro matches*(input, pattern: untyped): untyped = + ## Try to match `input` using `pattern` and return `false` on + ## failure. For DSL syntax details see start of the document. + let pattern = + if pattern.kind == nnkStmtList and pattern.len == 1: + pattern[0] + else: + pattern + + let + expr = genSym(nskLet, "expr") + (mexpr, vtable, _) = pattern.parseMatchExpr().makeMatchExpr( + expr, false, input # .toStrLit().strVal() + ) + + matched = toNode(mexpr, vtable, expr) + + result = quote do: + let `expr` = `input` + `matched` + +func buildTreeMaker( + prefix: string, + resType: NimNode, + match: Match, + newRes: bool = true, + tmp = genSym(nskVar, "res")): NimNode = + + case match.kind: + of kItem: + if (match.itemMatch == imkInfixEq): + if match.isPlaceholder: + if match.bindVar.getSome(bindv): + result = newIdentNode(bindv.nodeStr()) + else: + error( + "Only variable placeholders allowed for pattern " & + "construction, but expression is a `_` placeholder - " & + match.declNode.toStrLit().strVal().codeFmt() + , + match.declNode + ) + else: + if match.rhsNode != nil: + result = match.rhsNode + else: + error("Empty rhs node for item", match.declNode) + else: + error( + "Predicate expressions are not supported for tree " & + "construction, use `== ` to set field result" + ) + of kObject: + var res = newStmtList() + let tmp = genSym(nskVar, "res") + + res.add quote do: + var `tmp`: `resType` + when `tmp` is ref: + new(`tmp`) + + if match.kindCall.getSome(call): + let kind = ident call.nodeStr().addPrefix(prefix) + res.add quote do: + {.push warning[CaseTransition]: off.} + when declared(FieldDefect): + try: + `tmp`.kind = `kind` + except FieldDefect: + raise newException(FieldDefect, + "Error while setting `kind` for " & $typeof(`tmp`) & + " - type does not provide `kind=` override." + ) + else: + `tmp`.kind = `kind` + {.pop.} + + else: + error( + "Named tuple construction is not supported. To Create " & + "object use `Kind(f1: val1, f2: val2)`" , + match.declNode + ) + + for (name, patt) in match.fldElems: + res.add nnkAsgn.newTree(newDotExpr( + tmp, ident name + ), buildTreeMaker(prefix, resType, patt)) + + if match.seqMatches.getSome(seqm): + res.add buildTreeMaker(prefix, resType, seqm, false, tmp) + # if match.seqMatches.isSome(): + # for sub in match.seqMatches.get().seqElems: + # res.add newCall("add", tmp, buildTreeMaker( + # prefix, resType, sub.patt)) + + res.add tmp + + result = newBlockStmt(res) + of kSeq: + var res = newStmtList() + if newRes: + res.add quote do: + var `tmp`: seq[`resType`] + + for sub in match.seqElems: + # debugecho sub.kind, " ", sub.decl.repr + case sub.kind: + of lkAll: + if sub.bindVar.getSome(bindv): + res.add quote do: + for elem in `bindv`: + `tmp`.add elem + elif sub.patt.kind == kItem and + sub.patt.itemMatch == imkInfixEq and + sub.patt.infix == "==": + let body = sub.patt.rhsNode + res.add quote do: + for elem in `body`: + `tmp`.add elem + + # debugecho body.repr + else: + error("`all` for pattern construction must have varaible", + sub.decl) + of lkPos: + res.add newCall("add", tmp, buildTreeMaker( + prefix, resType, sub.patt)) + else: + raiseAssert("#[ IMPLEMENT ]#") + + if newRes: + res.add quote do: + `tmp` + + result = newBlockStmt(res) + else: + error( + &"Pattern of kind {match.kind} is " & + "not supported for tree construction", + match.declNode + ) + +func `kind=`*(node: var NimNode, kind: NimNodeKind) = + node = newNimNode(kind, node) + +func str*(node: NimNode): string = node.nodeStr() +func `str=`*(node: var NimNode, val: string) = + if node.kind in {nnkIdent, nnkSym}: + node = ident val + else: + node.strVal = val + +func getTypeIdent(node: NimNode): NimNode = + case node.getType().kind: + of nnkObjectTy, nnkBracketExpr: + newCall("typeof", node) + else: + node.getType() + +macro makeTreeImpl(node, kind: typed, patt: untyped): untyped = + var inpatt = patt + # debugecho "\e[41m*====\e[49m 093q45ih4qw33 \e[41m=====*\e[49m" + # debugecho "patt: ", patt.repr + if patt.kind in {nnkStmtList}: + if patt.len > 1: + inpatt = newStmtList(patt.toSeq()) + else: + inpatt = patt[0] + + # debugecho "inpatt: ", inpatt.repr + let (pref, _) = kind.getKindNames() + + var match = inpatt.parseMatchExpr() + result = buildTreeMaker(pref, node.getTypeIdent(), match) + + if patt.kind in {nnkStmtList} and + patt[0].len == 1 and + match.kind == kSeq and + patt[0].kind notin {nnkBracket} + : + result = nnkBracketExpr.newTree(result, newLit(0)) + + # echo result.repr + +template makeTree*(T: typed, patt: untyped): untyped = + ## Construct tree from pattern matching expression. For example of + ## use see documentation at the start of the module + block: + var tmp: T + when not compiles((var t: T; discard t.kind)): + static: error "No `kind` defined for " & $typeof(tmp) + + when not compiles((var t: T; t.kind = t.kind)): + static: error "Cannot set `kind=` for " & $typeof(tmp) + + when not compiles((var t: T; t.add t)): + static: error "No `add` defined for " & $typeof(tmp) + + makeTreeImpl(tmp, tmp.kind, patt) + +template `:=`*(lhs, rhs: untyped): untyped = + ## Shorthand for `assertMatch` + assertMatch(rhs, lhs) + +template `?=`*(lhs, rhs: untyped): untyped = + ## Shorthand for `matches` + matches(rhs, lhs) diff --git a/lib/pure/matching.rst b/lib/pure/matching.rst new file mode 100644 index 0000000000000..6e621b62eb6e0 --- /dev/null +++ b/lib/pure/matching.rst @@ -0,0 +1,541 @@ +.. raw:: html +

+ "you can probably make a macro for that" -- Rika, 22-09-2020 10:41:51 +

+ +:Author: haxscramper + +This module implements pattern matching for objects, tuples, +sequences, key-value pairs, case and derived objects. DSL can also be +used to create object trees (AST). + + + +Quick reference +=============== + +============================= ======================================================= + Example Explanation +============================= ======================================================= + ``(fld: @val)`` Field ``fld`` into variable ``@val`` + ``Kind()`` Object with ``.kind == Kind()`` [1] + ``of Derived()`` Match object of derived type + ``(@val, _)`` First element in tuple in ``@val`` + ``(@val, @val)`` Tuple with two equal elements + ``{"key" : @val}`` Table with "key", capture into ``@val`` [2] + ``[_, _]`` Sequence with ``len == 2`` [3] + ``[_, .._]`` At least one element + ``[_, all @val]`` All elements starting from index ``1`` + ``[until @val == "2", .._]`` Capture all elements *until* first ``"2"`` [4] + ``[until @val == 1, @val]`` All *including* first match + ``[all @val == 12]`` All elements are ``== 12``, capture into ``@val`` + ``[some @val == 12]`` At least *one* is ``== 12``, capture all matching into ``@val`` +============================= ======================================================= + +- [1] Kind fields can use shorted enum names - both ``nnkStrLit`` and + ``StrLit`` will work (prefix ``nnk`` can be omitted) +- [2] Or any object with ``contains`` and ``[]`` defined (for necessary types) +- [3] Or any object with ``len`` proc or field +- [4] Note that sequence must match *fully* and it is necessary to have + ``.._`` at the end in order to accept sequences of arbitrary length. + +Supported match elements +======================== + +- *seqs* - matched using ``[Patt1(), Patt2(), ..]``. Must have + ``len(): int`` and ``[int]: T`` defined. +- *tuples* - matched using ``(Patt1(), Patt2(), ..)``. +- *pairable* - matched using ``{Key: Patt()}``. Must have ``[Key]: T`` + defined. ``Key`` is not a pattern - search for whole collection + won't be performed. +- *set* - matched using ``{Val1, Val2, .._}``. Must have ``contains`` + defined. If variable is captured then ``Val1`` must be comparable + and collection should also implement ``items`` and ``incl``. +- *object* - matched using ``(field: Val)``. Case objects are matched + using ``Kind(field: Val)``. If you want to check agains multiple + values for kind field ``(kind: in SomeSetOfKinds)`` + +Element access +============== + +To determine whether particular object matches pattern *access +path* is generated - sequence of fields and ``[]`` operators that you +would normally write by hand, like ``fld.subfield["value"].len``. Due to +support for `method call syntax +`_ +there is no difference between field access and proc call, so things +like `(len: < 12)` also work as expected. + +``(fld: "3")`` Match field ``fld`` against ``"3"``. Generated access + is ``expr.fld == "3"``. + +``["2"]`` Match first element of expression agains patt. Generate + acess ``expr[pos] == "2"``, where ``pos`` is an integer index for + current position in sequence. + +``("2")`` For each field generate access using ``[1]`` + +``{"key": "val"}`` First check ``"key" in expr`` and then + ``expr["key"] == "val"``. No exception on missing keys, just fail + match. + +It is possible to have mixed assess for objects. Mixed object access +via ``(gg: _, [], {})`` creates the same code for checking. E.g ``([_])`` +is the same as ``[_]``, ``({"key": "val"})`` is is identical to just +``{"key": "val"}``. You can also call functions and check their values +(like ``(len: _(it < 10))`` or ``(len: in {0 .. 10})``) to check for +sequence length. + +Checks +====== + +- Any operators with exception of ``is`` (subpattern) and ``of`` (derived + object subpattern) is considered final comparison and just pasted as-is + into generated pattern match code. E.g. ``fld: in {2,3,4}`` will generate + ``expr.fld in {2,3,4}`` + +- ``(fld: Patt())`` - check if ``expr.fld`` matches pattern ``Patt()`` + +- ``(fld: _.matchesPredicate())`` - if call to + ``matchesPredicate(expr.fld)`` evaluates to true. + +Notation: ```` refers to any possible combination of checks. For +example + +- ``fld: in {1,2,3}`` - ```` is ``in {1,2,3}`` +- ``[_]`` - ```` is ``_`` +- ``fld: Patt()`` - ```` is ``Patt()`` + +Examples +-------- + +- ``(fld: 12)`` If rhs for key-value pair is integer, string or + identifier then ``==`` comparison will be generated. +- ``(fld: == ident("33"))`` if rhs is a prefix of ``==`` then ``==`` will + be generated. Any for of prefix operator will be converted to + ``expr.fld ``. +- ``(fld: in {1, 3, 3})`` or ``(fld: in Anything)`` creates ``fld.expr + in Anything``. Either ``in`` or ``notin`` can be used. + +Variable binding +================ + +Match can be bound to new varaible. All variable declarations happen +via ``@varname`` syntax. + +- To bind element to variable without any additional checks do: ``(fld: @varname)`` +- To bind element with some additional operator checks do: + + - ``(fld: @varname Value)`` first perform check using + ```` and then add ``Value`` to ``@varname`` + - ``(fld: @hello is ("2" | "3"))`` + +- Predicate checks: ``fld: @a.matchPredicate()`` +- Arbitrary expression: ``fld: @a(it mod 2 == 0)``. If expression has no + type it is considered ``true``. + +Bind order +---------- + +Bind order: if check evaluates to true variable is bound immediately, +making it possible to use in other checks. ``[@head, any @tail != +head]`` is a valid pattern. First match ``head`` and then any number +of ``@tail`` elements. Can use ``any _(if it != head: tail.add it)`` +and declare ``tail`` externally. + +Variable is never rebound. After it is bound, then it will have the +value of first binding. + +Bind variable type +------------------ + +- Any variadics are mapped to sequence +- Only once in alternative is option +- Explicitly optional is option +- Optional with default value is regular value +- Variable can be used only once if in alternative + + +========================== ===================================== + Pattern Ijected variables +========================== ===================================== + ``[@a]`` ``var a: typeof(expr[0])`` + ``{"key": @val}`` ``var val: typeof(expr["key"])`` + ``[all @a]`` ``var a: seq[typeof(expr[0])]`` + ``[opt @val]`` ``var a: Option[typeof(expr[0])]`` + ``[opt @val or default]`` ``var a: typeof(expr[0])`` + ``(fld: @val)`` ``var val: typeof(expr.fld)`` +========================== ===================================== + +Matching different things +========================= + +Sequence matching +----------------- + +Input sequence: ``[1,2,3,4,5,6,5,6]`` + +================================= ======================== ==================================== + Pattern Result Comment +================================= ======================== ==================================== + ``[_]`` **Fail** Input sequence size mismatch + ``[.._]`` **Ok** + ``[@a]`` **Fail** Input sequence size mismatch + ``[@a, .._]`` **Ok**, ``a = 1`` + ``[any @a, .._]`` **Error** + ``[any @a(it < 10)]`` **Ok**, ``a = [1..6]`` Capture all elements that match + ``[until @a == 6, .._]`` **Ok** All until first ocurrence of ``6`` + ``[all @a == 6, .._]`` **Ok** ``a = []`` All leading ``6`` + ``[any @a(it > 100)]`` **Fail** No elements ``> 100`` + ``[none @a(it in {6 .. 10})]`` **Fail** There is an element ``== 6`` + ``[0 .. 2 is < 10, .._]`` **Ok** First three elements ``< 10`` + ``[0 .. 2 is < 10]`` **Fail** Missing trailing ``.._`` +================================= ======================== ==================================== + +``until`` + non-greedy. Match everything until ```` + + - ``until ``: match all until frist element that matches Expr + +``all`` + greedy. Match everything that matches ```` + + - ``all ``: all elements should match Expr + + - ``all @val is ``: capture all elements in ``@val`` if ```` + is true for every one of them. +``opt`` + Single element match + + - ``opt @a``: match optional element and bind it to a + + - ``opt @a or "default"``: either match element to a or set a to + "default" +``any`` + greedy. Consume all sequence elements until the end and + succed only if any element has matched. + + - ``any @val is "d"``: capture all element that match ``is "d"`` + +``none`` + greedy. Consume all sequence elements until the end and + succed only if any element has matched. EE + +``[m .. n @capture]`` + Capture slice of elements from index `m` to `n` + +Greedy patterns match until the end of a sequence and cannot be +followed by anything else. + +For sequence to match is must either be completely matched by all +subpatterns or have trailing ``.._`` in pattern. + +============= ============== ============== + Sequence Pattern Match result +============= ============== ============== + ``[1,2,3]`` ``[1,2]`` **Fail** + ``[1, .._]`` **Ok** + ``[1,2,_]`` **Ok** +============= ============== ============== + +Use examples +~~~~~~~~~~~~ + +- capture all elements in sequence: ``[all @elems]`` +- get all elements until (not including "d"): ``[until @a is "d"]`` +- All leading "d": ``[all @leading is "d"]`` +- Match first two elements and ignore the rest ``[_, _, .._]`` +- Match optional third element ``[_, _, opt @trail]`` +- Match third element and if not matched use default value ``[_, _, + opt @trail or "default"]`` +- Capture all elements until first separator: ``[until @leading is + "sep", @middle is "sep", all @trailing]`` +- Extract all conditions from IfStmt: ``IfStmt([all ElseIf([@cond, + _]), .._])`` + + +In addition to working with nested subpatterns it is possible to use +pattern matching as simple text scanner, similar to strscans. Main +difference is that it allows to work on arbitrary sequences, meaning it is +possible, for example, to operate on tokens, or as in this example on +strings (for the sake of simplicity). + +.. code:: nim + + func allIs(str: string, chars: set[char]): bool = str.allIt(it in chars) + + "2019-10-11 school start".split({'-', ' '}).assertMatch([ + pref @dateParts(it.allIs({'0' .. '9'})), + pref _(it.allIs({' '})), + all @text + ]) + + doAssert dateParts == @["2019", "10", "11"] + doAssert text == @["school", "start"] + +Tuple matching +-------------- + +Input tuple: ``(1, 2, "fa")`` + +============================ ========== ============ + Pattern Result Comment +============================ ========== ============ + ``(_, _, _)`` **Ok** Match all + ``(@a, @a, _)`` **Fail** + ``(@a is (1 | 2), @a, _)`` **Error** + ``(1, 1 | 2, _)`` **Ok** +============================ ========== ============ + +There are not a lot of features implemented for tuple matching, though it +should be noted that `:=` operator can be quite handy when it comes to +unpacking nested tuples - + +.. code:: nim + + (@a, (@b, _), _) := ("hello", ("world", 11), 0.2) + +Object matching +--------------- + +For matching object fields you can use ``(fld: value)`` - + +.. code:: nim + + type + Obj = object + fld1: int8 + + func len(o: Obj): int = 0 + + case Obj(): + of (fld1: < -10): + discard + + of (len: > 10): + # can use results of function evaluation as fields - same idea as + # method call syntax in regular code. + discard + + of (fld1: in {1 .. 10}): + discard + + of (fld1: @capture): + doAssert capture == 0 + +Variant object matching +----------------------- + +Matching on ``.kind`` field is a very common operation and has special +syntax sugar - ``ForStmt()`` is functionally equivalent to ``(kind: +nnkForStmt)``, but much more concise. + +`nnk` pefix can be omitted - in general if your enum field name folows +`nep1` naming `conventions +`_ +(each enum name starts with underscore prefix (common for all enum +elements), followed PascalCase enum name. + + +Input AST + +.. code:: nim + + ForStmt + Ident "i" + Infix + Ident ".." + IntLit 1 + IntLit 10 + StmtList + Command + Ident "echo" + IntLit 12 + +- ``ForStmt([== ident("i"), .._])`` Only for loops with ``i`` as + variable +- ``ForStmt([@a is Ident(), .._])`` Capture for loop variable +- ``ForStmt([@a.isTuple(), .._])`` for loops in which first subnode + satisfies predicate ``isTuple()``. Bind match to ``a`` +- ``ForStmt([_, _, (len: in {1 .. 10})])`` between one to ten + statements in the for loop body + +- Using object name for pattern matching ``ObjectName()`` does not produce + a hard error, but if ``.kind`` field does not need to be checked ``(fld: + )`` will be sufficient. +- To check ``.kind`` against multiple operators prefix ``in`` can be used - + ``(kind: in {nnkForStmt, nnkWhileStmt})`` + + +Custom unpackers +---------------- + +It is possible to unpack regular object using tuple matcher syntax - in +this case overload for ``[]`` operator must be provided that accepts +``static[FieldIndex]`` argument and returns a field. + +.. code:: nim + + type + Point = object + x: int + y: int + + proc `[]`(p: Point, idx: static[FieldIndex]): auto = + when idx == 0: + p.x + elif idx == 1: + p.y + else: + static: + error("Cannot unpack `Point` into three-tuple") + + let point = Point(x: 12, y: 13) + + (@x, @y) := point + + assertEq x, 12 + assertEq y, 13 + +Note ``auto`` return type for ``[]`` proc - it is necessary if different +types of fields might be returned on tuple unpacking, but not mandatory. + +If different fields have varying types ``when`` **must** and ``static`` be +used to allow for compile-time code selection. + +Ref object matching +------------------- + +It is also possible to match derived ``ref`` objects with patterns using +``of`` operator. It allows for runtime selection of different derived +types. + +Note that ``of`` operator is necessary for distinguishing between multiple +derived objects, or getting fields that are present only in derived types. +In addition it performs ``isNil()`` check in the object, so it might be +used in cases when you are not dealing with derived types. + +Due to ``isNil()`` check this pattern only makes sense when working with +``ref`` objects. + +.. code:: nim + + type + Base1 = ref object of RootObj + fld: int + + First1 = ref object of Base1 + first: float + + Second1 = ref object of Base1 + second: string + + let elems: seq[Base1] = @[ + Base1(fld: 123), + First1(fld: 456, first: 0.123), + Second1(fld: 678, second: "test"), + nil + ] + + for elem in elems: + case elem: + of of First1(fld: @capture1, first: @first): + # Only capture `Frist1` elements + doAssert capture1 == 456 + doAssert first == 0.123 + + of of Second1(fld: @capture2, second: @second): + # Capture `second` field in derived object + doAssert capture2 == 678 + doAssert second == "test" + + of of Base1(fld: @default): + # Match all *non-nil* base elements + doAssert default == 123 + + else: + doAssert isNil(elem) + + +.. + Matching for ref objects is not really different from regular one - the + only difference is that you need to use ``of`` operator explicitly. For + example, if you want to do ``case`` match for different object kinds - and + + .. code:: nim + + case Obj(): + of of StmtList(subfield: @capture): + # do something with `capture` + + You can use ``of`` as prefix operator - things like ``{12 : of + SubRoot(fld1: @fld1)}``, or ``[any of Derived()]``. + + +KV-pairs matching +----------------- + +Input json string + +.. code:: json + + {"menu": { + "id": "file", + "value": "File", + "popup": { + "menuitem": [ + {"value": "New", "onclick": "CreateNewDoc()"}, + {"value": "Open", "onclick": "OpenDoc()"}, + {"value": "Close", "onclick": "CloseDoc()"} + ] + } + }} + +- Get input ``["menu"]["file"]`` from node and + +.. code:: nim + case inj: + of {"menu" : {"file": @file is JString()}}: + # ... + else: + raiseAssert("Expected [menu][file] as string, but found " & $inj) + +Option matching +--------------- + +``Some(@x)`` and ``None()`` is a special case that will be rewritten into +``(isSome: true, get: @x)`` and ``(isNone: true)`` respectively. This is +made to allow better integration with optional types. [9]_ . + +Tree construction +================= + +``makeTree`` provides 'reversed' implementation of pattern matching, +which allows to *construct* tree from pattern, using variables. +Example of use + +.. code:: nim + + type + HtmlNodeKind = enum + htmlBase = "base" + htmlHead = "head" + htmlLink = "link" + + HtmlNode = object + kind*: HtmlNodeKind + text*: string + subn*: seq[HtmlNode] + + func add(n: var HtmlNode, s: HtmlNode) = n.subn.add s + + discard makeTree(HtmlNode): + base: + link(text: "hello") + +In order to construct tree, ``kind=`` and ``add`` have to be defined. +Internally DSL just creats resulting object, sets ``kind=`` and then +repeatedly ``add`` elements to it. In order to properties for objects +either the field has to be exported, or ``fld=`` has to be defined +(where ``fld`` is the name of property you want to set). + diff --git a/tests/stdlib/tmatching.nim b/tests/stdlib/tmatching.nim new file mode 100644 index 0000000000000..a6491c3453c3a --- /dev/null +++ b/tests/stdlib/tmatching.nim @@ -0,0 +1,2699 @@ +discard """ + output: ''' + +[Suite] Matching + +[Suite] Gara tests + +[Suite] More tests + +[Suite] stdlib container matches + +[Suite] Article examples +''' +""" + + +import std/[strutils, sequtils, strformat, sugar, + macros, options, tables, json] + +import std/matching +{.experimental: "caseStmtMacros".} +{.push hint[XDeclaredButNotUsed]: off.} +{.push hint[ConvFromXtoItselfNotNeeded]: off.} +{.push hint[CondTrue]: off.} + +import unittest + +template assertEq(a, b: untyped): untyped = + block: + let + aval = a + bval = b + + if aval != bval: + raiseAssert("Comparison failed in " & + $instantiationInfo() & + " [a: " & $aval & "] [b: " & $bval & "] ") + +template testFail(str: string = ""): untyped = + doAssert false #, "Fail on " & $instantiationInfo() & ": " & str + +template multitest(name: string, body: untyped): untyped = + test name: + block: + body + + block: + static: + body + +template multitestSince( + name: string, + minStaticVersion: static[(int,int, int)], + body: untyped): untyped = + + when (NimMajor, NimMinor, NimPatch) >= minStaticVersion: + multitest(name, body) + else: + test name: + body + + +suite "Matching": + test "Pattern parser tests": + macro main(): untyped = + template t(body: untyped): untyped = + block: + parseMatchExpr( + n = ( + quote do: + body + ) + ) + + doAssert (t true).kind == kItem + + block: + let s = t [1, 2, all @b, @a] + doAssert s.seqElems[3].bindVar == some(ident("a")) + doAssert s.seqElems[2].bindVar == some(ident("b")) + doAssert s.seqElems[2].patt.bindVar == none(NimNode) + + discard t([1,2,3,4]) + discard t((1,2)) + discard t((@a | @b)) + discard t((a: 12, b: 2)) + # dumpTree: [(0 .. 3) @patt is JString()] + # dumpTree: [0..3 @patt is JString()] + discard t([(0 .. 3) @patt is JString()]) + discard t([0..3 @patt is JString()]) + + let node = quote: (12 .. 33) + + block: + case node: + of Par [_]: + discard + else: + raiseAssert("#[ IMPLEMENT ]#") + + case node: + of Par[_]: + discard + else: + raiseAssert("#[ IMPLEMENT ]#") + + block: + node.assertMatch(Par [_] | Par [_]) + + node.assertMatch(Par[Infix()]) + node.assertMatch(Par [Infix()]) + node.assertMatch(Par[Infix ()]) + node.assertMatch(Par [Infix ()]) + + node.assertMatch(Par [Infix ()]) + + block: + + [ + Par [Infix [_, @lhs, @rhs]] | + Command [Infix [_, @lhs, @rhs]] | + Infix [@infixId, @lhs, @rhs] + ] := node + + doAssert lhs is NimNode + doAssert rhs is NimNode + doAssert infixId is Option[NimNode] + doAssert lhs == newLit(12) + doAssert rhs == newLit(33) + + block: + discard node.matches( + Call[ + BracketExpr[@ident, opt @outType], + @body + ] | + Command[ + @ident is Ident(), + Bracket[@outType], + @body + ] + ) + + doAssert body is NimNode, $typeof(body) + doAssert ident is NimNode, $typeof(ident) + doAssert outType is Option[NimNode] + + block: + case node: + of Call[BracketExpr[@ident, opt @outType], @body] | + Command[@ident is Ident(), Bracket [@outType], @body] + : + static: + doAssert ident is NimNode, $typeof(ident) + doAssert body is NimNode, $typeof(body) + doAssert outType is Option[NimNode], $typeof(outType) + + of Call[@head is Ident(), @body]: + static: + doAssert head is NimNode + doAssert body is NimNode + + main() + + test "Pattern parser broken brackets": + block: JArray[@a, @b] := %*[1, 3] + block: (JArray [@a, @b]) := %*[1, 3] + block: (JArray [@a, @b is JString()]) := %[%1, %"hello"] + block: (JArray [@a, @b is JString ()]) := %[%1, %"hello"] + block: (JArray [ + @a, @b is JString (getStr: "hello")]) := %[%1, %"hello"] + + block: + let vals = @[ + %*[["AA", "BB"], "CC"], + %*["AA", ["BB"], "CC"] + ] + + template testTypes() {.dirty.} = + doAssert aa is JsonNode + doAssert bb is Option[JsonNode] + doAssert cc is JsonNode + + doAssert aa == %"AA" + if Some(@bb) ?= bb: doAssert bb == %"BB" + doAssert cc == %"CC" + + + for val in vals: + case val: + of JArray[JArray[@aa, opt @bb], @cc] | + JArray[@aa, JArray[@bb], @cc] + : + testTypes() + else: + testFail($val) + + block: + val.assertMatch( + JArray[JArray[@aa, opt @bb], @cc] | + JArray[@aa, JArray[@bb], @cc] + ) + + testTypes() + + block: + val.assertMatch: + JArray[JArray[@aa, opt @bb], @cc] | + JArray[@aa, JArray[@bb], @cc] + + testTypes() + + block: + val.assertMatch: + JArray [ JArray [@aa, opt @bb] , @cc] | + JArray [@aa, JArray [@bb], @cc] + + testTypes() + + block: + val.assertMatch: + JArray [ + JArray [ + @aa, + opt @bb + ] , + @cc + ] | + JArray [@aa, JArray [ + @bb], @cc] + + testTypes() + + + test "Simple uses": + case (12, 24): + of (_, 24): + discard + else: + raiseAssert("#[ not possible ]#") + + + case [1]: + of [_]: discard + + case [1,2,3,4]: + of [_]: testfail() + of [_, 2, 3, _]: + discard + + case (1, 2): + of (3, 4), (1, 2): + discard + else: + testFail() + + + assertEq "hehe", case (true, false): + of (true, _): "hehe" + else: "2222" + + doAssert (a: 12) ?= (a: 12) + assertEq "hello world", case (a: 12, b: 12): + of (a: 12, b: 22): "nice" + of (a: 12, b: _): "hello world" + else: "default value" + + doAssert (a: 22, b: 90) ?= (a: 22, b: 90) + block: + var res: string + + case (a: 22, b: 90): + of (b: 91): + res = "900999" + elif "some other" == "check": + res = "rly?" + elif true: + res = "default fallback" + + else: + raiseAssert("#[ not possible ! ]#") + + assertEq res, "default fallback" + + assertEq "000", case %{"hello" : %"world"}: + of {"999": _}: "nice" + of {"hello": _}: "000" + else: "discard" + + assertEq 1, case [(1, 3), (3, 4)]: + of [(1, _), _]: 1 + else: 999 + + assertEq 2, case (true, false): + of (true, true) | (false, false): 3 + else: 2 + + multitest "Len test": + macro e(body: untyped): untyped = + case body: + of Bracket([Bracket(len: in {1 .. 3})]): + newLit("Nested bracket !") + + of Bracket(len: in {3 .. 6}): + newLit(body.toStrLit().strVal() & " matched") + + else: + newLit("not matched") + + discard e([2,3,4]) + discard e([[1, 3, 4]]) + discard e([3, 4]) + + + multitest "Regular objects": + type + A1 = object + f1: int + + case A1(f1: 12): + of (f1: 12): + discard "> 10" + else: + testFail() + + assertEq 10, case A1(f1: 90): + of (f1: 20): 80 + else: 10 + + multitest "Private fields": + type + A2 = object + hidden: float + + func public(a: A2): string = $a.hidden + + case A2(): + of (public: _): + discard + else: + testFail() + + case A2(hidden: 8.0): + of (public: "8.0"): discard + else: testFail() + + type + En2 = enum + enEE + enEE1 + enZZ + + Obj2 = object + case kind: En2 + of enEE, enEE1: + eee: seq[Obj2] + of enZZ: + fl: int + + + multitest "Case objects": + case Obj2(): + of EE(): + discard + of ZZ(): + testFail() + + case Obj2(): + of (kind: in {enEE, enZZ}): discard + else: + testFail() + + + when false: # FIXME + const eKinds = {enEE, enEE1} + case Obj2(): + of (kind: in {enEE} + eKinds): discard + else: + testFail() + + case (c: (a: 12)): + of (c: (a: _)): discard + else: testfail() + + case [(a: 12, b: 3)]: + of [(a: 12, b: 22)]: testfail() + of [(a: _, b: _)]: discard + + case (c: [3, 3, 4]): + of (c: [_, _, _]): discard + of (c: _): testfail() + + case (c: [(a: [1, 3])]): + of (c: [(a: [_])]): testfail() + else: discard + + case (c: [(a: [1, 3]), (a: [1, 4])]): + of (c: [(a: [_]), _]): testfail() + else: + discard + + case Obj2(kind: enEE, eee: @[Obj2(kind: enZZ, fl: 12)]): + of enEE(eee: [(kind: enZZ, fl: 12)]): + discard + else: + testfail() + + case Obj2(): + of enEE(): + discard + of enZZ(): + testfail() + else: + testfail() + + case Obj2(): + of (kind: in {enEE, enEE1}): + discard + else: + testfail() + + func len(o: Obj2): int = o.eee.len + iterator items(o: Obj2): Obj2 = + for item in o.eee: + yield item + + multitest "Object items": + case Obj2(kind: enEE, eee: @[Obj2(), Obj2()]): + of [_, _]: + discard + else: + testfail() + + case Obj2(kind: enEE, eee: @[Obj2(), Obj2()]): + of EE(eee: [_, _, _]): testfail() + of EE(eee: [_, _]): discard + else: testfail() + + case Obj2(kind: enEE1, eee: @[Obj2(), Obj2()]): + of EE([_, _]): + testfail() + of EE1([_, _, _]): + testfail() + of EE1([_, _]): + discard + else: + testfail() + + + + multitest "Variable binding": + when false: # NOTE compilation error test + case (1, 2): + of ($a, $a, $a, $a): + discard + else: + testfail() + + assertEq "122", case (a: 12, b: 2): + of (a: @a, b: @b): $a & $b + else: "✠ ♰ ♱ ☩ ☦ ☨ ☧ ⁜ ☥" + + assertEq 12, case (a: 2, b: 10): + of (a: @a, b: @b): a + b + else: 89 + + assertEq 1, case (1, (3, 4, ("e", (9, 2)))): + of (@a, _): a + of (_, (@a, @b, _)): a + b + of (_, (_, _, (_, (@c, @d)))): c * d + else: 12 + + proc tupleOpen(a: (bool, int)): int = + case a: + of (true, @capture): capture + else: -90 + + assertEq 12, tupleOpen((true, 12)) + + multitest "Infix": + macro a(): untyped = + case newPar(ident "1", ident "2"): + of Par([@ident1, @ident2]): + doAssert ident1.strVal == "1" + doAssert ident2.strVal == "2" + else: + doAssert false + + a() + + multitest "Iflet 2": + macro ifLet2(head: untyped, body: untyped): untyped = + case head[0]: + of Asgn([@lhs is Ident(), @rhs]): + result = quote do: + let expr = `rhs` + if expr.isSome(): + let `lhs` = expr.get() + `body` + else: + head[0].expectKind({nnkAsgn}) + head[0][0].expectKind({nnkIdent}) + error("Expected assgn expression", head[0]) + + ifLet2 (nice = some(69)): + doAssert nice == 69 + + + when (NimMajor, NimMinor, NimPatch) >= (1, 2, 0): + multitest "min": + macro min1(args: varargs[untyped]): untyped = + let tmp = genSym(nskVar, "minResult") + result = makeTree(NimNode): + StmtList: + VarSection: + IdentDefs: + == tmp + Empty() + == args[0] + + IfStmt: + == (block: + collect(newSeq): + for arg in args[1 .. ^1]: + makeTree(NimNode): + ElifBranch: + Infix[== ident("<"), @arg, @tmp] + Asgn [@tmp, @arg] + ) + + == tmp + + macro min2(args: varargs[untyped]): untyped = + let tmp = genSym(nskVar, "minResult") + result = makeTree(NimNode): + StmtList: + VarSection: + IdentDefs: + ==tmp + Empty() + ==args[0] + + IfStmt: + all == ( + block: + collect(newSeq): + for i in 1 ..< args.len: + makeTree(NimNode): + ElifBranch: + Infix[== ident("<"), ==args[i], ==tmp] + Asgn [== tmp, ==args[i]] + ) + + == tmp + + doAssert min1("a", "b", "c", "d") == "a" + doAssert min2("a", "b", "c", "d") == "a" + + multitest "Alternative": + assertEq "matched", case (a: 12, c: 90): + of (a: 12 | 90, c: _): "matched" + else: "not matched" + + assertEq 12, case (a: 9): + of (a: 9 | 12): 12 + else: 666 + + + multitest "Set": + case [3]: + of [{2, 3}]: discard + else: testfail() + + [{'a' .. 'z'}, {' ', '*'}] := "z " + + "hello".assertMatch([all @ident in {'a' .. 'z'}]) + "hello:".assertMatch([pref in {'a' .. 'z'}, opt {':', '-'}]) + + multitest "Match assertions": + [1,2,3].assertMatch([all @res]); assertEq res, @[1,2,3] + [1,2,3].assertMatch([all @res2]); assertEq res2, @[1,2,3] + [1,2,3].assertMatch([@first, all @other]) + assertEq first, 1 + assertEq other, @[2, 3] + + + block: [@first, all @other] := [1,2,3] + block: [_, _, _] := @[1,2,3] + block: (@a, @b) := ("1", "2") + block: (_, (@a, @b)) := (1, (2, 3)) + block: + let tmp = @[1,2,3,4,5,6,5,6] + block: [until @a == 6, .._] := tmp; assertEq a, @[1,2,3,4,5] + block: [@a, .._] := tmp; assertEq a, 1 + block: [any @a(it < 100)] := tmp; assertEq a, tmp + block: [pref @a is (1|2|3)] := [1,2,3]; assertEq a, @[1,2,3] + block: [pref (1|2|3)] := [1,2,3] + block: [until 3, _] := [1,2,3] + block: [all 1] := [1,1,1] + block: doAssert [all 1] ?= [1,1,1] + block: doAssert not ([all 1] ?= [1,2,3]) + block: [opt @a or 12] := `@`[int]([]); assertEq a, 12 + block: [opt(@a or 12)] := [1]; assertEq a, 1 + block: [opt @a] := [1]; assertEq a, some(1) + block: [opt @a] := `@`[int]([]); assertEq a, none(int) + block: [opt(@a)] := [1]; assertEq a, some(1) + block: + {"k": opt @val1 or "12"} := {"k": "22"}.toTable() + static: doAssert val1 is string + {"k": opt(@val2 or "12")} := {"k": "22"}.toTable() + static: doAssert val2 is string + assertEq val1, val2 + assertEq val1, "22" + assertEq val2, "22" + + block: + {"h": Some(@x)} := {"h": some("22")}.toTable() + doAssert x is string + doAssert x == "22" + + block: + {"k": opt @val, "z": opt @val2} := {"z" : "2"}.toTable() + doAssert val is Option[string] + doAssert val.isNone() + doAssert val2 is Option[string] + doAssert val2.isSome() + doAssert val2.get() == "2" + + block: [all(@a)] := [1]; assertEq a, @[1] + block: (f: @hello is ("2" | "3")) := (f: "2"); assertEq hello, "2" + block: (f: @a(it mod 2 == 0)) := (f: 2); assertEq a, 2 + block: doAssert not ([1,2] ?= [1,2,3]) + block: doAssert [1, .._] ?= [1,2,3] + block: doAssert [1,2,_] ?= [1,2,3] + block: + ## Explicitly use `_` to match whole sequence + [until @head is 'd', _] := "word" + ## Can also use trailing `.._` + [until 'd', .._] := "word" + assertEq head, @['w', 'o', 'r'] + + block: + [ + [@a, @b], + [@c, @d, all @e], + [@f, @g, all @h] + ] := @[ + @[1,2], + @[2,3,4,5,6,7], + @[5,6,7,2,3,4] + ] + + block: (@a, (@b, @c), @d) := (1, (2, 3), 4) + block: (Some(@x), @y) := (some(12), none(float)) + block: @hello != nil := (var tmp: ref int; new(tmp); tmp) + + block: [all @head] := [1,2,3]; assertEq head, @[1,2,3] + block: [all (1|2|3|4|5)] := [1,2,3,4,1,1,2] + block: + [until @head is 2, all @tail] := [1,2,3] + + assertEq head, @[1] + assertEq tail, @[2,3] + + block: (_, _, _) := (1, 2, "fa") + block: ([1,2,3]) := [1,2,3] + block: ({0: 1, 1: 2, 2: 3}) := {0: 1, 1: 2, 2: 3}.toTable() + + + block: + block: [0..3 is @head] := @[1,2,3,4] + + case [%*"hello", %*"12"]: + of [any @elem is JString()]: + discard + else: + testfail() + + case ("foo", 78) + of ("foo", 78): + discard + of ("bar", 88): + testfail() + + block: Some(@x) := some("hello") + + if (Some(@x) ?= some("hello")) and + (Some(@y) ?= some("world")): + assertEq x, "hello" + assertEq y, "world" + else: + discard + + multitest "More examples": + func butLast(a: seq[int]): int = + case a: + of []: raiseAssert( + "Cannot take one but last from empty seq!") + of [_]: raiseAssert( + "Cannot take one but last from seq with only one element!") + of [@pre, _]: + return pre + of [_, all @tail]: + return butLast(tail) + else: + raiseAssert("Not possible") + + assertEq butLast(@[1,2,3,4]), 3 + + func butLastGen[T](a: seq[T]): T = + expand case a: + of []: raiseAssert( + "Cannot take one but last from empty seq!") + of [_]: raiseAssert( + "Cannot take one but last from seq with only one element!") + of [@pre, _]: pre + of [_, all @tail]: butLastGen(tail) + else: raiseAssert("Not possible") + + assertEq butLastGen(@["1", "2"]), "1" + + multitest "Use in generics": + func hello[T](a: seq[T]): T = + [@head, .._] := a + return head + + doAssert hello(@[1,2,3]) == 1 + + proc g1[T](a: seq[T]): T = + case a: + of [@a]: discard + else: testfail() + + expand case a: + of [_]: discard + else: testfail() + + expand case a: + of [_.startsWith("--")]: discard + else: testfail() + + expand case a: + of [(len: < 12)]: discard + else: testfail() + + discard g1(@["---===---=="]) + + + test "Predicates": + case ["hello"]: + of [_.startsWith("--")]: + testfail() + of [_.startsWith("==")]: + testfail() + else: + discard + + + [all _(it < 10)] := [1,2,3,5,6] + [all < 10] := [1,2,3,4] + [all (len: < 10)] := [@[1,2,3,4], @[1,2,3,4]] + [all _.startsWith("--")] := @["--", "--", "--=="] + + block: [@a.startsWith("--")] := ["--12"] + + proc exception() = + # This should generate quite nice exception message: + + # Match failure for pattern 'all _.startsWith("--")' expected + # all elements to match, but item at index 2 failed + [all _.startsWith("--")] := @["--", "--", "=="] + + expect MatchError: + exception() + + multitest "One-or-more": + case [1]: + of [@a]: assertEq a, 1 + else: testfail() + + case [1]: + of [all @a]: assertEq a, @[1] + else: testfail() + + case [1,2,3,4]: + of [_, until @a is 4, 4]: + assertEq a, @[2,3] + else: + testfail() + + + case [1,2,3,4]: + of [@a, .._]: + doAssert a is int + doAssert a == 1 + else: + testfail() + + + case [1,2,3,4]: + of [all @a]: + doAssert a is seq[int] + doAssert a == @[1,2,3,4] + else: + testfail() + + multitest "Optional matches": + case [1,2,3,4]: + of [pref @a is (1 | 2), _, opt @a or 5]: + assertEq a, @[1,2,4] + + + case [1,2,3]: + of [pref @a is (1 | 2), _, opt @a or 5]: + assertEq a, @[1,2,5] + + case [1,2,2,1,1,1]: + of [all (1 | @a)]: + doAssert a is seq[int] + assertEq a, @[2, 2] + + multitest "Tree construction": + macro testImpl(): untyped = + let node = makeTree(NimNode): + IfStmt[ + ElifBranch[== ident("true"), + Call[ + == ident("echo"), + == newLit("12")]]] + + + IfStmt[ElifBranch[@head, Call[@call, @arg]]] := node + assertEq head, ident("true") + assertEq call, ident("echo") + assertEq arg, newLit("12") + + block: + let input = "hello" + # expandMacros: + Ident(str: @output) := makeTree(NimNode, Ident(str: input)) + assertEq output, input + + + testImpl() + + type + HtmlNodeKind = enum + htmlBase = "base" + htmlHead = "head" + htmlLink = "link" + + HtmlNode = object + kind*: HtmlNodeKind + text*: string + subn*: seq[HtmlNode] + + func add(n: var HtmlNode, s: HtmlNode) = n.subn.add s + + func len(n: HtmlNode): int = n.subn.len + iterator items(node: HtmlNode): HtmlNode = + for sn in node.subn: + yield sn + + multitest "Match assertions custom type; treeRepr syntax": + HtmlNode(kind: htmlBase).assertMatch: + Base() + + HtmlNode( + kind: htmlLink, text: "text-1", subn: @[ + HtmlNode(kind: htmlHead, text: "text-2")] + ).assertMatch: + Link(text: "text-1"): + Head(text: "text-2") + + + HtmlNode( + kind: htmlLink, subn: @[ + HtmlNode(kind: htmlHead, text: "text-2"), + HtmlNode(kind: htmlHead, text: "text-3", subn: @[ + HtmlNode(kind: htmlBase, text: "text-4"), + HtmlNode(kind: htmlBase, text: "text-5") + ]) + ] + ).assertMatch: + Link: + Head(text: "text-2") + Head(text: "text-3"): + Base(text: "text-4") + Base() + + + multitestSince "Tree builder custom type", (1, 4, 0): + + discard makeTree(HtmlNode, Base()) + discard makeTree(HtmlNode, base()) + discard makeTree(HtmlNode, base([link()])) + discard makeTree(HtmlNode): + base: + link(text: "hello") + + template wrapper1(body: untyped): untyped = + makeTree(HtmlNode): + body + + template wrapper2(body: untyped): untyped = + makeTree(HtmlNode, body) + + let tmp1 = wrapper1: + base: link() + base: link() + + doAssert tmp1 is seq[HtmlNode] + + + let tmp3 = wrapper1: + base: + base: link() + base: link() + + doAssert tmp3 is HtmlNode + + let tmp2 = wrapper1: + base: + link() + + doAssert tmp2 is HtmlNode + + discard wrapper2: + base: + link() + + + multitest "Tree construction sequence operators": + block: + let inTree = makeTree(HtmlNode): + base: + link(text: "link1") + link(text: "link2") + + inTree.assertMatch: + base: + all @elems + + let inTree3 = makeTree(HtmlNode): + base: + all @elems + + assertEq inTree3, inTree + + + + multitest "withItCall": + macro withItCall(head: typed, body: untyped): untyped = + result = newStmtList() + result.add quote do: + var it {.inject.} = `head` + + for stmt in body: + case stmt: + of ( + kind: in {nnkCall, nnkCommand}, + [@head is Ident(), all @arguments] + ): + result.add newCall(newDotExpr( + ident "it", head + ), arguments) + else: + result.add stmt + + result.add ident("it") + + result = newBlockStmt(result) + + + let res {.used.} = @[12,3,3].withItCall do: + it = it.filterIt(it < 4) + it.add 99 + + + multitest "Examples from documentation": + block: [@a] := [1]; doAssert (a is int) and (a == 1) + block: + {"key" : @val} := {"key" : "val"}.toTable() + doAssert val is string + doAssert val == "val" + + block: [any @a] := [1,2,3]; doAssert a is seq[int] + block: + [any @a(it < 3)] := [1, 2, 3] + doAssert a is seq[int] + assertEq a, @[1, 2] + + block: + [until @a == 6, _] := [1, 2, 3, 6] + doAssert a is seq[int] + doAssert a == @[1, 2, 3] + + block: + [all @a == 6] := [6, 6, 6] + doAssert a is seq[int] + doAssert a == @[6, 6, 6] + + block: + [any @a > 100] := [1, 2, 101] + + doAssert @a is seq[int] + assertEq @a, @[101] + + block: + [any @a(it > 100)] := [1, 2, 101] + [any @b > 100] := [1, 2, 101] + doAssert a == b + + block: + [_ in {2 .. 10}] := [2] + + block: + [any @a in {2 .. 10}] := [1, 2, 3] + [any in {2 .. 10}] := [1, 2, 3] + [any _ in {2 .. 10}] := [1, 2, 3] + + block: + [none @a in {6 .. 10}] := [1, 2, 3] + doAssert a is seq[int] + doAssert a == @[1, 2, 3] + + [none in {6 .. 10}] := [1, 2, 3] + [none @b(it in {6 .. 10})] := [1, 2, 3] + + block: + [opt @val or 12] := [1] + doAssert val is int + doAssert val == 1 + + block: + [_, opt @val] := [1] + doAssert val is Option[int] + doAssert val.isNone() + + block: + [0 .. 3 @val, _] := [1, 2, 3, 4, 5] + doAssert val is seq[int] + doAssert val == @[1, 2, 3, 4] + [0 .. 1 @val1, 2 .. 3 @val2] := [1, 2, 3, 4] + doAssert val1 is seq[int] and val1 == @[1, 2] + doAssert val2 is seq[int] and val2 == @[3, 4] + + block: + let val = (1, 2, "fa") + doAssert (_, _, _) ?= val + doAssert not ((@a, @a, _) ?= val) + + block: + case (true, false): + of (@a, @a): + testfail() + of (@a, _): + doAssert a == true + + block: + block: (fld: @val) := (fld: 12); doAssert val == 12 + block: (@val, _) := (12, 2); doAssert val == 12 + block: + (@val, @val) := (12, 12); doAssert val == 12 + block: doAssert (@a, @a) ?= (12, 12) + block: doAssert not ((@a, @a) ?= (12, 3)) + + block: + doAssert [_, _] ?= [12, 2] + doAssert not ([_, _] ?= [12, 2, 2]) + + block: + doAssert [_, .._] ?= [12] + doAssert not ([_, _, .._] ?= [12]) + + block: + [_, all @val] := [12, 2, 2]; doAssert val == @[2, 2] + + # Note that + block: + # Does not work, because `assert` internally uses `if` and + # all variables declared inside are not accesible to the + # outside scope + doAssert [_, all @val] ?= [12, 2, 2] + when false: # NOTE - will not compile + doAssert val == @[2, 2] + + block: + [until @val is 12, _] := [2, 13, 12] + doAssert val == @[2, 13] + + block: + [until @val is 12, @val] := [2, 13, 12] + doAssert val == @[2, 13, 12] + + + multitest "Generic types": + type + GenKind = enum + ptkToken + ptkNterm + + Gen[Kind, Lex] = ref object + kindFld: Kind + case tkind*: GenKind + of ptkNterm: + subnodes*: seq[Gen[Kind, Lex]] + of ptkToken: + lex*: Lex + + func add[K, L](g: var Gen[K, L], t: Gen[K, L]) = + g.subnodes.add t + + func kind[K, L](g: Gen[K, L]): K = g.kindFld + + block: + type + Kind1 = enum + k1_val + k2_val + k3_val + + const kTokens = {k1_val, k2_val} + block: + k1_val(lex: @lex) := Gen[Kind1, string]( + tkind: ptkToken, + kindFld: k1_val, + lex: "Hello" + ) + + func `kind=`(g: var Gen[Kind1, string], k: Kind1) = + if k in kTokens: + g = Gen[Kind1, string](kindFld: k, tkind: ptkToken) + else: + g = Gen[Kind1, string](kindFld: k, tkind: ptkNterm) + + + let tree = makeTree(Gen[Kind1, string]): + k3_val: + k2_val(lex: "Hello") + k1_val(lex: "Nice") + + doAssert tree.kind == k3_val + + block: + (lex: @lex) := Gen[void, string](tKind: ptkToken, lex: "hello") + + + + + + + multitest "Nested objects": + type + Lvl3 = object + f3: float + + Lvl2 = object + f2: Lvl3 + + Lvl1 = object + f1: Lvl2 + + doAssert Lvl1().f1.f2.f3 < 10 + doAssert (f1.f2.f3: < 10) ?= Lvl1() + + case Lvl1(): + of (f1.f2.f3: < 10): + discard + of (f1: (f2: (f3: < 10))): + discard + else: + testfail() + + multitest "Nested key access": + let val = (@[1,2,3], @[3,4,5]) + + case val: + of ((len: <= 3), (len: <= 3)): + discard + else: + testfail() + + let val2 = (hello: @[1,2,3]) + + case val2: + of (hello.len: <= 3): + discard + else: + testfail() + + + let val3 = (hello3: @[@[@["eee"]]]) + if false: discard (hello3[0][1][2].len: < 10) ?= val3 + doAssert (hello3[0][0][0].len: < 10) ?= val3 + doAssert (hello3: is [[[(len: < 10)]]]) ?= val3 + + test "Match failure exceptions": + try: + [all 12] := [2,3,4] + except MatchError: + let msg = getCurrentExceptionMsg() + doAssert "all 1" in msg + doAssert "all elements" in msg + + + expect MatchError: + [any 1] := [2,3,4] + + try: + [any 1] := [2,3,4] + + except MatchError: + let msg = getCurrentExceptionMsg() + doAssert "any 1" in msg + + [any is (1 | 2)] := [1, 2] + + try: + [_, any is (1 | 2)] := [3,4,5] + + testfail("_, any is (1 | 2)") + + except MatchError: + let msg = getCurrentExceptionMsg() + doAssert "any is (1 | 2)" in msg + doAssert "[any is (1 | 2)]" notin msg + + expect MatchError: + [none is 12] := [1, 2, 12] + + expect MatchError: + [_, _, _] := [1, 2] + + try: + [_, _, _] := [1, 2] + except MatchError: + doAssert "range '3 .. 3'" in getCurrentExceptionMsg() + + try: + [_, opt _] := [1, 2, 3] + except MatchError: + doAssert "range '1 .. 2'" in getCurrentExceptionMsg() + + + try: + [(1 | 2)] := [3] + testfail("[(1 | 2)] := [3]") + except MatchError: + doAssert "pattern '(1 | 2)'" in getCurrentExceptionMsg() + + + 1 := 1 + + expect MatchError: + 1 := 2 + + expect MatchError: + (1, 2) := (2, 1) + + expect MatchError: + (@a, @a) := (2, 3) + + expect MatchError: + (_(it < 12), 1) := (14, 1) + + + test "Positional matching": + [0 is 0] := @[0] + + expect MatchError: + [1 is 0] := @[0] + + + test "Compilation errors": + # NOTE that don't know how to correctly test compilation errors, + # /without/ actuall failing compilation, so I just set `when true` + # things to see if error is correct + + when false: # Invalid field. NOTE - I'm not sure whether this + # should be allowed or not, so for now it is disabled. But + # technically this should not be that hard to allow explicit + # function calls as part of path expressions. + + # Error: Malformed path access - expected either field name, or + # bracket access, but found 'fld.call()' of kind nnkCall + (fld.call(): _) := 12 + + + multitest "Use in templates": + template match1(a: typed): untyped = + [@nice, @hh69] := a + + match1([12, 3]) + + doAssert nice == 12 + doAssert hh69 == 3 + + + type + Root = ref object of RootObj + fld1: int + fld2: float + + SubRoot = ref object of Root + fld3: int + + + multitest "Ref object field matching": + case (fld3: 12): + of (fld3: @subf): + discard + else: + testfail() + + var tmp: Root = SubRoot(fld3: 12) + doAssert tmp.SubRoot().fld3 == 12 + case tmp: + of of SubRoot(fld3: @subf): + doAssert subf == 12 + else: + testfail() + + multitest "Ref object in maps, subfields and sequences": + block: + @[SubRoot(), Root()].assertMatch([any of SubRoot()]) + + block: + @[SubRoot(fld1: 12), Root(fld1: 33)].assertMatch( + [all of Root(fld1: @vals)]) + + assertEq vals, @[12, 33] + + block: + let val = {12 : SubRoot(fld1: 33)}.toTable() + + val.assertMatch({ + 12 : of SubRoot(fld1: @fld1) + }) + + + val.assertMatch({ + 12 : of Root(fld1: fld1) + }) + + assertEq fld1, 33 + + test "Multiple kinds of derived objects": + type + Base1 = ref object of RootObj + fld: int + + First1 = ref object of Base1 + first: float + + Second1 = ref object of Base1 + second: string + + let elems: seq[Base1] = @[ + Base1(fld: 123), + First1(fld: 456, first: 0.123), + Second1(fld: 678, second: "test"), + nil + ] + + for elem in elems: + case elem: + of of First1(fld: @capture1, first: @first): + # Only capture `Frist1` elements + doAssert capture1 == 456 + doAssert first == 0.123 + + of of Second1(fld: @capture2, second: @second): + # Capture `second` field in derived object + doAssert capture2 == 678 + doAssert second == "test" + + of of Base1(fld: @default): + # Match all *non-nil* base elements + doAssert default == 123 + + else: + doAssert isNil(elem) + + var first: Base1 = First1() + doAssert matches(first, of First1(first: @tmp2)) + doAssert not matches(first, of Second1(second: @tmp3)) + + test "non-derived ref type": + type + RefType = ref object + RegType = object + fld: float + + doAssert matches(RefType(), of RefType()) + + doAssert matches(RegType(fld: 0.123), RegType(fld: @capture)) + doAssert matches(RegType(fld: 0.123), RegType(fld: 0.123)) + + let varn = RegType() + doAssert matches(varn, RegType()) + + var zzz: RegType + doAssert matches(addr zzz, of RegType()) + + var pt: ptr RegType = nil + doAssert not matches(pt, of RegType()) + + multitest "Custom object unpackers": + type + Point = object + x: int + y: int + metadata: string ## Some field that you dont' want to unpack + + proc `[]`(p: Point, idx: static[FieldIndex]): auto = + when idx == 0: + p.x + elif idx == 1: + p.y + else: + static: + error("Cannot unpack `Point` into three-tuple") + + let point = Point(x: 12, y: 13) + + (@x, @y) := point + + assertEq x, 12 + assertEq y, 13 + + + test "Nested access paths": + case [[[[[[12]]]]]]: + of [@test]: discard + of [[@test]]: discard + of [[[[[@test]]]]]: discard + + case (a: (b: (c: 12))): + of (a: @hello): discard + of (a: (b: @hello)): discard + of (a.b: @hello): discard + of (a.b.c: @hello): discard + of (a.b.c: 12): discard + + (a: (b: (c: 12))) := (a: (b: (c: 12))) + (a.b.c: 12) := (a: (b: (c: 12))) + (a[0][0]: 12) := (a: (b: (c: 12))) + + case (a: [2]): + of (a: @val): discard + of (a[0]: @val): discard + of (a[^1]: @val): discard + + + + + + +suite "Gara tests": + ## Test suite copied from gara pattern matching + type + Rectangle = object + a: int + b: int + + Repo = ref object + name: string + author: Author + commits: seq[Commit] + + Author = object + name: string + email: Email + + Email = object + raw: string + + # just an example: nice for match + CommitType = enum ctNormal, ctMerge, ctFirst, ctFix + + Commit = ref object + message: string + case kind: CommitType: + of ctNormal: + diff: string # simplified + of ctMerge: + original: Commit + other: Commit + of ctFirst: + code: string + of ctFix: + fix: string + + multitest "Capturing": + let a = 2 + case [a]: ## Wrap in `[]` to trigger `match` macro, otherwise it + ## will be treated as regular case match. + of [@b == 2]: + assertEq b, 2 + else: + testfail() + + + multitest "Object": + let a = Rectangle(a: 2, b: 0) + + case a: + of (a: 4, b: 1): + testfail() + of (a: 2, b: @b): + assertEq b, 0 + else : + testfail() + + multitest "Subpattern": + let repo = Repo( + name: "ExampleDB", + author: Author( + name: "Example Author", + email: Email(raw: "example@exampledb.org")), + commits: @[ + Commit(kind: ctFirst, message: "First", code: "e:0"), + Commit(kind: ctNormal, message: "Normal", diff: "+e:2\n-e:0") + ]) + + + case repo: + of (name: "New", commits: == @[]): + testfail() + of ( + name: @name, + author: ( + name: "Example Author", + email: @email + ), + commits: @commits + ): + assertEq name, "ExampleDB" + assertEq email.raw, "example@exampledb.org" + else: + testfail() + + test "Sequence": + let a = @[ + Rectangle(a: 2, b: 4), + Rectangle(a: 4, b: 4), + Rectangle(a: 4, b: 4) + ] + + case a: + of []: + testfail() + of [_, all @others is (a: 4, b: 4)]: + assertEq others, a[1 .. ^1] + else: + testfail() + + # _ is always true, (a: 4, b: 4) didn't match element 2 + + # _ is alway.. a.a was 4, but a.b wasn't 4 => not a match + + block: + [until @vals == 5, .._] := @[2, 3, 4, 5] + doAssert vals == @[2, 3, 4] + + + block: + [@a, @b] := @[2, 3] + + + test "Sequence subpattern": + let a = @[ + Rectangle(a: 2, b: 4), + Rectangle(a: 4, b: 0), + Rectangle(a: 4, b: 4), + Rectangle(a: 4, b: 4) + ] + + case a: + of []: + fail() + of [_, _, all (a: @list)]: + check(list == @[4, 4]) + else: + fail() + + test "Sequence subpatterns 2": + let inseq = @[1,2,3,4,5,6,5,6] + + [0 .. 2 is < 10, .._] := inseq + doAssert not matches(inseq, [0 .. 2 is < 10]) + [0 .. 2 @elems1 is < 10, .._] := inseq + doAssert elems1 == @[1, 2, 3] + + [12] := [12] + [[12]] := [[12]] + [[12], [13]] := [[12], [13]] + [0 .. 2 is 12] := [12, 12, 12] + # [0 is 12] := [12] + [^1 is 12] := [12] + + expect MatchError: + [^1 is 12] := [13] + + [^1 is (12, 12)] := [(12, 12)] + + [0 is 12, ^1 is 13] := [12, 13] + + expect MatchError: + [0 is 12, ^1 is 13] := [12, 14] + + + expect MatchError: + [0 is 2, ^1 is 13] := [12, 13] + + + expect MatchError: + # NOTE that's not how it supposed to be used, but it should work + # anyway. + [^1 is 13, 0 is 2] := [12, 13] + + [^1 is 13, 0 is 2] := [2, 13] + + + # [^1 is 13, 0 is 2] := [12, 13] + + + test "Variant": + let a = Commit(kind: ctNormal, message: "e", diff: "z") + + case a: + of Merge(original: @original, other: @other): + fail() + of Normal(message: @message): + check(message == "e") + else: + fail() + + multitest "Custom unpackers": + let repo = Repo( + name: "ExampleDB", + author: Author( + name: "Example Author", + email: Email(raw: "example@exampledb.org")), + commits: @[ + Commit(kind: ctFirst, message: "First", code: "e:0"), + Commit(kind: ctNormal, message: "Normal", diff: "+e:2\n-e:0") + ]) + + let email = repo.author.email + + proc data(email: Email): tuple[name: string, domain: string] = + let words = email.raw.split('@', 1) + (name: words[0], domain: words[1]) + + proc tokens(email: Email): seq[string] = + # work for js slow + result = @[] + var token = "" + for i, c in email.raw: + if not c.isAlphaNumeric(): + if token.len > 0: + result.add(token) + token = "" + result.add($c) + else: + token.add(c) + if token.len > 0: + result.add(token) + + # WARNING multiple calls for `tokens`. It might be possible to + # wrap each field access into macro helper that determines + # whether or not expressions is a function, or just regular + # field access, and cache execution results, but right now I + # have no idea how to implement this without redesing of the + # whole pattern matching construction (and even /with it/ I'm + # not really sure). So the thing is - expression like + when false: + (tokens: [@token, @token]) ?= Email() + # Internallt access `tokens` multiple times - to get number of + # elements, and each element. E.g. assumption was made that + # `obj.tokens` is a cheap expression to evaluate. But in this + # case we get + when false: + let expr_25420001 = Email_25095022() + block failBlock: + var pos_25420002 = 0 + if not contains({2..2}, len(tokens(expr_25420001))): # Call to `tokens` + break failBlock + ## lkPos @token + ## Set variable token vkRegular + if token == tokens(expr_25420001)[pos_25420002]: # Second call + true + # ... + ## lkPos @token + ## Set variable token vkRegular + if token == tokens(expr_25420001)[pos_25420002]: # Third call + true + # ... + + # Access `tokens` for different indices - `pos_25420002`. Using + # intermediate variable is not an option, since this would create + # copies each time, for each field access. But! this is not that + # big of an issue if we can `lent` everything, so this can + # probably be solved when view types become more stable. And no, + # it is not possible to determien whether or not `tokens` is a + # field or not, since pattern matching DSL does not have + # information about structure of the object being matched - this + # is one of the main assumptions that was made, so changing this + # is not possible without complete redesign and severe cuts in + # functionality. + + + # Note that above this just what I think at the moment, I would be + # glad if someone told me I'm missing something. + + + case email: + of (data: (name: "academy")): + testfail() + + of (tokens: [_, _, _, _, @token]): + assertEq token, "org" + + multitest "if": + let b = @[4, 0] + + case b: + of [_, @t(it mod 2 == 0)]: + assertEq t, 0 + else: + testfail() + + multitest "unification": + let b = @["nim", "nim", "c++"] + + var res = "" + case ["nim", "nim", "C++"]: + of [@x, @x, @x]: discard + of [@x, @x, _]: res = x + + assertEq res, "nim" + + + + case b: + of [@x, @x, @x]: + testfail() + of [@x, @x, _]: + assertEq x, "nim" + else: + testfail() + + multitest "option": + let a = some[int](3) + + case a: + of Some(@i): + assertEq i, 3 + else: + testfail() + + + multitest "nameless tuple": + let a = ("a", "b") + + case a: + of ("a", "c"): + testfail() + of ("a", "c"): + testfail() + of ("a", @c): + assertEq c, "b" + else: + testfail() + + multitest "ref": + type + Node = ref object + name: string + children: seq[Node] + + let node = Node(name: "2") + + case node: + of (name: @name): + assertEq name, "2" + else: + testfail() + + let node2: Node = nil + + case node2: + of (isNil: false, name: "4"): + testfail() + else: + discard + + multitest "weird integers": + let a = 4 + + case [a]: + of [4'i8]: + discard + else: + testfail() + + multitest "dot access": + let a = Rectangle(b: 4) + + case a: + of (b: == a.b): + discard + else: + testfail() + + multitest "arrays": + let a = [1, 2, 3, 4] + + case a: + of [1, @a, 3, @b, 5]: + testfail() + of [1, @a, 3, @b]: + assertEq a, 2 + assertEq b, 4 + else: + testfail() + + multitest "bool": + let a = Rectangle(a: 0, b: 0) + + if a.matches((b: 0)): + discard + else: + testfail() + +suite "More tests": + multitest "Matching boolean tables": + case (true, false, false): + of (true, false, false): + discard + + of (false, true, true): + testFail("Impossible") + + else: + testFail("Impossible") + + + test "Funcall results": + [@head, all @trail] := split("a|b|c|d|e", '|') + doAssert head == "a" + doAssert trail == @["b", "c", "d", "e"] + + case split("1,2,3,4,5", ','): + of [@head == "1", until @skip == "5", .._]: + doAssert skip == @["2", "3", "4"] + doAssert head == "1" + + else: + testFail("Pattern failed") + + + multitest "Enumparse": + type + Dir1 = enum + dirUp + dirDown + + proc parseDirection(str: string): Dir1 = + case str: + of "up": dirUp + of "down": dirDown + else: + raiseAssert( + &"Incorrect direction string expected up/down, but found: {str}") + + + for cmd in ["quit", "look", "get test", "go up", "drop a b c d"]: + case cmd.split(" "): + of ["quit"]: + doAssert "quit" in cmd + + of ["look"]: + doAssert "look" in cmd + + of ["get", @objectName]: + doAssert "get" in cmd + doASsert objectName == "test" + + of ["go", (parseDirection: @direction)]: + case direction: + of dirUp: + doAssert "go up" in cmd + + else: + testFail("Wrong enum value parse") + + of ["drop", all @args]: + doAssert "drop" in cmd + doAssert args == @["a", "b", "c", "d"] + + else: + testFail("Unmatched command " & cmd) + + test "Composing array patterns": + for patt in [@["a", "b", "d"], @["a", "XXX", "d"]]: + case patt: + of ["z", @alt1] | ["q", @alt1]: + static: + doAssert alt1 is string + + of ["z", @alt2] | ["q", @alt3]: + static: + doAssert alt2 is Option[string] + doAssert alt3 is Option[string] + + + of ["a", "b"] | ["a", "b", @altTail]: + doAssert altTail is Option[string] + doAssert altTail.get() == "d" + + of ["a", "***", "d"] | ["a", _, "d"]: + doAssert patt == @["a", "XXX", "d"] + + else: + testFail("Unmatched patter " & $patt) + + test "Alternative subpattern capture": + case @["a", "b"]: + of ["a", @second is ("a"|"b"|"c")]: + doAssert second == "b" + + else: + testFail() + + test "Use external var in enum; explicit `case`": + let varname = 404 + match case 404: + of 200: + testFail() + + of varname: + doAssert true + + else: + testFail() + + + test "Nested custom unpacker": + type + UserType1 = object + fld1: float + fld2: string + case isDefault: bool + of true: fld3: float + of false: fld4: string + + UserType2 = object + userFld: UserType1 + fld4: float + + + proc `[]`(obj: UserType1, idx: static[FieldIndex]): auto = + when idx == 0: + obj.fld1 + + elif idx == 1: + obj.fld2 + + elif idx == 2: + if obj.isDefault: + obj.fld3 + + else: + obj.fld4 + + else: + static: + error("Indvalid index for `UserType1` field " & + "- expected value in range[0..2], but got " & $idx + ) + + proc `[]`(obj: UserType2, idx: static[FieldIndex]): auto = + when idx == 0: + obj.userFld + + elif idx == 1: + obj.fld4 + + else: + static: + error("Indvalid index for `UserType2` field " & + "- expected value in range[0..1], but got " & $idx + ) + + block: + (@fld1, @fld2, _) := UserType1(fld1: 0.1, fld2: "hello") + + doAssert fld1 == 0.1 + doAssert fld2 == "hello" + + block: + (fld1: @fld1, fld2: @fld2) := UserType1(fld1: 0.1, fld2: "hello") + + doAssert fld1 == 0.1 + doAssert fld2 == "hello" + + block: + ((@fld1, @fld2, _), _) := UserType2(userFld: UserType1(fld1: 0.1, fld2: "hello")) + + doAssert fld1 == 0.1 + doAssert fld2 == "hello" + + + test "`is`": + (a: is 42) := (a: 42) + (a: 42) := (a: 42) + (a: == 42) := (a: 42) + + # expandMacros: + (a: (@a, @b)) := (a: (1, 2)) + (a: (1, 2)) := (a: (1, 2)) + (a: == (1, 2)) := (a: (1, 2)) + + + test "Nested case objects": + type + Kind2 = enum + kkFirst + + Object4 = ref object + val: int + case kind: Kind2 + of kkFirst: + nested: Object4 + + block: + assertMatch( + Object4(kind: kkFirst, + nested: Object4( + kind: kkFirst, + nested: Object4( + kind: kkFirst, + val: 10))), + First( + nested: First( + nested: First( + val: @capture)))) + + doAssert capture == 10 + + test "Line scanner": + iterator splitLines(str: string, sep: set[char]): seq[string] = + for line in str.split(sep): + yield line.split(" ") + + for line in splitLines("# a|#+ b :|#+ begin_txt :", {'|'}): + case line: + of ["#+", @name.startsWith("begin"), .._]: + assertEq name, "begin_txt" + + of ["#+", @name, .._]: + assertEq name, "b" + + of ["#", @name, .._]: + assertEq name, "a" + + else: + testFail() + + test "Alt pattern in sequence of ref objects": + type + Base = ref object of RootObj + field1: int + + Derived1 = ref object of Base + derived1: string + + Derived2 = ref object of Base + derived2: string + + let objs = [ + Base(), Derived1(derived1: "hello"), Derived2(derived2: "world")] + + objs.assertMatch([any ( + of Derived1(derived1: @vals) | + of Derived2(derived2: @vals)) + ]) + + assertEq vals, @["hello", "world"] + + type + Ast1Kind = enum + akFirst1 + akSecond1 + akThird1 + + Ast1 = object + case kind1: Ast1Kind + of akFirst1: + first: string + of akSecond1: + second: int + of akThird1: + asdf: int + third: seq[Ast1] + + Ast2Kind = enum + akFirst2 + akSecond2 + akThird2 + + Ast2 = object + case kind2: Ast2Kind + of akFirst2: + first: string + of akSecond2: + second: int + of akThird2: + third: seq[Ast2] + + func `kind=`(a1: var Ast1, k: Ast1Kind) = a1 = Ast1(kind1: k) + func `kind=`(a2: var Ast2, k: Ast2Kind) = a2 = Ast2(kind2: k) + + func kind(a1: Ast1): Ast1Kind = a1.kind1 + func kind(a2: Ast2): Ast2Kind = a2.kind2 + + func add(a1: var Ast1, sub: Ast1) = a1.third.add sub + func add(a2: var Ast2, sub: Ast2) = a2.third.add sub + + func len(a1: Ast1): int = a1.third.len + + iterator items(a1: Ast1): Ast1 = + for it in a1.third: + yield it + + multitestSince "AST-AST conversion using pattern matching", (1, 2, 0): + func convert(a1: Ast1): Ast2 = + case a1: + of First1(first: @value): + return makeTree(Ast2): + First2(first: value & "Converted") + of Second1(second: @value): + return makeTree(Ast2): + Second2(second: value + 12) + of Third1(third: @subnodes): + return makeTree(Ast2): + Third2(third: == subnodes.map(convert)) + + let val = makeTree(Ast1): + Third1: + First1(first: "Someval") + First1(first: "Someval") + First1(first: "Someval") + Second1(second: 12) + + discard val.convert() + + test "Match tree with statement list": + Ast1().assertMatch: + First1() + + Ast1().assertMatch: + First1(first: "") + + Ast1(kind1: akThird1, third: @[Ast1(), Ast1()]).assertMatch: + Third1(asdf: 0): + First1() + First1() + + Ast1(kind1: akThird1, third: @[Ast1()]).assertMatch: + Third1(asdf: 0): + First1() + + Ast1().assertMatch: + First1(first: "") + + + test "Raise error": + expect MatchError: + Ast1().assertMatch: + Third1() + + expect MatchError: + Ast1().assertMatch: + First1(first: "zzzzzzzzzzz") + + expect MatchError: + Ast1(kind1: akThird1, third: @[Ast1(), Ast1()]).assertMatch: + Third1(asdf: 0): + Second1() + Third1() + + expect MatchError: + Ast1(kind1: akThird1, third: @[Ast1()]).assertMatch: + Third1(asdf: 0): + First1() + First1() + + expect MatchError: + Ast1(kind1: akFirst1).assertMatch: + Third1(first: "") + + test "Pure enums": + type + Pure1 {.pure.} = enum + left + right + + PureAst1 = object + kind: Pure1 + + left() := PureAst1(kind: Pure1.left) + + test "Fully qualified, snake case": + type + Snake1 = enum + sn_left + sn_right + + SnakeAst1 = object + kind: Snake1 + + sn_left() := SnakeAst1(kind: sn_left) + left() := SnakeAst1(kind: sn_left) + + test "Option patterns": + block: + [any @x < 12] := @[1, 2, 3] + [any @y is < 12] := @[1, 2, 3] + [any @z is 12] := @[12] + [any @w is == 12] := @[12] + + block: + Some(Some([any @x < 12])) := some(some(@[1, 2, 3])) + doAssert x is seq[int] + doAssert x == @[1, 2, 3] + + block: + [any @elems is Some()] := [none(int), some(12)] + doAssert elems is seq[Option[int]] + doAssert elems.len == 1 + doAssert elems[0].get() == 12 + + block: + [any is Some(@elem)] := [some(12)] + doAssert elem is seq[int] + doAssert elem.len == 1 + doAssert elem[0] == 12 + + block: + Some(Some(@x)) := some(some(12)) + doAssert x is int + doAssert x == 12 + + block: + None() := none(int) + + + block: + Some(Some(None())) := some some none int + + + +import std/[deques, lists] + +suite "stdlib container matches": + test "Ques": + var que = initDeque[(int, string)]() + que.addLast (12, "hello") + que.addLast (3, "iiiiii") + + [any (_ < 12, @vals)] := que + + assertEq vals, @["iiiiii"] + + + +suite "Article examples": + test "Object matching": + type + Obj = object + fld1: int8 + + func len(o: Obj): int = 0 + + case Obj(): + of (fld1: < -10): + testFail() + + of (len: > 10): + # can use results of function evaluation as fields - same idea as + # method call syntax in regular code. + testFail() + + of (fld1: in {1 .. 10}): + testFail() + + of (fld1: @capture): + doAssert capture == 0 + + else: + testFail() + + + + + test "Nested tuples unpacking": + (@a, (@b, _), _) := ("hello", ("world", 11), 0.2) + + test "Simple string scanner": + "2019 school start".assertMatch([ + # Capture all prefix integers + pref @year in {'0' .. '9'}, + # Then skip all whitespaces + until notin {' '}, + # And store remained of the string in `events` variable + all @event + ]) + + doAssert year == "2019".toSeq() + doAssert event == "school start".toSeq() + + test "Tokenized string scanner": + func allIs(str: string, chars: set[char]): bool = str.allIt(it in chars) + + "2019-10-11 school start".split({'-', ' '}).assertMatch([ + pref @dateParts(it.allIs({'0' .. '9'})), + pref _(it.allIs({' '})), + all @text + ]) + + doAssert dateParts == @["2019", "10", "11"] + doAssert text == @["school", "start"] + + + test "Pattern matching lexer": + type + Lexer = object + buf: seq[string] + bufpos: int + + var maxbuf: int = 0 + iterator items(lex: Lexer): string = + for i in lex.bufpos .. lex.buf.high: + maxbuf = max(maxbuf, i) + yield lex.buf[i] + + func len(lex: Lexer): int = lex.buf.len - lex.bufpos + func isAllnum(str: string): bool = str.allIt(it in {'0' .. '9'}) + + var lexer = Lexer(buf: @["2019", "10", "11", "hello", "world"]) + + + if lexer.matches([ + # `isAlnum` is converted to `[somePos].isAlnum == true`, and can be + # used to check for properties of particular sequence elements, even + # though there are no such fields in the element itself. + @year is (isAllnum: true, len: 4), + @month is (isAllnum: true, len: 2), + # Capturing results of procs is also possible, though there + # is no particular guarantee wrt. to number of times proc + # could be executed, so some caution is necessary. + (isAllnum: true, len: 2, parseInt: @day), + .._ + ]): + assertEq year, "2019" + assertEq month, "10" + assertEq day, 11 + assertEq lexer.buf[maxbuf], "hello" + + else: + testFail() + + test "Small parts": + let txt = """ +root:x:0:0::/root:/bin/bash +bin:x:1:1::/:/usr/bin/nologin +daemon:x:2:2::/:/usr/bin/nologin +mail:x:8:12::/var/spool/mail:/usr/bin/nologin +""" + for line in txt.strip().split("\n"): + [@username, 1 .. 6 is _, @shell] := line.split(":") + + block: + [@a, _] := "A|B".split("|") + doAssert a is string + doAssert a == "A" + + block: + case parseJson("""{ "key" : "value" }"""): + of { "key" : JInt() }: + testFail() + + of { "key" : (getStr: @val) }: + doAssert val is string, $typeof(val) + + + block: + let it: seq[string] = "A|B".split("|") + [@a.startsWith("A"), .._] := it + doAssert a is string + doAssert a == "A" + + block: + (1, 2) | (@a, _) := (12, 3) + + doAssert a is Option[int] + + macro test1(): untyped = + var inBody: NimNode + if false: + Call[BracketExpr[@ident, opt @outType], @body] := inBody + + static: + doAssert ident is NimNode + doAssert outType is Option[NimNode] + doAssert body is NimNode + + if false: + Command[@ident is Ident(), Bracket[@outType], @body] := inBody + + static: + doAssert ident is NimNode + doAssert outType is NimNode + doAssert body is NimNode + + if false: + Call[BracketExpr[@ident, opt @outType], @body] | + Command[@ident is Ident(), Bracket[@outType], @body] := inBody + + static: + doAssert ident is NimNode + doAssert outType is Option[NimNode] + doAssert body is NimNode + + block: + var a = nnkBracketExpr.newTree(ident "map", ident "string") + + a.assertMatch: + BracketExpr: + @head + @typeParam + + doAssert head.strVal() == "map" + doAssert typeParam.strVal() == "string" + + test1() + + test "example from documentation": + case [(1, 3), (3, 4)]: + of [(1, @a), _]: + doASsert a == 3 + + else: + fail() + + test "Match proc declaration": + macro unpackProc(procDecl: untyped): untyped = + procDecl.assertMatch( + ProcDef[ + # Match proc name in full form + @name is ( # And get standalone `Ident` + Postfix[_, @procIdent] | # Either in exported form + (@procIdent is Ident()) # Or regular proc definition + ), + _, # Skip term rewriting template + _, # Skip generic parameters + [ # Match arguments/return types + @returnType, # Get return type + + # Match full `IdentDefs` for first argument, and extract it's name + # separately + @firstArg is IdentDefs[@firstArgName, _, _], + + # Match all remaining arguments. Collect both `IdentDefs` into + # sequence, and extract each argument separately + all @trailArgs is IdentDefs[@trailArgsName, _, _] + ], + .._ + ] + ) + + proc testProc1(arg1: int) {.unpackProc.} = + discard + + multitest "Flow macro": + type + FlowStageKind = enum + fskMap + fskFilter + fskEach + + FlowStage = object + outputType: Option[NimNode] + kind: FlowStageKind + body: NimNode + + + func identToKind(id: NimNode): FlowStageKind = + if id.eqIdent("map"): + fskMap + elif id.eqIdent("filter"): + fskFilter + elif id.eqIdent("each"): + fskEach + else: + raiseAssert("#[ IMPLEMENT ]#") + + proc rewrite(node: NimNode, idx: int): NimNode = + case node: + of Ident(strVal: "it"): + result = ident("it" & $idx) + of (kind: in nnkTokenKinds): + result = node + else: + result = newTree(node.kind) + for subn in node: + result.add subn.rewrite(idx) + + func makeTypeAssert( + expType, body, it: NimNode): NimNode = + let + bodyLit = body.toStrLit().strVal().strip().newLit() + pos = body.lineInfoObj() + ln = newLit((filename: pos.filename, line: pos.line)) + + return quote do: + when not (`it` is `expType`): + static: + {.line: `ln`.}: # To get correct line number when `error` + # is used it is necessary to use + # `{.line.}` pragma. + error "\n\nExpected type " & $(typeof(`expType`)) & + ", but expression \e[4m" & `bodyLit` & + "\e[24m has type of " & $(typeof(`it`)) + + + func evalExprFromStages(stages: seq[FlowStage]): NimNode = + block: + var expr = stages[^1].body + if Some(@expType) ?= stages[^1].outputType: +# ^^ ^^ +# | | +# | Pattern matching operator to determine whether +# | right part matches pattern on the left. +# | +# Special support for matching `Option[T]` types - + + let assrt = makeTypeAssert(expType, expr, expr) + # If type assertion is not `none` add type checking. + + expr = quote do: + `assrt` + `expr` + + result = newStmtList() + for idx, stage in stages: + # Rewrite body + let body = stage.body.rewrite(idx) + + + case stage.kind: + # If stage is a filter it is converted into `if` expression + # and new new variables are injected. + of fskFilter: + result.add quote do: + let stageOk = ((`body`)) + if not stageOk: + continue + + of fskEach: + # `each` has no variables or special formatting - just + # rewrite body and paste it back to resulting code + result.add body + of fskMap: + # Create new identifier for injected node and assign + # result of `body` to it. + let itId = ident("it" & $(idx + 1)) + result.add quote do: + let `itId` = `body` + + # If output type for stage needs to be explicitly checked + # create type assertion. + if Some(@expType) ?= stage.outputType: + result.add makeTypeAssert(expType, stage.body, itId) + + + + func typeExprFromStages(stages: seq[FlowStage], arg: NimNode): NimNode = + let evalExpr = evalExprFromStages(stages) + var resTuple = nnkPar.newTree(ident "it0") + + for idx, stage in stages: + if stage.kind notin {fskFilter, fskEach}: + resTuple.add ident("it" & $(idx + 1)) + + let lastId = newLit(stages.len - 1) + + result = quote do: + block: + ( + proc(): auto = # `auto` annotation allows to derive type + # of the proc from any assingment withing + # proc body - we take advantage of this, + # and avoid building type expression + # manually. + for it0 {.inject.} in `arg`: + `evalExpr` + result = `resTuple` +# ^^^^^^^^^^^^^^^^^^^ +# | +# Type of the return will be derived from this assinment. +# Even though it is placed within loop body, it will still +# derive necessary return type + )()[`lastId`] +# ^^^^^^^^^^^^ +# | | +# | Get last element from proc return type +# | +# After proc is declared we call it immediatey + + + macro flow(arg, body: untyped): untyped = + # Parse input DSl into sequence of `FlowStage` + var stages: seq[FlowStage] + for elem in body: + if elem.matches( + Call[BracketExpr[@ident, opt @outType], @body] | + # `map[string]:` + Command[@ident is Ident(), Bracket [@outType], @body] | + # `map [string]:` + Call[@ident is Ident(), @body] + # just `map:`, without type argument + ): + stages.add FlowStage( + kind: identToKind(ident), + outputType: outType, + body: body + ) + + # Create eval expression + let evalExpr = evalExprFromStages(stages) + + if stages[^1].kind notin {fskEach}: + # If last stage has return type (not `each`) then we need to + # accumulate results in temporary variable. + let resExpr = typeExprFromStages(stages, arg) + let lastId = ident("it" & $stages.len) + let resId = ident("res") + result = quote do: + var `resId`: seq[typeof(`resExpr`)] + + for it0 {.inject.} in `arg`: + `evalExpr` + `resId`.add `lastid` + + `resId` + else: + result = quote do: + for it0 {.inject.} in `arg`: + `evalExpr` + + + result = newBlockStmt(result) + + let data = """ +root:x:0:0:root:/root:/bin/bash +bin:x:1:1:bin:/bin:/sbin/nologin +daemon:x:2:2:daemon:/sbin:/sbin/nologin +adm:x:3:4:adm:/var/adm:/sbin/nologin +lp:x:4:7:lp:/var/spool/lpd:/sbin/nologin +sync:x:5:0:sync:/sbin:/bin/sync +shutdown:x:6:0:shutdown:/sbin:/sbin/shutdown +halt:x:7:0:halt:/sbin:/sbin/halt +mail:x:8:12:mail:/var/spool/mail:/sbin/nologin +news:x:9:13:news:/etc/news: +uucp:x:10:14:uucp:/var/spool/uucp:/sbin/nologin +operator:x:11:0:operator:/root:/sbin/nologin +games:x:12:100:games:/usr/games:/sbin/nologin +gopher:x:13:30:gopher:/var/gopher:/sbin/nologin +ftp:x:14:50:FTP User:/var/ftp:/sbin/nologin +nobody:x:99:99:Nobody:/:/sbin/nologin +nscd:x:28:28:NSCD Daemon:/:/sbin/nologin""" + + # expandMacros: + let res = flow data.split("\n"): + map[seq[string]]: + it.split(":") + filter: + let shell = it[^1] + it.len > 1 and shell.endsWith("bash") + map: + shell + + doAssert res is seq[string] +