#
#
#           The Nim Compiler
#        (c) Copyright 2015 Andreas Rumpf
#
#    See the file "copying.txt", included in this
#    distribution, for details about the copyright.
#

# included from sem.nim

discard """
  hygienic templates:

    template `||` (a, b: untyped): untyped =
      let aa = a
      if aa: aa else: b

    var
      a, b: T

    echo a || b || a

  Each evaluation context has to be different and we need to perform
  some form of preliminary symbol lookup in template definitions. Hygiene is
  a way to achieve lexical scoping at compile time.
"""

const
  errImplOfXNotAllowed = "implementation of '$1' is not allowed"

type
  TSymBinding = enum
    spNone, spGenSym, spInject

proc symBinding(n: PNode): TSymBinding =
  result = spNone
  for i in 0..<n.len:
    var it = n[i]
    var key = if it.kind == nkExprColonExpr: it[0] else: it
    if key.kind == nkIdent:
      case whichKeyword(key.ident)
      of wGensym: return spGenSym
      of wInject: return spInject
      else: discard

type
  TSymChoiceRule = enum
    scClosed, scOpen, scForceOpen

proc symChoice(c: PContext, n: PNode, s: PSym, r: TSymChoiceRule;
               isField = false): PNode =
  var
    a: PSym
    o: TOverloadIter = default(TOverloadIter)
  var i = 0
  a = initOverloadIter(o, c, n)
  while a != nil:
    if a.kind != skModule:
      inc(i)
      if i > 1: break
    a = nextOverloadIter(o, c, n)
  let info = getCallLineInfo(n)
  if i <= 1 and r != scForceOpen:
    # XXX this makes more sense but breaks bootstrapping for now:
    # (s.kind notin routineKinds or s.magic != mNone):
    # for instance 'nextTry' is both in tables.nim and astalgo.nim ...
    if not isField or sfGenSym notin s.flags:
      result = newSymNode(s, info)
      # possibly not final field sym
      incl(s.flags, sfUsed)
      markOwnerModuleAsUsed(c, s)
      onUse(info, s)
    else:
      result = n
  elif i == 0:
    # forced open but symbol not in scope, retain information
    result = n
  else:
    # semantic checking requires a type; ``fitNode`` deals with it
    # appropriately
    let kind = if r == scClosed or n.kind == nkDotExpr: nkClosedSymChoice
               else: nkOpenSymChoice
    result = newNodeIT(kind, info, newTypeS(tyNone, c))
    a = initOverloadIter(o, c, n)
    while a != nil:
      if a.kind != skModule and (not isField or sfGenSym notin a.flags):
        incl(a.flags, sfUsed)
        markOwnerModuleAsUsed(c, a)
        result.add newSymNode(a, info)
        onUse(info, a)
      a = nextOverloadIter(o, c, n)

proc semBindStmt(c: PContext, n: PNode, toBind: var IntSet): PNode =
  result = copyNode(n)
  for i in 0..<n.len:
    var a = n[i]
    # If 'a' is an overloaded symbol, we used to use the first symbol
    # as a 'witness' and use the fact that subsequent lookups will yield
    # the same symbol!
    # This is however not true anymore for hygienic templates as semantic
    # processing for them changes the symbol table...
    let s = qualifiedLookUp(c, a, {checkUndeclared})
    if s != nil:
      # we need to mark all symbols:
      let sc = symChoice(c, n, s, scClosed)
      if sc.kind == nkSym:
        toBind.incl(sc.sym.id)
        result.add sc
      else:
        for x in items(sc):
          toBind.incl(x.sym.id)
          result.add x
    else:
      illFormedAst(a, c.config)

proc semMixinStmt(c: PContext, n: PNode, toMixin: var IntSet): PNode =
  result = copyNode(n)
  for i in 0..<n.len:
    toMixin.incl(considerQuotedIdent(c, n[i]).id)
    let x = symChoice(c, n[i], nil, scForceOpen)
    result.add x

proc replaceIdentBySym(c: PContext; n: var PNode, s: PNode) =
  case n.kind
  of nkPostfix: replaceIdentBySym(c, n[1], s)
  of nkPragmaExpr: replaceIdentBySym(c, n[0], s)
  of nkIdent, nkAccQuoted, nkSym: n = s
  else: illFormedAst(n, c.config)

type
  TemplCtx = object
    c: PContext
    toBind, toMixin, toInject: IntSet
    owner: PSym
    cursorInBody: bool # only for nimsuggest
    scopeN: int
    noGenSym: int
    inTemplateHeader: int

proc isTemplParam(c: TemplCtx, s: PSym): bool {.inline.} =
  result = s.kind == skParam and
           s.owner == c.owner and sfTemplateParam in s.flags

