parserrt
nimony/src/nifler2/parserrt.nim
The runtime the generated parsers are written against.
Two halves, and the generator knows nothing about either beyond the names:
- The token stream.
nimlexerproduces it; everything the grammar's
semantic predicates ask about a token (noSpaceBefore, isUnary, dotLikeOps, ...) is answered here, from p.tok. The indentation class is the second half of the LL(1) decision domain, so indClass, checkInd, pushInd and popInd are as much a part of the interface as expect is.
- The output buffer. A
nifcore.TokenBuf, built out of order: a
mark records a position, and wrap retroactively inserts the opening tag there. That is what lets the generator left-factor freely and still build the tree the unfactored grammar describes -- and it is why the output cannot be a streaming nifbuilder.
wrap is the whole trick, so it is worth saying how it avoids duplicating nifcore's jump arithmetic: a wrapped node always ends at the current end of the buffer (marks nest, they never interleave), so after splicing the head token in at the mark, reopenLastTree + addParRi is exactly the situation nifcore's own closeTag is written for -- including the ExtendedSuffix it splices in when a body overflows the 19-bit jump field.
template grammar(rules: varargs[untyped])The grammar notation of
doc/internals/parser_generator.md, turned into onepRuleproc per rule at compile time.type IndClass = enum icNoInd = (0, "icNoInd") icLt = (1, "icLt") icEq = (2, "icEq") icGt = (3, "icGt")
func dollar`.IndClass(e: IndClass): stringtype PrimaryMode = enum pmNormal = (0, "pmNormal") pmTypeDesc = (1, "pmTypeDesc") pmTypeDef = (2, "pmTypeDef") pmTrySimple = (3, "pmTrySimple")
func dollar`.PrimaryMode(e: PrimaryMode): stringtype Mark = object pos: int64 info: NifLineInfo
type Parser = object lex: Lexer tok: Token dest: TokenBuf head: TokenBuf file: FileId currInd: int32 indStack: seq inPragma: int64 prevKind: TokKind filterFailed: bool prevEndLine: int64 prevEndCol: int64 inSemiStmtList: int64 sections: seq lastSection: NiflerKind infos: seq tail: TokenBuf wrapFields: seq pool: ref Pool.Obj failed: bool errLine: int64 errCol: int64 errMsg: string
proc openParser(src: string; filename: string; pool: ref Pool.Obj; tags: ref TagPool.Obj): Parserpoolandtagsare the output's: nifler2 writes throughnifpools' globals, a plugin parses into its own.proc info(p: Parser): NifLineInfoconst errInvalidIndentation: stringproc prettyTok(t: Token): stringprettyTokincompiler/lexer.nim.proc errorAt(p: var Parser; line: int64; col: int64; msg: string)The first syntax error ends the parse. Recovery would mean bailing out of an arbitrarily deep recursion, and the generated code cannot: there are no exceptions in this runtime and no notation for a recovery production. So the error is recorded and the token stream ends here: from now on the current token is an end of file that
getToknever moves past. That is what unwinds the recursion -- every repetition's continuation test fails on it, and a mandatory item that does not match reports into an error that is already recorded. It has to end the stream rather than merely record the error, becauseexpectdoes not consume the token it did not match, so a repetition whose body fails would make no progress and spin. A rule cut short leaves fewer trees than its layout expects, which is why the layouts checkfailed.proc reportFailure(p: Parser)nifler's output for a syntax error, and its exit code. The lexer's messages come first, which is where Nim, one token ahead as well, prints them; the stream ended at the error, so they are the ones up to it.
proc error(p: var Parser; msg: string)parMessage: at the current token.proc lexError(p: var Parser; msg: string)lexMessage: at the lexer's position, the end of the current token.proc identExpected(p: var Parser)proc exprExpected(p: var Parser)proc ruleError(p: var Parser; rule: string; misplaced: bool)Nothing in
rulestarts with the current token. parser.nim has no such single place: the message is whichever check of the hand-written procedure the token trips first. For a token that could start the rule at another indentation (misplaced) that is usually an indentation check, and otherwiseidentOrLiteral'serrExprExpected.proc strictListEnd(p: var Parser)See
strictListEndin the grammar.proc strictListStart(p: var Parser)An indented token that cannot start the list at all.
proc enumListEnd(p: var Parser)parseEnumloops onvalidIndand callsparseSymbolon whatever is there.proc paramStart(p: var Parser)parseParamList's messages for a token that starts no parameter.proc accentEnd(p: var Parser)proc listEnd(p: var Parser; close: TokKind)See
listEndin the grammar. A pragma ends in.}or}.proc missingEquals(p: var Parser)A routine without a body followed by an indented line.
proc requireFields(p: var Parser; m: Mark)proc funcType(p: var Parser)proc stmtListEnd(p: var Parser)parseStmt's block loop ends on these; any other token at the block's indentation is handed tocomplexOrSimpleStmt, which finds nothing.proc requireExcept(p: var Parser; m: Mark)proc noIndHere(p: var Parser)proc blockNameEnd(p: var Parser)proc tupleEnd(p: var Parser)proc indentError(p: var Parser)proc getTok(p: var Parser)proc expect(p: var Parser; k: TokKind)proc expect(p: var Parser; k: TokKind; s: string)The spelling matters for the operators the grammar names literally (
'->'inparamListArrow).proc expectLeaf(p: var Parser; k: TokKind)@'not'in the grammar: the terminal is content, not punctuation.proc expectLeaf(p: var Parser; k: TokKind; s: string)proc indClass(p: Parser): IndClassproc checkInd(p: var Parser; allowed: set[IndClass])proc afterOperator(p: var Parser)simpleExprAuxafter it consumed a binary operator:flexCommentandoptPar.proc pushInd(p: var Parser)proc pushIndAny(p: var Parser)withIndwithout therealIndassertion: the indentation becomes the current token's whatever that is, including -1 for a token that is not first on its line.semiStmtListis the one placeparser.nimdoes this, and it is why(\n when a: x\n elif b: y)has itselifat the same indentation as itswhenrather than at the enclosing block's.proc popInd(p: var Parser)proc noSpaceBefore(p: Parser): boolf(x)is a call,f (x)a command.proc isUnary(p: Parser): boolproc isSigilLike(p: Parser): boolproc dotLikeOps(p: Parser): boolnimPreviewDotLikeOps:a.?bwould parse as a field access rather than as an infix operator. nifler does not define it, so a dot-like operator is an ordinary infix operator --a.?b.cis(infix .? a (dot b c))-- and the answer isfalse.proc inTypeDesc(p: Parser; mode: PrimaryMode): boolparser.nim'sif mode == pmTypeDescincommandParam.proc parIsTuple(p: Parser; mode: PrimaryMode): boolparser.nim'sidentOrLiteraltakes'('to the comma-separatedexprColonEqExprListin a type, and toparsePar-- which also accepts a statement list, an assignment and adoblock -- everywhere else. Both productions exist in the grammar; this is the discriminator that says which one'('opens.proc inOrOut(p: Parser): boolparseGenericParam'sof tkIn, tkOut:-- the variance markers ofMyPtr[out T]. The grammar spells the operand asKEYWso that the keyword is emitted as the prefix operator's name, which a terminal would not be; this narrowsKEYWback to the two that are meant.proc pragmaOnPrimary(p: Parser; mode: PrimaryMode): boolsimpleExprAux'sif p.tok.tokType == tkCurlyDotLe and (p.tok.indent < 0 or realInd(p)) and mode == pmNormal. The indentation half is spelled in the grammar; this is the mode half.pmTrySimplecounts becausesimpleExprAuxrewrites it topmNormalright afterprimaryreturns, before it looks for the pragma.proc isTypedefOperand(p: Parser; mode: PrimaryMode): boolparseTypeDescKAux'sisTypedef: afterref/ptr/distinctin a type definition, anobjectortupleoperand brings a whole declaration with it --type T = ref objectand its indented field list -- while anything else is just aprimary. It is a two-token decision inparser.nimand a one-token one here, because by the time the operand is dispatched the keyword is already consumed.proc typeOperandFollows(p: Parser): boolparseTypeDescKAux'snot isOperator(p.tok) and isExprStart(p): what makesptrinSomeInteger | ptr | pointera bareptrrather than the prefix of(prefix | pointer). TheisExprStarthalf is the alternative's FIRST set and is already in the generated condition; the operator half is not expressible there, because an operator can also start a prefix expression.proc commandStart(p: Parser): boolparser.nim's guard onprimarySuffix's command branch. The token set is the one itscaselists, which is narrower than FIRST(commandParam):not,if,addrand friends can start an expression but not a command, soref int not nilis(infix not (ref int) nil)rather than a command whose argument isnot nil. An infix operator is not the start of a command either --import std / osis(infix / std os)-- and inside a pragma nothing is, because{.push hints:off.}must not become{.push(hints:off).}.proc commandAllowed(p: Parser; mode: PrimaryMode): boolparser.nim'scommandExprreturns its operand untouched when the mode ispmTrySimple, so in that mode a command is not a suffix at all. That is what leavesecho a, bforexprStmtto parse as one command with two parameters, instead of(cmd echo a)followed by a stray comma.proc suffixStart(p: Parser): boolparser.nim'sprimarySuffixloop guard, which the grammar cannot spell: a suffix continues on the same line, and a.may also open a continuation line that is indented at least as far as the line the primary started on. That last indentation is a parameter ofprimarySuffixthere; here it is the enclosing block's, which differs only for a primary that already spans lines.proc getPrecedence(p: Parser): int64parseOperatorsloops whileopPrec >= limit and p.tok.indent < 0 and not isUnary(p.tok). The unary test lives here rather than in the generator'sbinary(...), because "a unary operator has no infix precedence" is a fact about Nim's operators and not about precedence climbing. Without itecho $kind, "a"parsed as(infix $ echo kind)and then stalled on the comma.proc isRightAssoc(p: Parser): boolproc mark(p: Parser): Markproc discardUnused(m: Mark)A mark the rule turned out not to need. The generator emits one per alternative because it cannot know, before it has seen the whole alternative, whether a tag will claim it.
proc tagId(k: NiflerKind): TagIdThe master tag pool is seeded in
TagEnumorder, so a tag's id is its ordinal and no string is ever hashed to find it.proc wrapAt(p: var Parser; m: Mark; tag: NiflerKind; info: NifLineInfo)Retroactively make everything from
ma(tag ...)node positioned atinfo.proc wrap(p: var Parser; m: Mark; tag: NiflerKind)Retroactively make everything from
ma(tag ...)node.proc insertTokAt(p: var Parser; m: Mark; k: TokKind)^tag[ @'not' ... ]: consume the operator and insert it at the anchor, in front of the operand that is already on the buffer.proc insertLeafAt(p: var Parser; m: Mark; text: string)binary(...)'s operator:a + bis(infix + a b), so the operator has to land before the left operand, which was emitted before it was read.proc emitEmpty(p: var Parser).in the grammar: an absent child.proc setExportMarker(p: var Parser; m: Mark)exportMarker: nifler writesxin the export slot, whatever the operator was.proc pushSection(p: var Parser; tag: NiflerKind)proc pushLastSection(p: var Parser)using: nifler's section is one variable that nothing restores, andnkUsingStmtdoes not set it -- ausinggets the tag of whatever section was opened last in the module. Nothing downstream reads that tag, but a byte-identical tree has to have it.proc popSection(p: var Parser)proc pushFieldWrap(p: var Parser; wrap: bool)proc popFieldWrap(p: var Parser)proc wrapLikeFirst(p: var Parser; m: Mark; tag: NiflerKind)A node positioned at its first child:
newTree(nkCommand, a.info, a).proc fanOut(p: var Parser; m: Mark)name x pragmasrepeated, thentype value: one(section name x pragmas type value)per name. The count is exact because every slot is written,.or not -- which is what the placeholders buy.proc fanOutKv(p: var Parser; m: Mark)A tuple field list: names, then
type value. nifler keeps(kv name type)and drops the default.proc joinIdents(p: var Parser; m: Mark)Inside backquotes
parseSymbolglues a run of operator and bracket tokens into one identifier:[]=is(quoted []=), not three children, while=copyis(quoted = copy).proc inSemiStmtList(p: Parser): boolparseStmt'sif p.inSemiStmtList > 0: result = simpleStmt(p): inside a parenthesised statement list a one-line body is the bare statement, not astmts--(if a: b else: c)is(elif a b). The counter is not reset by what nests inside, so neither is this.proc stmtListExprLayout(p: var Parser; m: Mark)nkStmtListExpr: nifler writes all statements but the last in astmtsand the last one after it,(expr (stmts a b) c).proc rhsMode(mode: PrimaryMode): PrimaryModeThe mode of an operator's right operand.
simpleExprAuxturnspmTrySimpleintopmNormalafter the first primary, andparseOperatorsturnspmTypeDefintopmTypeDesc-- sox == Handle 0is(infix == x (cmd Handle 0)).proc literalAsIdent(p: var Parser; m: Mark)Inside backquotes
parseSymbolmakes a literal token an identifier of its text:'bigis(quoted ' big).proc posMarker(p: var Parser)A
.that only carries the current position to a layout, which removes it again.proc dotLayout(p: var Parser; m: Mark)dotExpr's rewrite ofx.y[:z](args)intoy[z](x, args): the dot node's children arex y (at z...) args...when the[:was there.proc curlyOrTable(p: var Parser; m: Mark)setOrTableConstrretagsnkCurlyasnkTableConstras soon as one element iskey: value.proc callOrObjConstr(p: var Parser; m: Mark)primarySuffix's(: a call whose first argument isname: valueis an object constructor,Foo(a: 1)is(oconstr Foo (kv a 1)).proc attachBlocks(p: var Parser; m: Mark)postExprBlocks: a trailing:ordoblock belongs to the expression in front of it.makeCallkeeps a node that is already a call --foo x:stays acmd-- and wraps anything else, and every block is appended as a child:c.into:is(call (dot c into) (stmts ...)). The rule that calls this has parsed the operand first, so the operand is the first tree sincemand the blocks are the rest.proc doLayout(p: var Parser; m: Mark; atBody: bool)A
doblock, parsed asparams ret pragmas body. Without a signature or pragmas it is just its body; otherwise(do params ret body)-- nifler writes the formal parameters as(params ...)plus the return type, and drops the pragmas.proc routineBodyAllowed(p: Parser; mode: PrimaryMode): boolparseProcExpr(p, mode != pmTypeDesc, ...): in a typeproc (): int = xhas no body -- the= xis the declaration's default value.proc emptyDiscriminator(p: var Parser)parseObjectCasewithout a discriminator: annkIdentDefsof empty nodes. Its name is the empty node, whose position has no file, so bridge.nim writes it absolute -- as~1,,???.proc wrapNoInfo(p: var Parser; m: Mark; tag: NiflerKind)A node bridge.nim writes with
addTreealone.proc pragmaBlock(p: var Parser; m: Mark)parseStmtPragma: a pragma followed by a block is annkPragmaBlock,(pragmax pragmas body), positioned at the pragma.proc inheritLayout(p: var Parser; m: Mark)nkOfInherit, created atof: one type is written as itself, several as(par A B).proc wrapSection(p: var Parser; m: Mark)A declaration that is already in slot form, tagged with the section: the
lets of a tuple unpacking, which bridge.nim writes without a position.proc pushInfo(p: var Parser)The position of the current token, for a node built after its keyword is consumed --
parseRoutinecreates the routine node atproc.proc popInfo(p: var Parser)proc moveLastToMark(p: var Parser; m: Mark)for x in itandlet (a, b) = v: nifler writes the iterated or unpacked value first.proc procLayout(p: var Parser; m: Mark; keyword: NiflerKind)An anonymous routine, parsed as
params ret pragmas bodywith.for each one that is absent. With a body it is a lambda,(proc . . . . params ret pragmas . body), and(params)is always there. Without one it is a type, and a type keeps what the source had: nothing at all is(proctype), no signature is a single.in place of params and result.proc routineLayout(p: var Parser; m: Mark; keyword: NiflerKind)A named routine, parsed as
name x pattern typevars params ret pragmas body:(keyword name x pattern typevars params ret pragmas . body), with the effects slot nifler reserves and(params)always present.proc emitLeaf(p: var Parser)The terminal the grammar matched, as a NIF atom. Which atom is decided by the token kind alone, so the generator never has to say (it emits the terminal's name as a comment only).