Skip to content

Commit

Permalink
sem: consolidate macro pragma processing (nim-works#1021)
Browse files Browse the repository at this point in the history
## Summary

Merge the separate implementations of macro/template pragma processing
into a single one. The benefits:
* less duplicated code
* macro and template annotations are ensured to work the same for all
  types of definitions
* a single place to change the behaviour in

This changes:
* routine AST passed to macro/template pragmas keeps the `nnkPragma`
  node, even if empty
* ill-formed accent quoted expressions preceding a macro/template
  pragma don't prevent the transform from taking place

## Details

Routines, types, and var/let/const definitions all had their own
implementation, but their only difference was in where the AST was
modified and whether a full copy was created or not.

There's nothing that requires fundamentally different processing for
each type of definition, so the processing is consolidated:
* the new `tryMacroPragma` procedure processes a single pragma,
  returning either the raw transform or nil
* the new `semAnnotation` processes a pragma list (`nkPragma`), trying
  each item with `tryMacroPragma`
* sem'ing of routines, types, and var/let/const definitions use
  `semAnnotation`

So that the macro/template pragma call can be temporarily removed from
the definition's pragma list (which is at different tree positions for
each definition AST), the address of the node is passed to
`semAnnotation`.

Differences compared to the previous implementation:
* empty `nkPragma` nodes are kept. This was previously only the case
  for var/let/const annotations
* instead of the manual `semMacroExpr` call, `afterCallActions` is
  used. This is a step towards removing the bare `semMacroExpr` calls

Ideally, `semAnnotation` would always return the raw transformation
result and leave sem'ing it to the callsite, but this is not yet
possible.
  • Loading branch information
zerbina authored Nov 8, 2023
1 parent e6a46de commit a988639
Show file tree
Hide file tree
Showing 4 changed files with 99 additions and 201 deletions.
3 changes: 3 additions & 0 deletions compiler/sem/sem.nim
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,8 @@ proc indexTypesMatch(c: PContext, f, a: PType, arg: PNode): PNode
proc semStaticExpr(c: PContext, n: PNode): PNode
proc semStaticType(c: PContext, childNode: PNode, prev: PType): PType
proc semTypeOf(c: PContext; n: PNode): PNode
proc semAnnotation(c: PContext, pragmas: ptr PNode, n: PNode,
flags: TExprFlags): PNode
proc computeRequiresInit(c: PContext, t: PType): bool
proc defaultConstructionError(c: PContext, t: PType, n: PNode): PNode
proc hasUnresolvedArgs(c: PContext, n: PNode): bool
Expand Down Expand Up @@ -547,6 +549,7 @@ proc semTemplateExpr(c: PContext, n: PNode, s: PSym,
flags: TExprFlags = {}): PNode
proc semMacroExpr(c: PContext, n: PNode, sym: PSym,
flags: TExprFlags = {}): PNode
proc afterCallActions(c: PContext; n: PNode, flags: TExprFlags): PNode

proc tryConstExpr(c: PContext, n: PNode): PNode =
addInNimDebugUtils(c.config, "tryConstExpr", n, result)
Expand Down
248 changes: 94 additions & 154 deletions compiler/sem/semstmts.nim
Original file line number Diff line number Diff line change
Expand Up @@ -480,6 +480,92 @@ proc copyExcept(n: PNode, i: int): PNode {.inline.} =
for j in 0..<n.len:
if j != i: result.add(n[j])

proc tryMacroPragma(c: PContext, pragmas: ptr PNode, i: int,
operand: PNode, flags: TExprFlags): PNode =
## If the `i`-th pragma in the `pragmas` list is a macro/template pragma,
## applies it to the untyped `operand` AST and returns the transform (or an
## error). `nil` is returned otherwise.
##
## `pragmas` needs to point to the ``nkPragma`` AST within the `operand`
## AST. `flags` are the flags passed to the after-call expansion.
##
## Example:
##
## ..code-block:: nim
##
## let l {.m(a, b), rest.} # the operand AST
## # is transformed into
## m(a, b, let l {.rest.})
##
let n = pragmas[]
assert n.kind == nkPragma
let
prag = n[i]
isCallSyntax = prag.kind in nkPragmaCallKinds and prag.len >= 1
key =
if isCallSyntax: prag[0]
else: prag