proc getIdentReplaceParams(c: var TemplCtx, n: var PNode): tuple[node: PNode, hasParam: bool] =
  case n.kind
  of nkPostfix: result = getIdentReplaceParams(c, n[1])
  of nkPragmaExpr: result = getIdentReplaceParams(c, n[0])
  of nkIdent:
    result = (n, false)
    let s = qualifiedLookUp(c.c, n, {})
    if s != nil and isTemplParam(c, s):
      n = newSymNode(s, n.info)
      result = (n, true)
  of nkSym:
    result = (n, isTemplParam(c, n.sym))
  of nkAccQuoted:
    result = (n, false)
    for i in 0..<n.safeLen:
      let (ident, hasParam) = getIdentReplaceParams(c, n[i])
      if hasParam:
        result.node[i] = ident
        result.hasParam = true
  else:
    illFormedAst(n, c.c.config)
    result = (n, false)

proc semTemplBody(c: var TemplCtx, n: PNode): PNode

proc openScope(c: var TemplCtx) =
  openScope(c.c)

proc closeScope(c: var TemplCtx) =
  closeScope(c.c)

proc semTemplBodyScope(c: var TemplCtx, n: PNode): PNode =
  openScope(c)
  result = semTemplBody(c, n)
  closeScope(c)

proc newGenSym(kind: TSymKind, n: PNode, c: var TemplCtx): PSym =
  result = newSym(kind, considerQuotedIdent(c.c, n), c.c.idgen, c.owner, n.info)
  incl(result.flags, sfGenSym)
  incl(result.flags, sfShadowed)

proc addLocalDecl(c: var TemplCtx, n: var PNode, k: TSymKind) =
  # locals default to 'gensym', fields default to 'inject':
  if (n.kind == nkPragmaExpr and symBinding(n[1]) == spInject) or
      k == skField:
    # even if injected, don't produce a sym choice here:
    #n = semTemplBody(c, n)
    let (ident, hasParam) = getIdentReplaceParams(c, n)
    if not hasParam:
      if k != skField:
        c.toInject.incl(considerQuotedIdent(c.c, ident).id)
  else:
    if (n.kind == nkPragmaExpr and n.len >= 2 and n[1].kind == nkPragma):
      let pragmaNode = n[1]
      for i in 0..<pragmaNode.len:
        let ni = pragmaNode[i]
        # see D20210801T100514
        var found = false
        if ni.kind == nkIdent:
          for a in templatePragmas:
            if ni.ident.id == ord(a):
              found = true
              break
        if not found:
          openScope(c)
          pragmaNode[i] = semTemplBody(c, pragmaNode[i])
          closeScope(c)
    let (ident, hasParam) = getIdentReplaceParams(c, n)
    if not hasParam:
      if n.kind != nkSym and not (n.kind == nkIdent and n.ident.id == ord(wUnderscore)):
        let local = newGenSym(k, ident, c)
        addPrelimDecl(c.c, local)
        styleCheckDef(c.c, n.info, local)
        onDef(n.info, local)
        replaceIdentBySym(c.c, n, newSymNode(local, n.info))
        if k == skParam and c.inTemplateHeader > 0:
          local.flags.incl sfTemplateParam

proc semTemplSymbol(c: var TemplCtx, n: PNode, s: PSym; isField, isAmbiguous: bool): PNode =
  incl(s.flags, sfUsed)
  # bug #12885; ideally sem'checking is performed again afterwards marking
  # the symbol as used properly, but the nfSem mechanism currently prevents
  # that from happening, so we mark the module as used here already:
  markOwnerModuleAsUsed(c.c, s)
  # we do not call onUse here, as the identifier is not really
  # resolved here. We will fixup the used identifiers later.
  case s.kind
  of skUnknown:
    # Introduced in this pass! Leave it as an identifier.
    result = n
  of OverloadableSyms:
    result = symChoice(c.c, n, s, scOpen, isField)
    if not isField and result.kind in {nkSym, nkOpenSymChoice}:
      if openSym in c.c.features:
        if result.kind == nkSym:
          result = newOpenSym(result)
        else:
          result.typ() = nil
      else:
        result.flags.incl nfDisabledOpenSym
        result.typ() = nil
  of skGenericParam:
    if isField and sfGenSym in s.flags: result = n
    else:
      result = newSymNodeTypeDesc(s, c.c.idgen, n.info)
      if not isField and s.owner != c.owner:
        if openSym in c.c.features:
          result = newOpenSym(result)
        else:
          result.flags.incl nfDisabledOpenSym
          result.typ() = nil
  of skParam:
    result = n
  of skType:
    if isField and sfGenSym in s.flags: result = n
    else:
      if isAmbiguous:
        # ambiguous types should be symchoices since lookup behaves
        # differently for them in regular expressions
        result = symChoice(c.c, n, s, scOpen, isField)
      else: result = newSymNodeTypeDesc(s, c.c.idgen, n.info)
      if not isField and not (s.owner == c.owner and
          s.typ != nil and s.typ.kind == tyGenericParam) and
          result.kind in {nkSym, nkOpenSymChoice}:
        if openSym in c.c.features:
          if result.kind == nkSym:
            result = newOpenSym(result)
          else:
            result.typ() = nil
        else:
          result.flags.incl nfDisabledOpenSym
          result.typ() = nil
  else:
    if isField and sfGenSym in s.flags: result = n
    else:
      result = newSymNode(s, n.info)
      if not isField:
        if openSym in c.c.features:
          result = newOpenSym(result)
        else:
          result.flags.incl nfDisabledOpenSym
          result.typ() = nil
    # Issue #12832
    when defined(nimsuggest):
      suggestSym(c.c.graph, n.info, s, c.c.graph.usageSym, false)
    # field access (dot expr) will be handled by builtinFieldAccess
    if not isField:
      styleCheckUse(c.c, n.info, s)

