Puppy.Runtime.Driver
- Package
- purescript-puppy-runtime
- Repository
- katsujukou/purescript-puppy-runtime
The table-driven parser every generated module calls.
A generated module builds a Table out of its emitted arrays and hands it
to one of the runners below. Nothing here knows anything about a particular
grammar, and nothing here depends on the generator, so a project that
merely uses a generated parser only needs this package.
Puppy.Runtime is the front door and re-exports all of this. Import that
one; this module is where the parser happens to live.
There is one LR loop here, and it is a machine that stops. start returns
it already waiting for a token, resume gives it one and gets back either
the next wait or an answer, and everything else in this module is an
adapter over those two: parse feeds it from an array, parseM from an
action in whatever monad the caller lexes in.
Stopping between tokens is not a contrivance. An LR parser consumes the lookahead only when it shifts -- a run of reductions all inspect the same one -- so "waiting for a token" is a state the automaton already passes through, and the machine here simply hands it back instead of reaching for the next element of an array. Nothing is buffered and nothing is read ahead: exactly one token is outstanding at a time.
#Action Source
type Action = IntOne cell of the LR action table, indexed by (state, terminal), as an
Int.
0 the token is an error here
1 accept
n >= 2 shift, and go to state n - 2
n < 0 reduce by production -n - 1
A number rather than a constructor, because of how many of these there are. A table with a cell for every (state, terminal) pair is read by index instead of searched, which is the whole point of an LR parser being a table -- and a cell for every pair is affordable as a number and is not affordable as an object. For a real grammar the difference is a table half the size that answers in one read instead of a scan.
Build them with the four below rather than by arithmetic.
#errorAction Source
errorAction :: ActionThe token cannot appear here.
#acceptAction Source
acceptAction :: ActionThe input is complete.
#ProductionInfo Source
type ProductionInfo = { arity :: Int, lhs :: Int, name :: String }What the driver needs to know about a production in order to reduce by it.
#Recovery Source
type Recovery tok val = { terminal :: Int, value :: ParseError tok -> val }What a parse needs in order to carry on past an error.
One thing rather than two, because a terminal number with no way to make a
value for it, or a value with no terminal to attach it to, is not half of
anything. terminal is one past endTerminal: recovery is the only thing
that names it, terminalIndex never returns it, and nothing lists it
among the tokens that were expected.
#Table Source
type Table tok val = { action :: Int -> Int -> Action, endTerminal :: Int, goto :: Int -> Int -> Int, production :: Int -> ProductionInfo, recovery :: Maybe (Recovery tok val), semanticAction :: Int -> Array val -> val, startState :: Int, terminalIndex :: tok -> Int, terminalName :: Int -> String, terminalValue :: tok -> val }Everything a generated module supplies. val is the type the generated
code boxes every semantic value into; the driver only ever moves those
values around, so it never needs to look inside one.
#Resume Source
newtype Resume tok valA parser stopped for want of a token.
Opaque on purpose. What is inside is the two stacks and the invariant
between them, and a caller who could take it apart could put it back
together wrong; resume and unexpectedEnd are the whole of what may be
done with one.
#canConsume Source
canConsume :: forall tok val. Resume tok val -> tok -> BooleanWhether the parser could take this token, without taking it.
A question about the tables and the state stack, and about nothing else. A
reduction on the way to the answer is followed on the states alone: no
semantic value is unboxed, terminalValue is never asked what the token
carries and semanticAction is never run. Answering has no cost beyond
the reductions it walks and leaves nothing behind, so the same Resume
can be asked about as many tokens as a caller likes.
true includes accepting. A token that ends the parse is one the parser
can take, which is what a caller looking for somewhere to carry on from
wants to hear -- the alternative is a caller that walks past the end of a
complete parse looking for a better place.
expected is not a substitute for this. That is the cheap approximation
every LR parser reports: it asks the top state alone and so can name a
token that the reduction underneath would have rejected. This follows the
reductions, and so answers exactly.
#RecoveryResult Source
data RecoveryResult tok valWhat came of a parse that was allowed to carry on past an error.
Three answers rather than two, because "it parsed" and "it parsed, and here is what was wrong with it" are different things to be told and a caller acts differently on each. The errors are in the order they were found.
Constructors
ParseSucceeded valParseRecovered (Array (ParseError tok)) valParseFailed (Array (ParseError tok))
Instances
(Eq tok, Eq val) => Eq (RecoveryResult tok val)(Show tok, Show val) => Show (RecoveryResult tok val)
#parseMRecovering Source
parseMRecovering :: forall m tok val. MonadRec m => Table tok val -> m tok -> m (RecoveryResult tok val)Parse from tokens pulled one at a time, carrying on past what it can.
The same LR loop as parseM, read one case further: where that one stops
at a failure, this one looks for somewhere the grammar said a parse may be
picked up again, puts an ERROR there, and then throws tokens away until
it finds one the parser can take.
canConsume is what "can take" means, and it has to be that rather than
the cheaper question. A token the top state would reduce on and the state
underneath would reject is not one to carry on from; taking it would fail
again immediately and the parser would throw the next one away for the
same reason, all the way to the end.
End of input is where this stops. A parser that cannot take it has nowhere left to go, and asking the source for another token after it has said there are none is asking a question that has already been answered.
#parseRecoveringAt Source
parseRecoveringAt :: forall tok val. Table tok val -> (Int -> tok) -> RecoveryResult tok valParse from something a caller can look up by position, recovering where it can.
at has to be total, and out past the end of the input it has to keep
answering with the terminal the grammar ends on. A generated module gets
that for nothing: its driver token is a Maybe, and looking past the end
of an array is Nothing, which is the end of input as far as the tables
are concerned.
There is no second loop here. Reading by position is a token source like
any other once it is given somewhere to keep the position, so this is
parseMRecovering with that somewhere -- and no copy of the input is made
on the way.
#unexpectedEnd Source
unexpectedEnd :: forall tok val. Resume tok val -> ParseError tokThe error a parser is owed when its supply of tokens runs out.
This is not how a parse is finished. A grammar ends on a terminal of its
own -- Nothing, for a generated parser -- and a caller who has one to
give should call resume with it. This is for a caller who has not: the
array ran off the end, the file was truncated, the socket closed.
It is a function rather than a field of Await because expected costs a
pass over every terminal in the grammar. Computing it each time the parser
stopped for a token would make parsing cost the length of the input times
the size of the alphabet, to answer a question almost no caller asks.
#parse Source
parse :: forall tok val. Table tok val -> Array tok -> Either (ParseError tok) valRun the LR automaton over an array of tokens.
The array must be terminated by whatever token the grammar declared as its
end marker; this does not append one. Running off the end is therefore
reported as an error rather than treated as end of input -- see
unexpectedEnd.
#parseM Source
parseM :: forall m tok val. MonadRec m => Table tok val -> m tok -> m (Either (ParseError tok) val)Run the LR automaton over tokens pulled one at a time.
The action is run exactly when the parser wants a token, and not once
after it has an answer: a parse that accepts or fails on the token it is
holding does not ask for another. There is no need for the source to know
when to stop, only what to say at the end, which for a generated parser is
Nothing.
Whatever else m can do, the source may do while lexing. Failing is the
useful case -- a lexer that meets a character it cannot read wants to say
so, and in ExceptT or Aff saying so abandons the parse without the
driver needing an opinion about it.
MonadRec rather than Monad because the loop is as long as the input.