# we only want to process macro pragmas (ast transforms)
# xxx: @saem: this dance to filter through them is another sign that pragmas
# shouldn't be associated to symbols as the syntax hints
if whichPragma(prag) == wInvalid and key.kind in nkIdentKinds:
# a custom pragma as opposed to a built-in
let (ident, err) = considerQuotedIdent(c, key)
if err != nil:
# XXX: replace with propagating ``nkError``. As it is now, an erroneous
# ``nkAccQuoted`` will not disable following macro pragmas if
# errorMax > 1!
localReport(c.config, n)
return
elif strTableGet(c.userPragmas, ident) != nil:
return # User defined pragma
else:
var amb = false
let sym = searchInScopes(c, ident, amb)
if sym != nil and sfCustomPragma in sym.flags:
return # User custom pragma
else:
return

var x = newNodeI(nkCall, key.info)
x.add(key)

if isCallSyntax:
# pass pragma arguments to the macro too:
for j in 1..<prag.len:
x.add prag[j]

# drop the pragma from the list, this prevents getting caught in endless
# recursion when the nkCall is semanticized
pragmas[] = copyExcept(n, i)
# leave the pragma list as is, even if empty

x.add(operand) # the definition AST the pragma appears on

# recursion assures that this works for multiple macro annotations too:
let r = semOverloadedCall(c, x, {skMacro, skTemplate}, {efNoUndeclared})
if r.isNil:
# restore the old list of pragmas since we couldn't process this one
pragmas[] = n
# no matching macro was found but there's always a possibility this may
# be a .pragma. template instead
else:
result = afterCallActions(c, r, flags)

proc semAnnotation(c: PContext, pragmas: ptr PNode, n: PNode,
flags: TExprFlags): PNode =
## Applies the first macro pragma in the `pragmas` list of `operand`,
## producing the transform. If no transformation is applied, `nil` is
## returned. `pragmas` needs to point to the ``nkPragma`` AST within
## `operand`.
for i in 0..<pragmas[].len:
result = tryMacroPragma(c, pragmas, i, n, flags)
if result != nil:
# always return the expanded-to-AST, even if it's an error. The
# remaining annotations are processed by the following sem check
break

proc semConstLetOrVarAnnotation(c: PContext, n: PNode): PNode =
## analyses normalized const, let, or var section for pragma annotations and
## applies the first macro pragma producing the transform wrapped in an
Expand All @@ -502,89 +588,8 @@ proc semConstLetOrVarAnnotation(c: PContext, n: PNode): PNode =
if pragExpr.kind != nkPragmaExpr:
# no pragmas, return unevaluated `n`
return n

let pragmas = pragExpr[1] # get the pragma node

for i, prag in pragmas.pairs:
let key = if prag.kind in nkPragmaCallKinds and prag.len >= 0:
prag[0]
else:
prag

# we only want to process macro pragmas (ast transforms)
# xxx: this dance to filter through them is another sign that pragmas
# shouldn't be associated to symbols as the syntax hints
if whichPragma(prag) == wInvalid and key.kind in nkIdentKinds:
# a custom pragma as opposed to a built-in
let (ident, err) = considerQuotedIdent(c, key)

if err != nil:
# TODO convert to nkError
localReport(c.config, err)
elif strTableGet(c.userPragmas, ident) != nil:
continue # User defined pragma
else:
var amb = false
let sym = searchInScopes(c, ident, amb)
if sym != nil and sfCustomPragma in sym.flags:
continue # User custom pragma
else:
# not a custom pragma, we can ignore it
continue

# transform `let l {.m, rest.}` to `m(do: let l {.rest.})` and let the
# semantic checker deal with the it:
var x = newNodeI(nkCall, key.info)
x.add(key)

if prag.kind in nkPragmaCallKinds and prag.len > 1:
# pass pragma arguments to the macro too:
for j in 1..<prag.len:
x.add prag[j]

# Drop the pragma from the list, this prevents getting caught in endless
# recursion when the nkCall is semanticized
n[0][0][1] = copyExcept(pragmas, i)
# leave the pragma list as is, even if empty