proc semRoutineInTemplName(c: var TemplCtx, n: PNode, explicitInject: bool): PNode =
  result = n
  if n.kind == nkIdent:
    let s = qualifiedLookUp(c.c, n, {})
    if s != nil:
      if s.owner == c.owner and (s.kind == skParam or
          (sfGenSym in s.flags and not explicitInject)):
        incl(s.flags, sfUsed)
        result = newSymNode(s, n.info)
        onUse(n.info, s)
  else:
    for i in 0..<n.safeLen:
      result[i] = semRoutineInTemplName(c, n[i], explicitInject)

proc semRoutineInTemplBody(c: var TemplCtx, n: PNode, k: TSymKind): PNode =
  result = n
  checkSonsLen(n, bodyPos + 1, c.c.config)
  if n.kind notin nkLambdaKinds:
    # routines default to 'inject':
    let binding = symBinding(n[pragmasPos])
    if binding == spGenSym:
      let (ident, hasParam) = getIdentReplaceParams(c, n[namePos])
      if not hasParam:
        var s = newGenSym(k, ident, c)
        s.ast = n
        addPrelimDecl(c.c, s)
        styleCheckDef(c.c, n.info, s)
        onDef(n.info, s)
        n[namePos] = newSymNode(s, n[namePos].info)
      else:
        n[namePos] = ident
    else:
      n[namePos] = semRoutineInTemplName(c, n[namePos], binding == spInject)
  # open scope for parameters
  openScope(c)
  for i in patternPos..paramsPos-1:
    n[i] = semTemplBody(c, n[i])

  if k == skTemplate: inc(c.inTemplateHeader)
  n[paramsPos] = semTemplBody(c, n[paramsPos])
  if k == skTemplate: dec(c.inTemplateHeader)

  for i in paramsPos+1..miscPos:
    n[i] = semTemplBody(c, n[i])
  # open scope for locals
  inc c.scopeN
  openScope(c)
  n[bodyPos] = semTemplBody(c, n[bodyPos])
  # close scope for locals
  closeScope(c)
  dec c.scopeN
  # close scope for parameters
  closeScope(c)

proc semTemplIdentDef(c: var TemplCtx, a: PNode, symKind: TSymKind) =
  checkMinSonsLen(a, 3, c.c.config)
  when defined(nimsuggest):
    inc c.c.inTypeContext
  a[^2] = semTemplBody(c, a[^2])
  when defined(nimsuggest):
    dec c.c.inTypeContext
  a[^1] = semTemplBody(c, a[^1])
  for j in 0..<a.len-2:
    addLocalDecl(c, a[j], symKind)

proc semTemplSomeDecl(c: var TemplCtx, n: PNode, symKind: TSymKind; start = 0) =
  for i in start..<n.len:
    var a = n[i]
    case a.kind:
    of nkCommentStmt: continue
    of nkIdentDefs, nkVarTuple, nkConstDef:
      semTemplIdentDef(c, a, symKind)
    else:
      illFormedAst(a, c.c.config)


proc semPattern(c: PContext, n: PNode; s: PSym): PNode

proc semTemplBodySons(c: var TemplCtx, n: PNode): PNode =
  result = n
  for i in 0..<n.len:
    result[i] = semTemplBody(c, n[i])

