summary refs log tree commit diff stats
diff options
context:
space:
mode:
-rw-r--r--changelog.md14
-rw-r--r--compiler/options.nim4
-rw-r--r--compiler/semstmts.nim181
-rw-r--r--doc/manual.rst52
4 files changed, 172 insertions, 79 deletions
diff --git a/changelog.md b/changelog.md
index e2203e602..5437f16ac 100644
--- a/changelog.md
+++ b/changelog.md
@@ -95,7 +95,7 @@
 - Added the procs ``rationals.`div```, ``rationals.`mod```, ``rationals.floorDiv`` and ``rationals.floorMod`` for rationals.
 - Added the proc ``math.prod`` for product of elements in openArray.
 - Added the proc ``parseBinInt`` to parse a binary integer from a string, which returns the value.
-- ``parseOct`` and ``parseBin`` in parseutils now also support the ``maxLen`` argument similar to ``parseHexInt``
+- ``parseOct`` and ``parseBin`` in parseutils now also support the ``maxLen`` argument similar to ``parseHexInt``.
 - Added the proc ``flush`` for memory mapped files.
 - Added the ``MemMapFileStream``.
 - Added ``macros.copyLineInfo`` to copy lineInfo from other node.
@@ -138,7 +138,13 @@
 - ``func`` is now an alias for ``proc {.noSideEffect.}``.
 - In order to make ``for`` loops and iterators more flexible to use Nim now
   supports so called "for-loop macros". See
