#
#
# Nim's Runtime Library
# (c) Copyright 2021 Nim contributors
#
# See the file "copying.txt", included in this
# distribution, for details about the copyright.
#
## Integration helpers between ``docgen.nim`` and ``rst.nim``.
##
## Function `toLangSymbol(linkText)`_ produces a signature `docLink` of
## `type LangSymbol`_ in ``rst.nim``, while `match(generated, docLink)`_
## matches it with `generated`, produced from `PNode` by ``docgen.rst``.
import rstast
import std/strutils
when defined(nimPreviewSlimSystem):
import std/[assertions, syncio]
type
LangSymbol* = object ## symbol signature in Nim
symKind*: string ## "proc", "const", "type", etc
symTypeKind*: string ## ""|enum|object|tuple -
## valid only when `symKind == "type"`
name*: string ## plain symbol name without any parameters
generics*: string ## generic parameters (without brackets)
isGroup*: bool ## is LangSymbol a group with overloads?
# the following fields are valid iff `isGroup` == false
# (always false when parsed by `toLangSymbol` because link like foo_
# can point to just a single symbol foo, e.g. proc).
parametersProvided*: bool ## to disambiguate `proc f`_ and `proc f()`_
parameters*: seq[tuple[name: string, `type`: string]]
## name-type seq, e.g. for proc
outType*: string ## result type, e.g. for proc
proc `$`*(s: LangSymbol): string = # for debug
("(symkind=$1, symTypeKind=$2, name=$3, generics=$4, isGroup=$5, " &
"parametersProvided=$6, parameters=$7, outType=$8)") % [
s.symKind, s.symTypeKind , s.name, s.generics, $s.isGroup,
$s.parametersProvided, $s.parameters, s.outType]
func nimIdentBackticksNormalize*(s: string): string =
## Normalizes the string `s` as a Nim identifier.
##
## Unlike `nimIdentNormalize` removes spaces and backticks.
##
## .. Warning:: No checking (e.g. that identifiers cannot start from
## digits or '_', or that number of backticks is even) is performed.
runnableExamples:
doAssert nimIdentBackticksNormalize("Foo_bar") == "Foobar"
doAssert nimIdentBackticksNormalize("FoO BAr") == "Foobar"
doAssert nimIdentBackticksNormalize("`Foo BAR`") == "Foobar"
doAssert nimIdentBackticksNormalize("` Foo BAR `") == "Foobar"
# not a valid identifier:
doAssert nimIdentBackticksNormalize("`_x_y`") == "_xy"
result = newString(s.len)
var firstChar = true
var j = 0
for i in 0..len(s) - 1:
if s[i] in {'A'..'Z'}:
if not firstChar: # to lowercase
result[j] = chr(ord(s[i]) + (ord('a') - ord('A')))
else:
result[j] = s[i]
firstChar = false
inc j
elif s[i] notin {'_', ' ', '`'}:
result[j] = s[i]
inc j
firstChar = false
elif s[i] == '_' and firstChar:
result[j] = '_'
inc j
firstChar = false
else: discard # just omit '`' or ' '
if j != s.len: setLen(result, j)
proc langSymbolGroup*(kind: string, name: string): LangSymbol =
if kind notin ["proc", "func", "macro", "method", "iterator",
"template", "converter"]:
raise newException(ValueError, "unknown symbol kind $1" % [kind])
result = LangSymbol(symKind: kind, name: name, isGroup: true)
proc toLangSymbol*(linkText: PRstNode): LangSymbol =
## Parses `linkText` into a more structured form using a state machine.
##
## This proc is designed to allow link syntax with operators even
## without escaped backticks inside:
##
## `proc *`_
## `proc []`_
##
## This proc should be kept in sync with the `renderTypes` proc from
## ``compiler/typesrenderer.nim``.
template fail(msg: string) =
raise newException(ValueError, msg)
if linkText.kind notin {rnRstRef, rnInner}:
fail("toLangSymbol: wrong input kind " & $linkText.kind)
const NimDefs = ["proc", "func", "macro", "method", "iterator",
"template", "converter", "const", "type", "var",
"enum", "object", "tuple", "module"]
template resolveSymKind(x: string) =
if x in ["enum", "object", "tuple"]:
result.symKind = "type"
result.symTypeKind = x
else:
result.symKind = x
type
State = enum
inBeginning
afterSymKind
beforeSymbolName # auxiliary state to catch situations like `proc []`_ after space
atSymbolName
afterSymbolName
genericsPar
parameterName
parameterType
outType
var state = inBeginning
var curIdent = ""
template flushIdent() =
if curIdent != "":
case state
of inBeginning: fail("incorrect state inBeginning")
of afterSymKind: resolveSymKind curIdent
of beforeSymbolName: fail("incorrect state beforeSymbolName")
of atSymbolName: result.name = curIdent.nimIdentBackticksNormalize
of afterSymbolName: fail("incorrect state afterSymbolName")
of genericsPar: result.generics = curIdent
of parameterName: result.parameters.add (curIdent, "")
of parameterType:
for a in countdown(result.parameters.len - 1, 0):
if result.parameters[a].`type` == "":
result.parameters[a].`type` = curIdent
of outType: result.outType = curIdent
curIdent = ""
var parens = 0
let L = linkText.sons.len
template s(i: int): string = linkText.sons[i].text
var i = 0
template nextState =
case s(i)
of " ":
if state == afterSymKind:
flushIdent
state = beforeSymbolName
of "`":
curIdent.add "`"
inc i
while i < L: # add contents between ` ` as a whole
curIdent.add s(i)
if s(i) == "`":
break
inc i
curIdent = curIdent.nimIdentBackticksNormalize
if state in {inBeginning, afterSymKind, beforeSymbolName}:
state = atSymbolName
flushIdent
state = afterSymbolName
of "[":
if state notin {inBeginning, afterSymKind, beforeSymbolName}:
inc parens
if state in {inBeginning, afterSymKind, beforeSymbolName}:
state = atSymbolName
curIdent.add s(i)
elif state in {atSymbolName, afterSymbolName} and parens == 1:
flushIdent
state = genericsPar
curIdent.add s(i)
else: curIdent.add s(i)
of "]":
if state notin {inBeginning, afterSymKind, beforeSymbolName, atSymbolName}:
dec parens
if state == genericsPar and parens == 0:
curIdent.add s(i)
flushIdent
else: curIdent.add s(i)
of "(":
inc parens
if state in {inBeginning, afterSymKind, beforeSymbolName}:
result.parametersProvided = true
state = atSymbolName
flushIdent
state = parameterName
elif state in {atSymbolName, afterSymbolName, genericsPar} and parens == 1:
result.parametersProvided = true
flushIdent
state = parameterName
else: curIdent.add s(i)
of ")":
dec parens
if state in {parameterName, parameterType} and parens == 0:
flushIdent
state = outType
else: curIdent.add s(i)
of "{": # remove pragmas
while i < L:
if s(i) == "}":
break
inc i
of ",", ";":
if state in {parameterName, parameterType} and parens == 1:
flushIdent
state = parameterName
else: curIdent.add s(i)
of "*": # skip export symbol
if state == atSymbolName:
flushIdent
state = afterSymbolName
elif state == afterSymbolName:
discard
else: curIdent.add "*"
of ":":
if state == outType: discard
elif state == parameterName:
flushIdent
state = parameterType
else: curIdent.add ":"
else:
let isPostfixSymKind = i > 0 and i == L - 1 and
result.symKind == "" and s(i) in NimDefs
if isPostfixSymKind: # for links like `foo proc`_
resolveSymKind s(i)
else:
case state
of inBeginning:
if s(i) in NimDefs:
state = afterSymKind
else:
state = atSymbolName
curIdent.add s(i)
of afterSymKind, beforeSymbolName:
state = atSymbolName
curIdent.add s(i)
of parameterType:
case s(i)
of "ref": curIdent.add "ref."
of "ptr": curIdent.add "ptr."
of "var": discard
else: curIdent.add s(i).nimIdentBackticksNormalize
of atSymbolName:
curIdent.add s(i)
else:
curIdent.add s(i).nimIdentBackticksNormalize
while i < L:
nextState
inc i
if state == afterSymKind: # treat `type`_ as link to symbol `type`
state = atSymbolName
flushIdent
result.isGroup = false
proc match*(generated: LangSymbol, docLink: LangSymbol): bool =
## Returns true if `generated` can be a target for `docLink`.
## If `generated` is an overload group then only `symKind` and `name`
## are compared for success.
result = true
if docLink.symKind != "":
if generated.symKind == "proc":
result = docLink.symKind in ["proc", "func"]
else:
result = generated.symKind == docLink.symKind
if result and docLink.symKind == "type" and docLink.symTypeKind != "":
result = generated.symTypeKind == docLink.symTypeKind
if not result: return
result = generated.name == docLink.name
if not result: return
if generated.isGroup:
# if `()` were added then it's not a reference to the whole group:
return not docLink.parametersProvided
if docLink.generics != "":
result = generated.generics == docLink.generics
if not result: return
if docLink.outType != "":
result = generated.outType == docLink.outType
if not result: return
if docLink.parametersProvided:
result = generated.parameters.len == docLink.parameters.len
if not result: return
var onlyType = false
for i in 0 ..< generated.parameters.len:
let g = generated.parameters[i]
let d = docLink.parameters[i]
if i == 0:
if g.`type` == d.name:
onlyType = true # only types, not names, are provided in `docLink`
if onlyType:
result = g.`type` == d.name
else:
if d.`type` != "":
result = g.`type` == d.`type`
if not result: return
result = g.name == d.name
if not result: return