proc semTemplBody(c: var TemplCtx, n: PNode): PNode =
  result = n
  semIdeForTemplateOrGenericCheck(c.c.config, n, c.cursorInBody)
  case n.kind
  of nkIdent:
    if n.ident.id in c.toInject: return n
    c.c.isAmbiguous = false
    let s = qualifiedLookUp(c.c, n, {})
    if s != nil:
      if s.owner == c.owner and s.kind == skParam and sfTemplateParam in s.flags:
        incl(s.flags, sfUsed)
        result = newSymNode(s, n.info)
        onUse(n.info, s)
      elif contains(c.toBind, s.id):
        result = symChoice(c.c, n, s, scClosed, c.noGenSym > 0)
      elif contains(c.toMixin, s.name.id):
        result = symChoice(c.c, n, s, scForceOpen, c.noGenSym > 0)
      elif s.owner == c.owner and sfGenSym in s.flags and c.noGenSym == 0:
        # template tmp[T](x: var seq[T]) =
        # var yz: T
        incl(s.flags, sfUsed)
        result = newSymNode(s, n.info)
        onUse(n.info, s)
      else:
        if s.kind in {skVar, skLet, skConst}:
          discard qualifiedLookUp(c.c, n, {checkAmbiguity, checkModule})
        result = semTemplSymbol(c, n, s, c.noGenSym > 0, c.c.isAmbiguous)
  of nkBind:
    result = semTemplBody(c, n[0])
  of nkBindStmt:
    result = semBindStmt(c.c, n, c.toBind)
  of nkMixinStmt:
    if c.scopeN > 0: result = semTemplBodySons(c, n)
    else: result = semMixinStmt(c.c, n, c.toMixin)
  of nkEmpty, nkSym..nkNilLit, nkComesFrom:
    discard
  of nkIfStmt:
    for i in 0..<n.len:
      var it = n[i]
      if it.len == 2:
        openScope(c)
        it[0] = semTemplBody(c, it[0])
        it[1] = semTemplBody(c, it[1])
        closeScope(c)
      else:
        n[i] = semTemplBodyScope(c, it)
  of nkWhileStmt:
    openScope(c)
    for i in 0..<n.len:
      n[i] = semTemplBody(c, n[i])
    closeScope(c)
  of nkCaseStmt:
    openScope(c)
    n[0] = semTemplBody(c, n[0])
    for i in 1..<n.len:
      var a = n[i]
      checkMinSonsLen(a, 1, c.c.config)
      for j in 0..<a.len-1:
        a[j] = semTemplBody(c, a[j])
      a[^1] = semTemplBodyScope(c, a[^1])
    closeScope(c)
  of nkForStmt, nkParForStmt:
    openScope(c)
    n[^2] = semTemplBody(c, n[^2])
    for i in 0..<n.len - 2:
      if n[i].kind == nkVarTuple:
        for j in 0..<n[i].len-1:
          addLocalDecl(c, n[i][j], skForVar)
      else:
        addLocalDecl(c, n[i], skForVar)
    openScope(c)
    n[^1] = semTemplBody(c, n[^1])
    closeScope(c)
    closeScope(c)
  of nkBlockStmt, nkBlockExpr, nkBlockType:
    checkSonsLen(n, 2, c.c.config)
    openScope(c)
    if n[0].kind != nkEmpty:
      addLocalDecl(c, n[0], skLabel)
      when false:
        # labels are always 'gensym'ed:
        let s = newGenSym(skLabel, n[0], c)
        addPrelimDecl(c.c, s)
        styleCheckDef(c.c, s)
        onDef(n[0].info, s)
        n[0] = newSymNode(s, n[0].info)
    n[1] = semTemplBody(c, n[1])
    closeScope(c)
  of nkTryStmt, nkHiddenTryStmt:
    checkMinSonsLen(n, 2, c.c.config)
    n[0] = semTemplBodyScope(c, n[0])
    for i in 1..<n.len:
      var a = n[i]
      checkMinSonsLen(a, 1, c.c.config)
      openScope(c)
      for j in 0..<a.len-1:
        if a[j].isInfixAs():
          addLocalDecl(c, a[j][2], skLet)
          a[j][1] = semTemplBody(c, a[j][1])
        else:
          a[j] = semTemplBody(c, a[j])
      a[^1] = semTemplBodyScope(c, a[^1])
      closeScope(c)
  of nkVarSection: semTemplSomeDecl(c, n, skVar)
  of nkLetSection: semTemplSomeDecl(c, n, skLet)
  of nkFormalParams:
    checkMinSonsLen(n, 1, c.c.config)
    semTemplSomeDecl(c, n, skParam, 1)
    n[0] = semTemplBody(c, n[0])
  of nkConstSection: semTemplSomeDecl(c, n, skConst)
  of nkTypeSection:
    for i in 0..<n.len:
      var a = n[i]
      if a.kind == nkCommentStmt: continue
      if (a.kind != nkTypeDef): illFormedAst(a, c.c.config)
      checkSonsLen(a, 3, c.c.config)
      addLocalDecl(c, a[0], skType)
    for i in 0..<n.len:
      var a = n[i]
      if a.kind == nkCommentStmt: continue
      if (a.kind != nkTypeDef): illFormedAst(a, c.c.config)
      checkSonsLen(a, 3, c.c.config)
      if a[1].kind != nkEmpty:
        openScope(c)
        a[1] = semTemplBody(c, a[1])
        a[2] = semTemplBody(c, a[2])
        closeScope(c)
      else:
        a[2] = semTemplBody(c, a[2])
  of nkObjectTy:
    openScope(c)
    result = semTemplBodySons(c, n)
    closeScope(c)
  of nkRecList:
    for i in 0..<n.len:
      var a = n[i]
      case a.kind:
      of nkCommentStmt, nkNilLit, nkSym, nkEmpty: continue
      of nkIdentDefs:
        semTemplIdentDef(c, a, skField)
      of nkRecCase, nkRecWhen:
        n[i] = semTemplBody(c, a)
      else:
        illFormedAst(a, c.c.config)
  of nkRecCase:
    semTemplIdentDef(c, n[0], skField)
    for i in 1..<n.len:
      n[i] = semTemplBody(c, n[i])
  of nkProcDef, nkLambdaKinds:
    result = semRoutineInTemplBody(c, n, skProc)
  of nkFuncDef:
    result = semRoutineInTemplBody(c, n, skFunc)
  of nkMethodDef:
    result = semRoutineInTemplBody(c, n, skMethod)
  of nkIteratorDef:
    result = semRoutineInTemplBody(c, n, skIterator)
  of nkTemplateDef:
    result = semRoutineInTemplBody(c, n, skTemplate)
  of nkMacroDef:
    result = semRoutineInTemplBody(c, n, skMacro)
  of nkConverterDef:
    result = semRoutineInTemplBody(c, n, skConverter)
  of nkPragmaExpr:
    result = semTemplBodySons(c, n)
  of nkPostfix:
    result[1] = semTemplBody(c, n[1])
  of nkPragma:
    for i in 0 ..< n.len:
      let x = n[i]
      let prag = whichPragma(x)
      if prag == wInvalid:
        # only sem if not a language-level pragma 
        result[i] = semTemplBody(c, x)
      elif x.kind in nkPragmaCallKinds:
        # is pragma, but value still needs to be checked
        for j in 1 ..< x.len:
          x[j] = semTemplBody(c, x[j])
  of nkBracketExpr:
    if n.typ == nil:
      # if a[b] is nested inside a typed expression, don't convert it
      # back to `[]`(a, b), prepareOperand will not typecheck it again
      # and so `[]` will not be resolved
      # checking if a[b] is typed should be enough to cover this case
      result = newNodeI(nkCall, n.info)
      result.add newIdentNode(getIdent(c.c.cache, "[]"), n.info)
      for i in 0..<n.len: result.add(n[i])
    result = semTemplBodySons(c, result)
  of nkCurlyExpr:
    if n.typ == nil:
      # see nkBracketExpr case for explanation
      result = newNodeI(nkCall, n.info)
      result.add newIdentNode(getIdent(c.c.cache, "{}"), n.info)
      for i in 0..<n.len: result.add(n[i])
    result = semTemplBodySons(c, result)
  of nkAsgn, nkFastAsgn, nkSinkAsgn:
    checkSonsLen(n, 2, c.c.config)
    let a = n[0]
    let b = n[1]

    let k = a.kind
    case k
    of nkBracketExpr:
      if a.typ == nil:
        # see nkBracketExpr case above for explanation
        result = newNodeI(nkCall, n.info)
        result.add newIdentNode(getIdent(c.c.cache, "[]="), n.info)
        for i in 0..<a.len: result.add(a[i])
        result.add(b)
      let a0 = semTemplBody(c, a[0])
      result = semTemplBodySons(c, result)
    of nkCurlyExpr:
      if a.typ == nil:
        # see nkBracketExpr case above for explanation
        result = newNodeI(nkCall, n.info)
        result.add newIdentNode(getIdent(c.c.cache, "{}="), n.info)
        for i in 0..<a.len: result.add(a[i])
        result.add(b)
      result = semTemplBodySons(c, result)
    else:
      result = semTemplBodySons(c, n)
  of nkCallKinds-{nkPostfix}:
    # do not transform runnableExamples (bug #9143)
    if not isRunnableExamples(n[0]):
      result = semTemplBodySons(c, n)
  of nkDotExpr, nkAccQuoted:
    # dotExpr is ambiguous: note that we explicitly allow 'x.TemplateParam',
    # so we use the generic code for nkDotExpr too
    c.c.isAmbiguous = false
    let s = qualifiedLookUp(c.c, n, {})
    if s != nil:
      # mirror the nkIdent case
      # do not symchoice a quoted template parameter (bug #2390):
      if s.owner == c.owner and s.kind == skParam and
          n.kind == nkAccQuoted and n.len == 1:
        incl(s.flags, sfUsed)
        onUse(n.info, s)
        return newSymNode(s, n.info)
      elif contains(c.toBind, s.id):
        return symChoice(c.c, n, s, scClosed, c.noGenSym > 0)
      elif contains(c.toMixin, s.name.id):
        return symChoice(c.c, n, s, scForceOpen, c.noGenSym > 0)
      else:
        if s.kind in {skVar, skLet, skConst}:
          discard qualifiedLookUp(c.c, n, {checkAmbiguity, checkModule})
        return semTemplSymbol(c, n, s, c.noGenSym > 0, c.c.isAmbiguous)
    if n.kind == nkDotExpr:
      result = n
      result[0] = semTemplBody(c, n[0])
      inc c.noGenSym
      result[1] = semTemplBody(c, n[1])
      dec c.noGenSym
      if result[1].kind == nkSym and result[1].sym.kind in routineKinds:
        # prevent `dotTransformation` from rewriting this node to `nkIdent`
        # by making it a symchoice
        # in generics this becomes `nkClosedSymChoice` but this breaks code
        # as the old behavior here was that this became `nkIdent`
        var choice = newNodeIT(nkOpenSymChoice, n[1].info, newTypeS(tyNone, c.c))
        choice.add result[1]
        result[1] = choice
    else:
      result = semTemplBodySons(c, n)
  of nkExprColonExpr, nkExprEqExpr:
    if n.len == 2:
      inc c.noGenSym
      result[0] = semTemplBody(c, n[0])
      dec c.noGenSym
      result[1] = semTemplBody(c, n[1])
    else:
      result = semTemplBodySons(c, n)
  of nkTableConstr:
    # also transform the keys (bug #12595)
    for i in 0..<n.len:
      result[i] = semTemplBodySons(c, n[i])
  else:
    result = semTemplBodySons(c, n)