-  the `manual <manual.html#macros-for-loop-macros>`_ for more details.
+  the [manual](manual.html#macros-for-loop-macros) for more details.
+  This feature enables a Python-like generic ``enumerate`` implementation.
+
+- Case statements can now be rewritten via macros. See the [manual]() for more information.
+  This feature enables custom pattern matchers.
+
+
 - the `typedesc` special type has been renamed to just `type`.
 - `static` and `type` are now also modifiers similar to `ref` and `ptr`.
   They denote the special types `static[T]` and `type[T]`.
@@ -171,7 +177,7 @@
 - Thread-local variables can now be declared inside procs. This implies all
   the effects of the ``global`` pragma.
 
-- Nim now supports ``except`` clause in the export statement.
+- Nim now supports the ``except`` clause in the export statement.
 
 - Range float types, example ``range[0.0 .. Inf]``. More details in language manual.
 - The ``{.this.}`` pragma has been deprecated. It never worked within generics and
@@ -219,6 +225,6 @@
 - macros.bindSym now capable to accepts not only literal string or string constant expression.
   bindSym enhancement make it also can accepts computed string or ident node inside macros /
   compile time functions / static blocks. Only in templates / regular code it retains it's old behavior.
-  This new feature can be accessed via {.experimental: "dynamicBindSym".} pragma/switch
+  This new feature can be accessed via {.experimental: "dynamicBindSym".} pragma/switch.
 
 ### Bugfixes
diff --git a/compiler/options.nim b/compiler/options.nim
index bc5545488..1873d9d5b 100644
--- a/compiler/options.nim
+++ b/compiler/options.nim
@@ -119,8 +119,8 @@ type
     destructor,
     notnil,
     dynamicBindSym,
-    forLoopMacros
-    #caseStmtMacros
+    forLoopMacros,
+    caseStmtMacros
 
   SymbolFilesOption* = enum
     disabledSf, writeOnlySf, readOnlySf, v2Sf
diff --git a/compiler/semstmts.nim b/compiler/semstmts.nim
index 7eb915dad..170ac799e 100644
--- a/compiler/semstmts.nim
+++ b/compiler/semstmts.nim
@@ -178,71 +178,6 @@ proc semIf(c: PContext, n: PNode): PNode =
     result.kind = nkIfExpr
     result.typ = typ
 
-proc semCase(c: PContext, n: PNode): PNode =
-  result = n
-  checkMinSonsLen(n, 2, c.config)
-  openScope(c)
-  n.sons[0] = semExprWithType(c, n.sons[0])
-  var chckCovered = false
-  var covered: BiggestInt = 0
-  var typ = commonTypeBegin
-  var hasElse = false
-  let caseTyp = skipTypes(n.sons[0].typ, abstractVarRange-{tyTypeDesc})
-  case caseTyp.kind
-  of tyInt..tyInt64, tyChar, tyEnum, tyUInt..tyUInt32, tyBool:
-    chckCovered = true
-  of tyFloat..tyFloat128, tyString, tyError:
-    discard
-  else:
-    localError(c.config, n.info, errSelectorMustBeOfCertainTypes)
-    return
-  for i in countup(1, sonsLen(n) - 1):
-    var x = n.sons[i]
-    when defined(nimsuggest):
-      if c.config.ideCmd == ideSug and exactEquals(c.config.m.trackPos, x.info) and caseTyp.kind == tyEnum:
-        suggestEnum(c, x, caseTyp)
-    case x.kind
-    of nkOfBranch:
-      checkMinSonsLen(x, 2, c.config)
-      semCaseBranch(c, n, x, i, covered)
-      var last = sonsLen(x)-1
-      x.sons[last] = semExprBranchScope(c, x.sons[last])
-      typ = commonType(typ, x.sons[last])
-    of nkElifBranch:
-      chckCovered = false
-      checkSonsLen(x, 2, c.config)
-      openScope(c)
-      x.sons[0] = forceBool(c, semExprWithType(c, x.sons[0]))
-      x.sons[1] = semExprBranch(c, x.sons[1])
-      typ = commonType(typ, x.sons[1])
-      closeScope(c)
-    of nkElse:
-      chckCovered = false
-      checkSonsLen(x, 1, c.config)
-      x.sons[0] = semExprBranchScope(c, x.sons[0])
-      typ = commonType(typ, x.sons[0])
-      hasElse = true
-    else:
-      illFormedAst(x, c.config)
-  if chckCovered:
-    if covered == toCover(c, n.sons[0].typ):
-      hasElse = true
-    else:
-      localError(c.config, n.info, "not all cases are covered")
-  closeScope(c)
-  if isEmptyType(typ) or typ.kind in {tyNil, tyExpr} or not hasElse:
-    for i in 1..n.len-1: discardCheck(c, n.sons[i].lastSon)
-    # propagate any enforced VoidContext:
-    if typ == c.enforceVoidContext:
-      result.typ = c.enforceVoidContext
-  else:
-    for i in 1..n.len-1:
-      var it = n.sons[i]
-      let j = it.len-1
-      if not endsInNoReturn(it.sons[j]):
-        it.sons[j] = fitNode(c, typ, it.sons[j], it.sons[j].info)
-    result.typ = typ
-
 proc semTry(c: PContext, n: PNode): PNode =
 
   var check = initIntSet()
@@ -683,29 +618,28 @@ proc isTrivalStmtExpr(n: PNode): bool =
       return false
   result = true
 
-proc handleForLoopMacro(c: PContext; n: PNode): PNode =
-  let iterExpr = n[^2]
-  if iterExpr.kind in nkCallKinds:
+proc handleStmtMacro(c: PContext; n, selector: PNode; magicType: string): PNode =
+  if selector.kind in nkCallKinds:
     # we transform
     # n := for a, b, c in m(x, y, z): Y
     # to
     # m(n)
-    let forLoopStmt = magicsys.getCompilerProc(c.graph, "ForLoopStmt")
-    if forLoopStmt == nil: return
+    let maType = magicsys.getCompilerProc(c.graph, magicType)
+    if maType == nil: return
 
-    let headSymbol = iterExpr[0]
+    let headSymbol = selector[0]
     var o: TOverloadIter
     var match: PSym = nil
     var symx = initOverloadIter(o, c, headSymbol)
     while symx != nil:
       if symx.kind in {skTemplate, skMacro}:
-        if symx.typ.len == 2 and symx.typ[1] == forLoopStmt.typ:
+        if symx.typ.len == 2 and symx.typ[1] == maType.typ:
           if match == nil:
             match = symx
           else:
             localError(c.config, n.info, errAmbiguousCallXYZ % [
               getProcHeader(c.config, match),
-              getProcHeader(c.config, symx), $iterExpr])
+              getProcHeader(c.config, symx), $selector])
       symx = nextOverloadIter(o, c, headSymbol)
 
     if match == nil: return