x.add(n)

# recursion assures that this works for multiple macro annotations too:
var r = semOverloadedCall(c, x, {skMacro, skTemplate}, {efNoUndeclared})
if r.isNil:
# Restore the old list of pragmas since we couldn't process this one
n[0][0][1] = pragmas
# No matching macro was found but there's always a possibility this may
# be a .pragma. template instead
continue

# TODO: temporarily handle nkError here, rather than proper propagation
case r.kind
of nkError:
localReport(c.config, r)
return # the rest is likely too broken, don't bother continuing
of nkCall:
doAssert r[0].kind == nkSym

let m = r[0].sym

case m.kind
of skMacro: result = semMacroExpr(c, r, m, {})
of skTemplate: result = semTemplateExpr(c, r, m, {})
else:
n[0][0][1] = pragmas # restore the originals
continue # we need to keep looking for macros pragmas

doAssert result != nil,
"a macro/template pragma must produce a non-nil result"

# the macro/template can entirely transform the node, so return the
# result and let the caller sem it again
break
else:
# TODO: set result to an error
discard

result = semAnnotation(c, addr pragExpr[1], n, {})
result =
if result.isNil:
n
Expand Down Expand Up @@ -1788,7 +1793,7 @@ proc typeDefLeftSidePass(c: PContext, typeSection: PNode, i: int) =
s.typ = newTypeS(tyForward, c)
s.typ.sym = s # process pragmas:
if name.kind == nkPragmaExpr:
let rewritten = applyTypeSectionPragmas(c, name[1], typeDef)
let rewritten = semAnnotation(c, addr name[1], typeDef, {efNoSemCheck})
if rewritten != nil:
typeSection[i] = rewritten
typeDefLeftSidePass(c, typeSection, i)
Expand Down Expand Up @@ -2142,75 +2147,11 @@ proc addResult(c: PContext, n: PNode, t: PType) =


proc semProcAnnotation(c: PContext, prc: PNode): PNode =
var n = prc[pragmasPos]
let n = prc[pragmasPos]
if n == nil or n.kind == nkEmpty: return
for i in 0..<n.len:
let it = n[i]
let key = if it.kind in nkPragmaCallKinds and it.len >= 1: it[0] else: it

# we only want to process macro pragmas (ast transforms)
# xxx: this dance to filter through them is another sign that pragmas
# shouldn't be associated to symbols as the syntax hints
if whichPragma(it) == wInvalid and key.kind in nkIdentKinds:
let (ident, err) = considerQuotedIdent(c, key)
if err != nil:
localReport(c.config, err)
if strTableGet(c.userPragmas, ident) != nil:
continue # User defined pragma
else:
var amb = false
let sym = searchInScopes(c, ident, amb)
if sym != nil and sfCustomPragma in sym.flags:
continue # User custom pragma
else:
# Not a custom pragma
continue

# we transform `proc p {.m, rest.}` into `m(do: proc p {.rest.})` and
# let the semantic checker deal with it:
var x = newNodeI(nkCall, key.info)
x.add(key)

if it.kind in nkPragmaCallKinds and it.len > 1:
# pass pragma arguments to the macro too:
for i in 1..<it.len:
x.add(it[i])

# Drop the pragma from the list, this prevents getting caught in endless
# recursion when the nkCall is semanticized
prc[pragmasPos] = copyExcept(n, i)
if prc[pragmasPos].kind != nkEmpty and prc[pragmasPos].len == 0:
prc[pragmasPos] = c.graph.emptyNode

x.add(prc)

# recursion assures that this works for multiple macro annotations too:
var r = semOverloadedCall(c, x, {skMacro, skTemplate}, {efNoUndeclared})
if r == nil:
# Restore the old list of pragmas since we couldn't process this
prc[pragmasPos] = n
# No matching macro was found but there's always the possibility this may
# be a .pragma. template instead
continue

# XXX: temporarily handle nkError here, rather than proper propagation.
# this should be refactored over time.
if r.kind == nkError:
localReport(c.config, r)
return # the rest is likely too broken, don't bother continuing