proc semTemplBodyDirty(c: var TemplCtx, n: PNode): PNode =
  result = n
  semIdeForTemplateOrGenericCheck(c.c.config, n, c.cursorInBody)
  case n.kind
  of nkIdent:
    let s = qualifiedLookUp(c.c, n, {})
    if s != nil:
      if s.owner == c.owner and s.kind == skParam:
        result = newSymNode(s, n.info)
      elif contains(c.toBind, s.id):
        result = symChoice(c.c, n, s, scClosed)
  of nkBind:
    result = semTemplBodyDirty(c, n[0])
  of nkBindStmt:
    result = semBindStmt(c.c, n, c.toBind)
  of nkEmpty, nkSym..nkNilLit, nkComesFrom:
    discard
  else:
    # dotExpr is ambiguous: note that we explicitly allow 'x.TemplateParam',
    # so we use the generic code for nkDotExpr too
    if n.kind == nkDotExpr or n.kind == nkAccQuoted:
      let s = qualifiedLookUp(c.c, n, {})
      if s != nil and contains(c.toBind, s.id):
        return symChoice(c.c, n, s, scClosed)
    result = n
    for i in 0..<n.len:
      result[i] = semTemplBodyDirty(c, n[i])

# in semstmts.nim:
proc semProcAnnotation(c: PContext, prc: PNode; validPragmas: TSpecialWords): PNode

