implements 'case statement macros' in order to encourage the development of pattern matching mechanisms that are not terrible to look at

This commit is contained in:
Andreas Rumpf
2018-08-16 00:16:49 +02:00
parent da41fc1801
commit 1061e26bc9
4 changed files with 172 additions and 79 deletions

View File

@@ -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

View File

@@ -119,8 +119,8 @@ type
destructor,
notnil,
dynamicBindSym,
forLoopMacros
#caseStmtMacros
forLoopMacros,
caseStmtMacros
SymbolFilesOption* = enum
disabledSf, writeOnlySf, readOnlySf, v2Sf

View File

@@ -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)

View File

@@ -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
=============