parser-transform
Version:
Streaming+Async lexer and parser
219 lines (163 loc) • 6.57 kB
Markdown
{LRParser,LRParsingTable,Grammar,CanonicalCollection,LRParserGenerator} = require 'syntax-cli'
{EntryType} = LRParsingTable
SpecialSymbols = require 'syntax-cli/dist/special-symbols.js'
Stream = require 'stream'
vm = require 'vm'
class StreamingLRParserGenerator extends LRParserGenerator
constructor: ({grammar,options = {}}) ->
super {grammar,options}
@context =
yytext: ''
yyleng: 0
__: null
__handler: []
context = vm.createContext @context
@vm =
eval: (code) -> vm.runInContext code, context
return
This is in `traits`.
buildSemanticAction: (production) ->
# action is the text
action = @getSemanticActionCode production
return null unless action
# args is the Array containing the argument names
args = @getSemanticActionParams production
# Save the action, they are injected later.
# "__handler[1]", "__handler[2]", etc.
index = production.getNumber()
name = "__handler[#{index}]"
handler = @vm.eval "__handler[#{index}] = async function (#{args}) {\n#{action}\n}"
name
generateModuleInclude: ->
moduleInclude = @_grammar.getModuleInclude()
if moduleInclude?
@vm.eval moduleInclude
return
Replaces the "template"
class StreamingLRParser
constructor: (generator) ->
@_generator = generator
@_grammar = generator._grammar
@_table = generator.getTable()
@_stack = []
@_startingState = @_table._canonicalCollection
.getStartingState()
.getNumber()
@_stack.push @_startingState
@_shiftedToken = null
return
_peek: LRParser::_peek
_shift: LRParser::_shift
_reduce: (entry,token) ->
productionNumber = entry.slice(1)
production = this._grammar.getProduction(productionNumber)
hasSemanticAction = production.hasSemanticAction()
semanticValueArgs = if hasSemanticAction then [] else null
locationArgs =
if hasSemanticAction and this._grammar.shouldCaptureLocations() then [] else null
# Pop 2x symbols from the stack (RHS + state number for each),
# unless it's an ε-production for which nothing to pop.
unless production.isEpsilon()
rhsLengh = production.getRHS().length
while rhsLengh--
# Pop state number
this._stack.pop()
# Pop production symbol.
stackEntry = this._stack.pop()
if (hasSemanticAction)
semanticValueArgs.unshift(stackEntry.semanticValue)
if (locationArgs)
locationArgs.unshift(stackEntry.loc)
previousState = @_peek()
symbolToReduceWith = production.getLHS().getSymbol()
reduceStackEntry = {symbol: symbolToReduceWith}
if hasSemanticAction
@_generator.context.yytext = if token then token.value else ''
@_generator.context.yyleng = if token then token.value.length else 0
semanticActionArgs =
if locationArgs isnt null
semanticValueArgs.concat(locationArgs)
else
semanticValueArgs
# Run corresponding semantic action, result is in $$ (__).
handler = @_generator.context.__handler[productionNumber]
await handler(semanticActionArgs...).catch console.error
reduceStackEntry.semanticValue = @_generator.context.__
if (locationArgs)
reduceStackEntry.loc = CodeUnit.getSandbox().$$loc
# Then push LHS.
this._stack.push(reduceStackEntry)
nextState = this._table.get()[previousState][symbolToReduceWith]
# And push the next state (goto)
this._stack.push(nextState)
return
class ParserTransform extends Stream.Transform
constructor: (grammar,options = {}) ->
options.objectMode = true
super options
generator = new StreamingLRParserGenerator {grammar}
generator.generateParserData()
generator.context.emit = (chunk) => @push chunk
@parser = new StreamingLRParser generator
return
_transform: (chunk,encoding,next) ->
token =
type: if chunk.eof then SpecialSymbols.EOF else chunk.token
value: chunk.text
line: chunk.line
column: chunk.column
unless @parser._stack.length >= 1
next new Error "Parser stack too small"
return
state = @parser._peek()
column = token.type
entry = @parser._table.get()[state][column]
unless entry
next new Error "Unexpected token #{column} in state #{state} at line #{token.line} column #{token.column}"
return
switch LRParsingTable.getEntryType(entry)
when EntryType.SHIFT
@parser._shift token, entry
@parser._shiftedToken = token
next()
when EntryType.REDUCE
do =>
try
await @parser._reduce entry, @parser._shiftedToken
# Don't advance tokens on reduce.
@_transform chunk,encoding,next
catch error
next error
return
when EntryType.SR_CONFLICT
next new Error "ConflictError: shift-reduce in #{state}, #{column}"
when EntryType.RR_CONFLICT
next new Error "ConflictError: reduce-reduce in #{state}, #{column}"
when EntryType.ACCEPT
# Pop starting production and its state number.
@parser._stack.pop()
parsed = @parser._stack.pop()
unless @parser._stack.length is 1 and @parser._stack[0] is @parser._startingState and chunk.eof
next new Error "Unexpected token #{token}"
return
result = {status: 'accept'}
if parsed.hasOwnProperty 'semanticValue'
result.value = parsed.semanticValue
@emit 'success', result
next()
return
_final: (next) ->
do =>
error = null
try
while @parser._stack.length > 1
await new Promise (resolve) =>
@_transform eof:true, null, resolve
catch error
if error?
next error
else
next()
return
return
module.exports = ParserTransform