proc semTemplateDef(c: PContext, n: PNode): PNode =
  result = semProcAnnotation(c, n, templatePragmas)
  if result != nil: return result
  result = n
  var s: PSym
  if isTopLevel(c):
    s = semIdentVis(c, skTemplate, n[namePos], {sfExported})
    incl(s.flags, sfGlobal)
  else:
    s = semIdentVis(c, skTemplate, n[namePos], {})
  assert s.kind == skTemplate

  let info = getLineInfo(n[namePos])
  suggestSym(c.graph, info, s, c.graph.usageSym)

  styleCheckDef(c, s)
  onDef(n[namePos].info, s)
  # check parameter list:
  #s.scope = c.currentScope
  # push noalias flag at first to prevent unwanted recursive calls:
  incl(s.flags, sfNoalias)
  pushOwner(c, s)
  openScope(c)
  n[namePos] = newSymNode(s)
  s.ast = n # for implicitPragmas to use
  pragmaCallable(c, s, n, templatePragmas)
  implicitPragmas(c, s, n.info, templatePragmas)

  setGenericParamsMisc(c, n)
  # process parameters:
  var allUntyped = true
  var nullary = true
  if n[paramsPos].kind != nkEmpty:
    semParamList(c, n[paramsPos], n[genericParamsPos], s)
    # a template's parameters are not gensym'ed even if that was originally the
    # case as we determine whether it's a template parameter in the template
    # body by the absence of the sfGenSym flag:
    let retType = s.typ.returnType
    if retType != nil and retType.kind != tyUntyped:
      allUntyped = false
    for i in 1..<s.typ.n.len:
      let param = s.typ.n[i].sym
      if param.name.id != ord(wUnderscore):
        param.flags.incl sfTemplateParam
        param.flags.excl sfGenSym
      if param.typ.kind != tyUntyped: allUntyped = false
      # no default value, parameters required in call
      if param.ast == nil: nullary = false
  else:
    s.typ = newTypeS(tyProc, c)
    # XXX why do we need tyTyped as a return type again?
    s.typ.n = newNodeI(nkFormalParams, n.info)
    rawAddSon(s.typ, newTypeS(tyTyped, c))
    s.typ.n.add newNodeIT(nkType, n.info, s.typ.returnType)
  if n[genericParamsPos].safeLen == 0:
    # restore original generic type params as no explicit or implicit were found
    n[genericParamsPos] = n[miscPos][1]
    n[miscPos] = c.graph.emptyNode
  if allUntyped: incl(s.flags, sfAllUntyped)
  if nullary and
      n[genericParamsPos].kind == nkEmpty and
      n[bodyPos].kind != nkEmpty:
    # template can be called with alias syntax, remove pushed noalias flag
    excl(s.flags, sfNoalias)

  if n[patternPos].kind != nkEmpty:
    n[patternPos] = semPattern(c, n[patternPos], s)

  var ctx = TemplCtx(
    toBind: initIntSet(),
    toMixin: initIntSet(),
    toInject: initIntSet(),
    c: c,
    owner: s
  )
  # handle default params:
  for i in 1..<s.typ.n.len:
    let param = s.typ.n[i].sym
    if param.ast != nil:
      # param default values need to be treated like template body:
      if sfDirty in s.flags:
        param.ast = semTemplBodyDirty(ctx, param.ast)
      else:
        param.ast = semTemplBody(ctx, param.ast)
      if param.ast.referencesAnotherParam(s):
        param.ast.flags.incl nfDefaultRefsParam
  if sfDirty in s.flags:
    n[bodyPos] = semTemplBodyDirty(ctx, n[bodyPos])
  else:
    n[bodyPos] = semTemplBody(ctx, n[bodyPos])
  # only parameters are resolved, no type checking is performed
  semIdeForTemplateOrGeneric(c, n[bodyPos], ctx.cursorInBody)
  closeScope(c)
  popOwner(c)

  if sfCustomPragma in s.flags:
    if n[bodyPos].kind != nkEmpty:
      localError(c.config, n[bodyPos].info, errImplOfXNotAllowed % s.name.s)
  elif n[bodyPos].kind == nkEmpty:
    localError(c.config, n.info, "implementation of '$1' expected" % s.name.s)
  var (proto, comesFromShadowscope) = searchForProc(c, c.currentScope, s)
  if proto == nil:
    addInterfaceOverloadableSymAt(c, c.currentScope, s)
  elif not comesFromShadowscope:
    if {sfTemplateRedefinition, sfGenSym} * s.flags == {}:
      #wrongRedefinition(c, n.info, proto.name.s, proto.info)
      message(c.config, n.info, warnImplicitTemplateRedefinition, s.name.s)
    symTabReplace(c.currentScope.symbols, proto, s)
  if n[patternPos].kind != nkEmpty:
    c.patterns.add(s)