doAssert r[0].kind == nkSym
let m = r[0].sym
case m.kind
of skMacro: result = semMacroExpr(c, r, m, {})
of skTemplate: result = semTemplateExpr(c, r, m, {})
else:
prc[pragmasPos] = n
continue

doAssert result != nil

return # breaks the loop on the first macro pragma, then we'll reprocess
# we transform `proc p {.m, rest.}` into `m(do: proc p {.rest.})` and
# let the semantic checker deal with it:
result = semAnnotation(c, addr prc[pragmasPos], prc, {})

proc semInferredLambda(c: PContext, pt: TIdTable, n: PNode): PNode {.nosinks.} =
## used for resolving 'auto' in lambdas based on their callsite
Expand Down Expand Up @@ -3031,9 +2972,8 @@ proc semRoutineDef(c: PContext, n: PNode): PNode =
# before doing anything else, attempt to apply macro or template pragmas:
result = semProcAnnotation(c, n)
if result != nil:
# the definition was rewritten (or an error occured) and the result is
# already sem-checked
return
# the definition was rewritten, sem-check the result and return
return semExpr(c, result, {})

let kind =
case n.kind
Expand Down
47 changes: 1 addition & 46 deletions compiler/sem/semtypes.nim
Original file line number Diff line number Diff line change
Expand Up @@ -1883,57 +1883,12 @@ proc semTypeClass(c: PContext, n: PNode, prev: PType): PType =
localReport(c.config, result.n[3])
closeScope(c)

proc applyTypeSectionPragmas(c: PContext; pragmas, operand: PNode): PNode =
for p in pragmas:
let key = if p.kind in nkPragmaCallKinds and p.len >= 1: p[0] else: p

if p.kind == nkEmpty or whichPragma(p) != wInvalid:
discard "builtin pragma"
else:
let (ident, err) = considerQuotedIdent(c, key)
if err != nil:
# XXX: use nkError instead (or don't report an error yet and allow a
# macro to recover)
localReport(c.config, err)
elif strTableGet(c.userPragmas, ident) != nil:
discard "User-defined pragma"
else:
var amb = false
let sym = searchInScopes(c, ident, amb)
# XXX: What to do here if amb is true?
if sym != nil and sfCustomPragma in sym.flags:
discard "Custom user pragma"
else:
# we transform ``(arg1, arg2: T) {.m, rest.}`` into ``m((arg1, arg2: T) {.rest.})`` and
# let the semantic checker deal with it:
var x = newNodeI(nkCall, key.info)
x.add(key)
if p.kind in nkPragmaCallKinds and p.len > 1:
# pass pragma arguments to the macro too:
for i in 1 ..< p.len:
x.add(p[i])
# Also pass the node the pragma has been applied to
x.add(operand.copyTreeWithoutNode(p))
# recursion assures that this works for multiple macro annotations too:
var r = semOverloadedCall(c, x, {skMacro, skTemplate}, {efNoUndeclared})
if r != nil:
if r.kind == nkError:
localReport(c.config, r)
return

doAssert r[0].kind == nkSym
let m = r[0].sym
case m.kind
of skMacro: return semMacroExpr(c, r, m, {efNoSemCheck})
of skTemplate: return semTemplateExpr(c, r, m, {efNoSemCheck})
else: doAssert(false, "cannot happen")

proc semProcTypeWithScope(c: PContext, n: PNode,
prev: PType, kind: TSymKind): PType =
checkSonsLen(n, 2, c.config)

if n[1].kind != nkEmpty and n[1].len > 0:
let macroEval = applyTypeSectionPragmas(c, n[1], n)
let macroEval = semAnnotation(c, addr n[1], n, {efNoSemCheck})
if macroEval != nil:
return semTypeNode(c, macroEval, prev)

Expand Down
2 changes: 1 addition & 1 deletion tests/pragmas/tcustom_pragma.nim
Original file line number Diff line number Diff line change
Expand Up @@ -354,7 +354,7 @@ ProcDef
Ident "s"
Ident "string"
Empty
Empty
Pragma
Empty
StmtList
ReturnStmt
Expand Down

0 comments on commit a988639

Please sign in to comment.