@@ -717,6 +651,38 @@ proc handleForLoopMacro(c: PContext; n: PNode): PNode =
     of skTemplate: result = semTemplateExpr(c, callExpr, match, {})
     else: result = nil
 
+proc handleForLoopMacro(c: PContext; n: PNode): PNode =
+  result = handleStmtMacro(c, n, n[^2], "ForLoopStmt")
+
+proc handleCaseStmtMacro(c: PContext; n: PNode): PNode =
+  # n[0] has been sem'checked and has a type. We use this to resolve
+  # 'match(n[0])' but then we pass 'n' to the 'match' macro. This seems to
+  # be the best solution.
+  var toResolve = newNodeI(nkCall, n.info)
+  toResolve.add newIdentNode(getIdent(c.cache, "match"), n.info)
+  toResolve.add n[0]
+
+  var errors: CandidateErrors
+  var r = resolveOverloads(c, toResolve, toResolve, {skTemplate, skMacro}, {},
+                           errors, false)
+  if r.state == csMatch:
+    var match = r.calleeSym
+    markUsed(c.config, n[0].info, match, c.graph.usageSym)
+    styleCheckUse(n[0].info, match)
+
+    # but pass 'n' to the 'match' macro, not 'n[0]':
+    r.call.sons[1] = n
+    let toExpand = semResolvedCall(c, r, r.call, {})
+    case match.kind
+    of skMacro: result = semMacroExpr(c, toExpand, toExpand, match, {})
+    of skTemplate: result = semTemplateExpr(c, toExpand, match, {})
+    else: result = nil
+  # this would be the perfectly consistent solution with 'for loop macros',
+  # but it kinda sucks for pattern matching as the matcher is not attached to
+  # a type then:
+  when false:
+    result = handleStmtMacro(c, n, n[0], "CaseStmt")
+
 proc semFor(c: PContext, n: PNode): PNode =
   checkMinSonsLen(n, 3, c.config)
   var length = sonsLen(n)
@@ -758,6 +724,75 @@ proc semFor(c: PContext, n: PNode): PNode =
     result.typ = c.enforceVoidContext
   closeScope(c)
 