proc semPatternBody(c: var TemplCtx, n: PNode): PNode =
  template templToExpand(s: untyped): untyped =
    s.kind == skTemplate and (s.typ.len == 1 or sfAllUntyped in s.flags)

  proc newParam(c: var TemplCtx, n: PNode, s: PSym): PNode =
    # the param added in the current scope is actually wrong here for
    # macros because they have a shadowed param of type 'PNimNode' (see
    # semtypes.addParamOrResult). Within the pattern we have to ensure
    # to use the param with the proper type though:
    incl(s.flags, sfUsed)
    onUse(n.info, s)
    let x = c.owner.typ.n[s.position+1].sym
    assert x.name == s.name
    result = newSymNode(x, n.info)

  proc handleSym(c: var TemplCtx, n: PNode, s: PSym): PNode =
    result = n
    if s != nil:
      if s.owner == c.owner and s.kind == skParam:
        result = newParam(c, n, s)
      elif contains(c.toBind, s.id):
        result = symChoice(c.c, n, s, scClosed)
      elif templToExpand(s):
        result = semPatternBody(c, semTemplateExpr(c.c, n, s, {efNoSemCheck}))
      else:
        discard
        # we keep the ident unbound for matching instantiated symbols and
        # more flexibility

  proc expectParam(c: var TemplCtx, n: PNode): PNode =
    let s = qualifiedLookUp(c.c, n, {})
    if s != nil and s.owner == c.owner and s.kind == skParam:
      result = newParam(c, n, s)
    else:
      localError(c.c.config, n.info, "invalid expression")
      result = n

  result = n
  case n.kind
  of nkIdent:
    let s = qualifiedLookUp(c.c, n, {})
    result = handleSym(c, n, s)
  of nkBindStmt:
    result = semBindStmt(c.c, n, c.toBind)
  of nkEmpty, nkSym..nkNilLit: discard
  of nkCurlyExpr:
    # we support '(pattern){x}' to bind a subpattern to a parameter 'x';
    # '(pattern){|x}' does the same but the matches will be gathered in 'x'
    if n.len != 2:
      localError(c.c.config, n.info, "invalid expression")
    elif n[1].kind == nkIdent:
      n[0] = semPatternBody(c, n[0])
      n[1] = expectParam(c, n[1])
    elif n[1].kind == nkPrefix and n[1][0].kind == nkIdent:
      let opr = n[1][0]
      if opr.ident.s == "|":
        n[0] = semPatternBody(c, n[0])
        n[1][1] = expectParam(c, n[1][1])
      else:
        localError(c.c.config, n.info, "invalid expression")
    else:
      localError(c.c.config, n.info, "invalid expression")
  of nkStmtList, nkStmtListExpr:
    if stupidStmtListExpr(n):
      result = semPatternBody(c, n.lastSon)
    else:
      for i in 0..<n.len:
        result[i] = semPatternBody(c, n[i])
  of nkCallKinds:
    let s = qualifiedLookUp(c.c, n[0], {})
    if s != nil:
      if s.owner == c.owner and s.kind == skParam: discard
      elif contains(c.toBind, s.id): discard
      elif templToExpand(s):
        return semPatternBody(c, semTemplateExpr(c.c, n, s, {efNoSemCheck}))

    if n.kind == nkInfix and (let id = considerQuotedIdent(c.c, n[0]); id != nil):
      # we interpret `*` and `|` only as pattern operators if they occur in
      # infix notation, so that '`*`(a, b)' can be used for verbatim matching:
      if id.s == "*" or id.s == "**":
        result = newNodeI(nkPattern, n.info, n.len)
        result[0] = newIdentNode(id, n.info)
        result[1] = semPatternBody(c, n[1])
        result[2] = expectParam(c, n[2])
        return
      elif id.s == "|":
        result = newNodeI(nkPattern, n.info, n.len)
        result[0] = newIdentNode(id, n.info)
        result[1] = semPatternBody(c, n[1])
        result[2] = semPatternBody(c, n[2])
        return

    if n.kind == nkPrefix and (let id = considerQuotedIdent(c.c, n[0]); id != nil):
      if id.s == "~":
        result = newNodeI(nkPattern, n.info, n.len)
        result[0] = newIdentNode(id, n.info)
        result[1] = semPatternBody(c, n[1])
        return

    for i in 0..<n.len:
      result[i] = semPatternBody(c, n[i])
  else:
    # dotExpr is ambiguous: note that we explicitly allow 'x.TemplateParam',
    # so we use the generic code for nkDotExpr too
    case n.kind
    of nkDotExpr, nkAccQuoted:
      let s = qualifiedLookUp(c.c, n, {})
      if s != nil:
        if contains(c.toBind, s.id):
          return symChoice(c.c, n, s, scClosed)
        else:
          return newIdentNode(s.name, n.info)
    of nkPar:
      if n.len == 1: return semPatternBody(c, n[0])
    else: discard
    for i in 0..<n.len:
      result[i] = semPatternBody(c, n[i])

proc semPattern(c: PContext, n: PNode; s: PSym): PNode =
  openScope(c)
  var ctx = TemplCtx(
    toBind: initIntSet(),
    toMixin: initIntSet(),
    toInject: initIntSet(),
    c: c,
    owner: getCurrOwner(c)
  )
  result = flattenStmts(semPatternBody(ctx, n))
  if result.kind in {nkStmtList, nkStmtListExpr}:
    if result.len == 1:
      result = result[0]
    elif result.len == 0:
      localError(c.config, n.info, "a pattern cannot be empty")
  closeScope(c)
  addPattern(c, LazySym(sym: s))