+proc semCase(c: PContext, n: PNode): PNode =
+  result = n
+  checkMinSonsLen(n, 2, c.config)
+  openScope(c)
+  n.sons[0] = semExprWithType(c, n.sons[0])
+  var chckCovered = false
+  var covered: BiggestInt = 0
+  var typ = commonTypeBegin
+  var hasElse = false
+  let caseTyp = skipTypes(n.sons[0].typ, abstractVarRange-{tyTypeDesc})
+  case caseTyp.kind
+  of tyInt..tyInt64, tyChar, tyEnum, tyUInt..tyUInt32, tyBool:
+    chckCovered = true
+  of tyFloat..tyFloat128, tyString, tyError:
+    discard
+  else:
+    if caseStmtMacros in c.features:
+      result = handleCaseStmtMacro(c, n)
+      if result != nil: return result
+
+    localError(c.config, n.info, errSelectorMustBeOfCertainTypes)
+    return
+  for i in countup(1, sonsLen(n) - 1):
+    var x = n.sons[i]
+    when defined(nimsuggest):
+      if c.config.ideCmd == ideSug and exactEquals(c.config.m.trackPos, x.info) and caseTyp.kind == tyEnum:
+        suggestEnum(c, x, caseTyp)
+    case x.kind
+    of nkOfBranch:
+      checkMinSonsLen(x, 2, c.config)
+      semCaseBranch(c, n, x, i, covered)
+      var last = sonsLen(x)-1
+      x.sons[last] = semExprBranchScope(c, x.sons[last])
+      typ = commonType(typ, x.sons[last])
+    of nkElifBranch:
+      chckCovered = false
+      checkSonsLen(x, 2, c.config)
+      openScope(c)
+      x.sons[0] = forceBool(c, semExprWithType(c, x.sons[0]))
+      x.sons[1] = semExprBranch(c, x.sons[1])
+      typ = commonType(typ, x.sons[1])
+      closeScope(c)
+    of nkElse:
+      chckCovered = false
+      checkSonsLen(x, 1, c.config)
+      x.sons[0] = semExprBranchScope(c, x.sons[0])
+      typ = commonType(typ, x.sons[0])
+      hasElse = true
+    else:
+      illFormedAst(x, c.config)
+  if chckCovered:
+    if covered == toCover(c, n.sons[0].typ):
+      hasElse = true
+    else:
+      localError(c.config, n.info, "not all cases are covered")
+  closeScope(c)
+  if isEmptyType(typ) or typ.kind in {tyNil, tyExpr} or not hasElse:
+    for i in 1..n.len-1: discardCheck(c, n.sons[i].lastSon)
+    # propagate any enforced VoidContext:
+    if typ == c.enforceVoidContext:
+      result.typ = c.enforceVoidContext
+  else:
+    for i in 1..n.len-1:
+      var it = n.sons[i]
+      let j = it.len-1
+      if not endsInNoReturn(it.sons[j]):
+        it.sons[j] = fitNode(c, typ, it.sons[j], it.sons[j].info)
+    result.typ = typ
+
 proc semRaise(c: PContext, n: PNode): PNode =
   result = n
   checkSonsLen(n, 1, c.config)
diff --git a/doc/manual.rst b/doc/manual.rst
index 935db34ac..bb2650799 100644
--- a/doc/manual.rst
+++ b/doc/manual.rst
@@ -5411,6 +5411,58 @@ Currently for loop macros must be enabled explicitly
 via ``{.experimental: "forLoopMacros".}``.
 
 
+Case statement macros
+---------------------
+
+A macro that needs to be called `match`:idx: can be used to
+rewrite ``case`` statements in order to
+implement `pattern matching`:idx: for certain types. The following
+example implements a simplistic form of pattern matching for tuples,
+leveraging the existing equality operator for tuples (as provided in
+ ``system.==``):
+
+.. code-block:: nim
+    :test: "nim c $1"
+
+  {.experimental: "caseStmtMacros".}
+
+  import macros
+
+  macro match(n: tuple): untyped =
+    result = newTree(nnkIfStmt)
+    let selector = n[0]
+    for i in 1 ..< n.len:
+      let it = n[i]
+      case it.kind
+      of nnkElse, nnkElifBranch, nnkElifExpr, nnkElseExpr:
+        result.add it
+      of nnkOfBranch:
+        for j in 0..it.len-2:
+          let cond = newCall("==", selector, it[j])
+          result.add newTree(nnkElifBranch, cond, it[^1])
+      else:
+        error "'match' cannot handle this node", it
+    echo repr result
+
+  case ("foo", 78)
+  of ("foo", 78): echo "yes"
+  of ("bar", 88): echo "no"
+  else: discard
+
+
+Currently case statement macros must be enabled explicitly
+via ``{.experimental: "caseStmtMacros".}``.
+
+``match`` macros are subject to overload resolution. First the
+``case``'s selector expression is used to determine which ``match``
+macro to call. To this macro is then the complete ``case`` statement
+body is passed and the macro is evaluated.
+
+In other words, the macro needs to transform the full ``case`` statement
+but only the statement's selector expression is used to determine which
+``macro`` to call.
+
+
 Special Types
 =============