123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497149814991500150115021503150415051506150715081509151015111512151315141515151615171518151915201521152215231524152515261527152815291530153115321533153415351536153715381539154015411542154315441545154615471548154915501551155215531554155515561557155815591560156115621563156415651566156715681569157015711572157315741575157615771578157915801581158215831584158515861587158815891590159115921593159415951596159715981599160016011602160316041605160616071608160916101611161216131614161516161617161816191620162116221623162416251626162716281629163016311632163316341635163616371638163916401641164216431644164516461647164816491650165116521653165416551656165716581659166016611662166316641665166616671668166916701671167216731674167516761677167816791680168116821683168416851686168716881689169016911692169316941695169616971698169917001701170217031704170517061707170817091710171117121713171417151716171717181719172017211722172317241725172617271728172917301731173217331734173517361737173817391740174117421743174417451746174717481749175017511752175317541755175617571758175917601761176217631764176517661767176817691770177117721773177417751776177717781779178017811782178317841785178617871788178917901791179217931794179517961797179817991800180118021803180418051806180718081809181018111812181318141815181618171818181918201821182218231824182518261827182818291830183118321833183418351836183718381839184018411842184318441845184618471848184918501851185218531854185518561857185818591860186118621863186418651866186718681869187018711872187318741875187618771878187918801881188218831884188518861887188818891890189118921893189418951896189718981899190019011902190319041905190619071908190919101911191219131914191519161917191819191920192119221923192419251926192719281929193019311932193319341935193619371938193919401941194219431944194519461947194819491950195119521953195419551956195719581959196019611962196319641965196619671968196919701971197219731974197519761977197819791980198119821983198419851986198719881989199019911992199319941995199619971998199920002001200220032004200520062007200820092010201120122013201420152016201720182019202020212022202320242025202620272028202920302031203220332034203520362037203820392040204120422043204420452046204720482049205020512052205320542055205620572058205920602061206220632064206520662067206820692070207120722073207420752076207720782079208020812082208320842085208620872088208920902091209220932094209520962097209820992100210121022103210421052106210721082109211021112112211321142115211621172118211921202121212221232124212521262127212821292130213121322133213421352136213721382139214021412142214321442145214621472148214921502151215221532154215521562157215821592160216121622163216421652166216721682169217021712172217321742175217621772178217921802181218221832184218521862187218821892190219121922193219421952196219721982199220022012202220322042205220622072208220922102211221222132214221522162217221822192220222122222223222422252226222722282229223022312232223322342235223622372238223922402241224222432244224522462247224822492250225122522253225422552256225722582259226022612262226322642265226622672268226922702271227222732274227522762277227822792280228122822283228422852286228722882289229022912292229322942295229622972298229923002301230223032304230523062307230823092310231123122313231423152316231723182319232023212322232323242325232623272328232923302331233223332334233523362337233823392340234123422343234423452346234723482349235023512352235323542355235623572358235923602361236223632364236523662367236823692370237123722373237423752376237723782379238023812382238323842385238623872388238923902391239223932394239523962397239823992400240124022403240424052406240724082409241024112412241324142415241624172418241924202421242224232424242524262427242824292430243124322433243424352436243724382439244024412442244324442445244624472448244924502451245224532454245524562457245824592460246124622463246424652466246724682469247024712472247324742475247624772478247924802481248224832484248524862487248824892490249124922493249424952496249724982499250025012502250325042505250625072508250925102511251225132514251525162517251825192520252125222523252425252526252725282529253025312532253325342535253625372538253925402541254225432544254525462547254825492550255125522553255425552556255725582559256025612562256325642565256625672568256925702571257225732574257525762577257825792580258125822583258425852586258725882589259025912592259325942595259625972598259926002601260226032604260526062607260826092610261126122613261426152616261726182619262026212622262326242625262626272628262926302631263226332634263526362637263826392640264126422643264426452646264726482649265026512652265326542655265626572658265926602661266226632664266526662667266826692670267126722673267426752676267726782679268026812682268326842685268626872688268926902691269226932694269526962697269826992700270127022703270427052706270727082709271027112712 |
- .RP
- .TL
- Top-down Non-Correcting Error Recovery
- in LLgen
- .AU
- Arthur van Deudekom
- Peter Kooiman
- .AI
- Department of Mathematics and Computer Science
- Vrije Universiteit
- Amsterdam
- Supervised by
- .AU
- dr. D. Grune
- .AI
- Department of Mathematics and Computer Science
- Vrije Universiteit
- Amsterdam
- .AB
- This paper describes the design and implementation of a parser
- generator with non-correcting error recovery based on the extended LL(1)
- parser generator LLgen. It describes a top-down algorithm for implementing
- this error recovery technique that can handle left-recursive grammars.
- The parser generator has been tested with several existing ACK-compilers,
- among which C and Modula-2. Various optimizations have been tried and are
- discussed in this paper.
- .AE
- .LP
- .nr PS 12
- .nr VS 14
- .NH
- Introduction
- .EQ
- delim $$
- .EN
- .nr PS 10
- .nr VS 12
- .RS
- .LP
- One of the trickier problems in constructing parser-generators is what
- to do when the input to the generated parser is not well formed. Several
- approaches are known, most of which are `correcting', meaning that they
- modify the input to make it correct. However, in most cases there are
- several possible corrections, and often the one chosen will turn out
- to be the wrong one. As a result of such an incorrect choice, spurious error
- messages can occur. Every programmer knows from experience how the omission
- of a single `)' can on occasion lead to pages of error messages.
- .LP
- A radically different approach is to just discard all the input up to
- and including the offending token, and start with a clean slate at the
- token following the offending one. [RICHTER] describes how
- this idea can be used to construct a non-correcting error recovery system
- that will never introduce spurious error messages. It is, however,
- possible that errors are overlooked.
- .LP
- In this paper we describe the incorporation of this non-correcting error
- recovery into LLgen, an existing LL(1) parser generator.
- In this introduction, we will describe in detail this non-correcting error
- recovery technique, give an overview of LLgen and how it handles
- errors, and finally describe how we have incorporated noncorrecting
- error recovery in LLgen.
- .RE
- .NH 2
- Non-correcting syntax error recovery
- .LP
- Richter describes how syntax error recovery can be done
- without making any corrections to the input text. Richter gives three
- reasons why recovery without correction is desirable:
- .IP 1
- In most cases there are many possible corrections, the choice among which
- will severely influence the further processing of the input. Thus, the
- probability of selecting the right correction is not high.
- .IP 2
- The harm done by selecting the wrong correction is often unlimited.
- .IP 3
- The loss of information to the user of a non-correcting recovery technique
- need not be grave.
- .LP
- The non-correcting technique described by Richter can be summarized as
- follows: When a syntax-error has occurred, the input up to and including the
- erroneous symbol is discarded; the remainder of the
- input is processed by a substring parser of the input
- language, that is a parser that recognizes any substring of a string in the input
- language. When the substring parser detects a syntax error, the offending
- symbol is reported as another error, and the input up to and including the
- erroneous symbol is discarded. The process is then repeated with the remaining input, possibly
- finding other syntax errors, until all the input is scanned.
- This process yields what Richter calls a
- .I
- suffix analysis
- .R
- of an input string. Formally, given an input string
- .I x
- , suffix analysis produces a set of strings $w sub k$ and a set of symbols
- $ a sub k$ such that
- .br
- .IP
- $x~ =~ w sub 0 a sub 0 w sub 1 a sub 1~...w sub n-1 a sub n-1 w sub n$
- .LP
- and such that:
- .br
- .IP
- $w sub 0$ is the longest prefix of $x$ that is a prefix of
- a string in the input language L, formally: there is a string $y$ such that
- $w sub 0 y$ is in L, but there is no string $z$ such that $w sub 0 a sub 0 z$
- is in L;
- .IP
- For $0 < k < n$, $w sub k$ is a longest substring of $x$ that is also a
- substring of a string in L, formally there are strings $u$ and $v$ such that
- $u w sub k v$ is in L, but there are no strings $y$ en $z$ such that
- $y w sub k a sub k z$ is in L;
- .IP
- $w sub n$ is a substring of $x$
- that is a substring of a string in L, formally:
- there exist $u$ and $v$, such that $u w sub n v$ is in L. Note that
- $w sub n$ need not be a suffix of a string in L, if $x$ represents incomplete
- input $w sub n$ is not a suffix of a string in L.
- .LP
- Now, the $a sub k$ indicate points at which an error is detected. The
- "real" error need not be at $a sub k$, it can have occurred anywhere
- within $w sub k a sub k$.
- In his paper, Richter shows that, although this method may miss errors, it
- will never introduce spurious errors.
- .LP
- For implementing the technique, a parser that recognizes any
- substring of the input language is needed. If we confine ourselves to
- syntactical analysis, it is sufficient to construct a substring
- recognizer. Richter himself does not give a practical construction, but
- [CORMACK] describes how a LR substring parser can be constructed
- that handles BC-LR(1,1) grammars. In this paper, we describe the construction
- of a LL substring recognizer that can handle any grammar. Furthermore,
- our recognizer is actually a suffix-recognizer, that is, a recognizer that
- recognizes any suffix of a string in the input language. Our suffix recognizer has the
- correct-prefix property,
- meaning that it detects the first syntax error as early as possible
- in a left-to-right scan of the input. Specifically, if the input language
- is L and the invalid input is $x$ , it finds a string $w$ and an input symbol
- $a$ such that $x = way$ , there is a string $z$ such that $wz$
- is in L, and there is no string $z$ such that $waz$ is in L.
- Because the suffix parser has this correct-prefix property, it can be
- used as a substring parser, because it will detect the first input symbol that
- is not part of a substring of the language. Because it is a suffix-recognizer,
- it additionally will detect incomplete input, because in that case
- at the end of the input the parser will not be in an accepting state.
- .NH 2
- Overview of LLgen
- .LP
- LLgen is an extended LL(1) parser generator. For a complete description,
- see [GRUNE].
- LLgen can actually handle grammars that are not LL(1), because it allows
- the use of conflict-resolvers. In case of an LL(1) conflict, these resolvers
- are used to statically or dynamically decide which rule to use. As we will see
- later, this feature makes it necessary for the suffix-recognizer to
- handle grammars that are not LL(1). Semantic actions can occur anywhere
- in the grammar rules, and they are executed when their position is
- reached during parsing. A typical LLgen rule looks like
- .br
- .IP
- S: A {
- .I action
- } B
- .LP
- where the action is a piece of C-code, that will be executed
- when the parser is using the rule for S and has recognized A.
- .LP
- LLgen-generated parsers use correcting syntax error recovery, based on a
- scheme designed by R\*:ohrich [ROEHRICH], inserting or deleting symbols at the point of error detection
- until correct input results. This means that actions in the parser will
- always be executed in an order that could also have resulted from
- syntactically correct input, and most importantly, once a grammar-rule
- is started it is guaranteed to be completed. This means that syntactic
- errors can never result in inconsistencies for the actions. Actions
- only have to deal with syntactically correct input. In a nutshell, the
- error recovery in LLgen-parsers works as follows: Suppose the parser is
- presented with correct input that breaks off before the end. The error
- recovery mechanism now provides a continuation path, chosen in such a
- way that all active rules are left as soon as possible. Effectively, the
- continuation path is the `shortest way out'. The symbols on this path are
- called `acceptable', and end-of-file is also `acceptable'. Furthermore, at
- each point along this `shortest path' there can be other terminals that
- would be correct; these are `acceptable' as well. Now, when an
- error occurs, all symbols that are not acceptable are discarded, until
- an acceptable symbol appears in the input. The tokens on the path up to
- but not including the acceptable input symbol are inserted.
- From then on, normal parsing resumes.
- .NH 2
- Incorporation of non-correcting error recovery in LLgen
- .LP
- An important consideration in incorporating the non-correcting recovery
- in LLgen was that correct programs should suffer as little as possible
- in what regards compilation speed. Furthermore, the existing error
- recovery method has the highly desirable property that rules that are
- started will be finished too, thus ensuring that errors in the
- input text will not cause inconsistencies in the semantic actions. We have
- implemented the non-correcting error recovery in such a way that this
- property is preserved.
- .LP
- The way we have achieved these goals is by actually including
- the suffix recognizer as a `second recognizer' in the generated parser.
- Correct programs are handled in the usual way by the parser, but if an error
- occurs the following happens: instead of going to the standard error
- recovery routine, the parser starts executing the non-correcting error
- handler. This process continues, reporting all errors, until the
- end of the input text is reached. Then, control is handed back to
- the standard error recovery routine. This routine will now think
- there is no more input, and thus start inserting tokens so as to construct
- a `shortest way out'. This ensures that all rules that were started are
- also finished, and no inconsistencies can occur in the semantic actions.
- However, this method does require some modifications to the error reporting
- routine. Normally, if the generated parser inserts a token, it reports
- this to the user, but in this case this is undesirable. The insertions only
- serve to maintain consistency in the semantic actions
- and do not signify errors, so reporting of insertions should be suppressed.
- .bp
- .nr PS 12
- .nr VS 14
- .PS
- boxwid = boxwid / 1.5
- boxht = boxht / 1.5
- arcrad = arcrad / 1.5
- movewid = movewid / 1.5
- moveht = moveht / 1.5
- arrowwid = arrowwid / 1.5
- arrowht = arrowht / 1.5
- arrowhead = arrowhead / 1.5
- linewid = linewid / 1.5
- lineht = lineht / 1.5
- .PE
- .NH
- The LL suffix parser
- .nr PS 10
- .nr VS 12
- .RS
- .LP
- In this chapter, we describe the construction of the LL suffix parser.
- The described parser is not restricted to LL(1) grammars, because the
- presence of conflict resolvers in LLgen allows for more general grammars,
- that may even be left-recursive. We start this chapter with a discussion
- of the implications of conflict resolvers, and continue with descriptions
- of the parser algorithm, the used data-structures,
- the handling of left- and right recursion, and some possible optimizations.
- .RE
- .NH 2
- LLgen conflict resolvers and their implications
- .LP
- In grammars that are nearly but not completely LL(1), conflicts
- will arise in the two places where parsing decisions are made: the choice
- of which alternative to start (`alternation conflicts') and the decision
- to stop or continue a repeated item (`repetition conflicts'). In order to
- allow LLgen to handle this type of grammar, the user can
- specify conflict resolvers in those places where conflicts arise.
- These resolvers are Boolean expressions labeling an alternative,
- and are evaluated when a conflict arises during parsing. If the
- expression evaluates to `true' the labeled alternative will be taken.
- The Boolean expressions are expressions in C, and can consult
- any information available at the point they occur.
- However, if a syntactic error has occurred in the input, and the non-correcting
- error recovery starts, we can no longer rely on the conflict resolvers to
- guide parsing decisions. The suffix recognizer is only concerned with
- syntax, and will not execute any semantic actions. It recognizes suffices
- of correct input, but does not know or care what prefix would make
- the suffix a correct program; as a result, the information that conflict
- resolvers could use is not available, because the semantic actions
- that would build this information have not been executed.
- Therefore, the information used by the conflict resolvers is no longer
- reliable, and the suffix parser needs to be able to handle the underlying
- grammar without their help. In particular, it has to be able to handle
- left-recursive grammars.
- .NH 2
- The suffix parser algorithm
- .LP
- Our algorithm needs easy access to the grammar rules; in the description
- we assume there is an efficient way to access the grammar rules. In
- the next chapter we will describe the details of the actual implementation.
- For the moment, we will only consider grammars that are not left- or
- right-recursive. In the next section, we will discuss how the algorithm has to be adapted
- to handle left- and right recursion.
- .LP
- Suppose the grammar is G, and the input to the suffix recognizer is
- $a sub 0 a sub 1 ... a sub n-1 a sub n$. Remember that parsing is
- always started by the `normal' LLgen generated parser. It's only after
- a syntactic error has occurred that the suffix recognizer will be started.
- The input to the suffix recognizer thus is the `tail' of the input, starting
- at the first symbol after the position where the first syntax error was
- found.
- .LP
- Now, in order to get parsing going again, the parser scans the grammar
- for rules which contain symbol $a sub 0$ in the right hand side:
- .br
- A: $alpha ~ a sub 0 ~ beta$
- .br
- .LP
- where $alpha$ and $beta$ represent a string of terminals and non-terminals,
- possible empty. Now, for each of these rules found, and for any string
- $b sub 0 b sub 1$...$ b sub m$ that can be generated by $beta$ it holds that
- $a sub 0 b sub o b sub 1$...$b sub m$ is a substring of some string in L.
- This can be shown as follows, supposing that the start symbol is S and
- S $-> sup * gamma$ A $delta$:
- .br
- S $-> sup * gamma$ A $delta$ $-> sup * gamma ~ alpha ~ a sub 0 beta ~ delta
- -> sup * gamma ~ alpha ~ a sub 0 b sub 0 b sub 1$...$b sub m delta$
- .br
- Of course, there may very well be more than one such string
- $b sub 1 b sub 2$..$b sub m$, and one of these strings can be empty as well, if
- $beta$ can produce empty. Now, in what we will call the
- .I
- predicting phase
- .R
- the algorithm will
- produce all possible symbols $b sub 0$. Then, in what we will call the
- .I
- accepting phase
- .R
- these symbols are matched against
- the input, and those not matching are discarded. Then, entering the next
- predicting phase, the algorithm will produce
- all symbols $b sub 1$, and match them against the next input symbol in
- the subsequent accepting phase,
- etc. In case one of the strings $b sub 0$...$b sub m$ is empty, or
- the end of one of the strings is reached, some way to continue is
- needed; we will discuss this later. First let's see how the
- algorithm produces the strings $b sub 0$...$b sub m$ .
- .LP
- For each rule in the grammar of the form
- .br
- A: $alpha a sub 0 W sub 1 W sub 2$...$W sub p$
- .br
- with each $W sub k$ a terminal or nonterminal, a
- .I
- prediction graph
- .R
- is created that looks like this:
- .PS
- down; box "$a sub 0$"; arrow; box "$W sub 1$"; arrow
- box "$W sub 2$"; arrow dashed; box "$W sub p$"
- arrow; box "END" "$[A]$"
- .PE
- .LP
- The bottom element of these prediction graphs is an end-marker containing the
- left-hand side of the rule used. All these graphs have $a sub 0$ on top, and
- this $a sub 0$ is matched against the $a sub 0$ in the input in the
- accepting phase that follows, removing the
- $a sub 0$ from the graph. If the prediction graph is now empty, we have to find a way
- to continue; this case is treated later. First we will consider what to do if
- the prediction graph is not empty. There are two possibilities: either $W sub 1$ is a
- terminal, or it is a nonterminal. If it is a terminal, we are finished for
- the moment; if not, the algorithm scans for rules of the form
- .br
- $W sub 1$: $U sub 1 U sub 2$...$U sub i$
- .br
- .LP
- with each $U sub k$ a terminal or nonterminal. Now, the algorithm substitutes
- the top of the prediction graph with the right-hand sides
- of all the rules found. Because there can be more than one rule, the
- prediction graph can now become a DAG (Directed Acyclic Graph).
- Supposing there are two rules with $W sub 1$ in the LHS:
- .br
- $W sub 1$: $U sub 1 U sub 2$...$U sub i$
- .br
- $W sub 1$: $V sub 1 V sub 2$...$V sub j$
- .LP
- the prediction graph will now look like this:
- .PS
- B1: box "$U sub 1$"
- move
- B2: box "$V sub 1$"
- arrow dashed down from bottom of B1
- B3: box "$U sub i$"
- arrow dashed down from bottom of B2
- B4:box "$V sub j$"
- move to 0.5 <B3.se, B4.sw>
- down;move
- B5:box "$[W sub 1 ]$"
- arrow dashed;
- box "$W sub p$"
- arrow;
- box "END" "$[A]$"
- arrow from B3.bottom to B5.top
- arrow from B4.bottom to B5.top
- .PE
- .LP
- The graph element representing $W sub 1$ is left in the stack, the
- notation $[W sub 1 ]$ indicates it has been substituted. These substituted
- element will from now on be ignored by the algorithm. The elements
- $U sub 1$ and $V sub 1$ are now `on top' of the prediction graph.
- .LP
- If $W sub 1$ can also produce empty, its successor in the prediction graph
- has to be processed
- as well; the algorithm walks down the graph to this successor, and
- there the process is repeated; if it is a terminal we are finished, else we
- substitute it with the right hand sides of its grammar rule.
- However, the element that we want to substitute now, say $W sub k$, cannot
- be marked `substituted' just like that, because it can be on another
- path, on which it cannot be substituted yet. Therefore, a copy of element
- $W sub k$ is made, it is marked $[W sub k ]$, and an edge is created
- from $[W sub k ]$ to the successor of $W sub k$. This produces graphs like
- this:
- .br
- .PS
- B1: box "$U sub 1$"
- move
- B2: box "$V sub 1$"
- move
- X1:box "$X sub 1$"
- arrow dashed down from bottom of B1
- B3: box "$U sub m$"
- arrow dashed down from bottom of B2
- B4:box "$V sub j$"
- arrow dashed down from bottom of X1
- Xj: box "$X sub j$"
- move to 0.5 <B3.se, B4.sw>
- down;move
- B5:box "$[W sub 1 ]$"
- arrow dashed;
- B6: box "$W sub k$"
- arrow
- Wk1:box "$W sub k+1$"
- arrow dashed
- box "$W sub n$"
- arrow;
- box "END" "$[A]$"
- arrow from B3.bottom to B5.top
- arrow from B4.bottom to B5.top
- move down from Xj.top;move;move;move
- Wk: box "$[W sub k ]$"
- arrow from Xj.bottom to Wk.top
- arrow from Wk.bottom to Wk1.top
- .PE
- .LP
- This process of substituting is repeated with all nonterminals that are
- now on top of the prediction graph, until there are only terminals on top of
- the graph.
- This completes the prediction phase of the algorithm, not taking into account
- what to do if an END marker appears on top of the graph.
- Now, the algorithm enters its accepting phase, in which
- the terminals on top are compared with the next symbol in the input.
- If a terminal in the graph matches the input, its element is deleted
- from the graph, and the substitution process will continue with its
- successors, in the next prediction phase.
- If a terminal on top of the graph does not
- match the input, the path it is on represents a `dead-end', which
- does not need to be processed any further. The terminal is no longer
- a `top', and the algorithm will not visit it again.
- .LP
- There is one tricky situation: consider again this graph:
- .PS
- B1: box "$U$"
- move
- B2: box "$a$"
- move to 0.5 <B1.se, B2.sw>
- down;move
- B5:box "$W sub 1 $"
- arrow dashed;
- box "$W sub n$"
- arrow;
- box "END" "$[A]$"
- arrow from B1.bottom to B5.top
- arrow from B2.bottom to B5.top
- .PE
- .LP
- Here, the algorithm is processing $W sub 1$ in the predicting phase, and
- using some rule it has produced $a$ on top; there is another rule with
- $W sub 1$ in its LHS which has produced nonterminal $U$ on top.
- Now, suppose $U$ is a nonterminal that can
- produce empty. Now, the algorithm starts substituting $U$, and walks
- down $W sub 1$. What we definitely do not want
- is the algorithm to start substituting $W sub 1$ again, because then we
- would loop forever. Therefore, if the algorithm starts processing
- element $W sub 1$ it should make it $[W sub 1 ]$ before it does
- anything else. On entering the element
- for the second time in the prediction phase , it sees that it is already substituted,
- so there is nothing to do.
- It then just walks to the successor of $W sub 1$ and
- starts substituting it. This is correct, since the fact that the algorithm
- enters an element for the second time in a prediction phase means that the element
- indirectly can produce the empty string, and thus its successor must
- be substituted as well in the prediction phase.
- .LP
- It is easy to see that the substitution process will stop: the algorithm can
- only loop if it starts processing an element for the second time in a
- prediction phase,
- or if the processing of an element eventually yields a graph with that
- same element on top.
- The first case cannot occur because the algorithm marks elements it is
- processing as `substituted' before it does anything else, meaning that those elements will not
- be processed again; the second case can only occur if the grammar is
- left-recursive, which we assumed it was not.
- .LP
- The algorithm simulates
- left-most derivations of strings $a sub 0 b sub 0 b sub 1$..$b sub n$
- starting from $a sub 0 W sub 1$..$W sub p$; as we showed before, if
- the algorithm recognizes a string $a sub 0 b sub 0$..$b sub n$ that
- string is a substring of some string in L. Conversely, because the
- algorithm start out by using all rules of the form
- A: $alpha a sub 0 beta$, and then proceeds to simulate all
- possible left-most derivations, it will recognize all input
- $a sub 0 b sub 0$... $b sub n$ that can be produced starting from
- $a sub 0 beta$.
- .LP
- Now we will discuss what has to be done if an END marker appears as
- top of the prediction graph.
- When this happens, it means that starting from some rule
- .br
- A: $alpha a sub 0 beta$
- .br
- the algorithm has produced a leftmost-derivation of a string
- $a sub 0 b sub 1 .. b sub n$ starting from $a sub 0 beta$, or that $beta$ can produce
- empty and the string so far is just $a sub 0$. The next step is to assume
- that the have recognized A and that that some string produced by $alpha$
- is part of the prefix that makes the suffix we are recognizing a
- correct string in L. Remember that in the END marker we kept record of
- the LHS of the rule that has started the graph, and we will now use this
- LHS to continue recognizing. What the algorithm does is scan for all
- rules of the form:
- .br
- B: $gamma$ A $delta$
- .br
- with $gamma$ and $delta$ possibly empty strings of terminals and nonterminals.
- The algorithm now starts a new component in the prediction graph, and if $delta$ is
- $W sub 1 W sub 2$...$W sub n$ it looks like this:
- .PS
- down;box "$W sub 1$"; arrow
- box "$W sub 2$"; line dashed; box "$W sub n$"
- arrow; box "END" "$[B]$"
- .PE
- .LP
- Note that the END marker now contains B, because we have started to match
- a rule for B. If the $delta$ in the rule for B was empty, this just produces
- and END marker with B in it; in this case, the process is just repeated
- with all rules of the form:
- .br
- C: $zeta$ B $eta$
- .br
- .LP
- etc, until we have a prediction graph with a nonterminal or terminal on top.
- Now, the substitution algorithm is again applied over all nonterminals on
- top, until every top contains a terminal. It is possible that during
- substitution again an END marker will turn up; if this happens
- we again scan for rules to continue with etc.
- This `continuation algorithm' can only loop if, when
- trying to build a new prediction graph for matched symbol A, it produces an empty
- graph with again matched symbol A. If this happens, the grammar was
- (directly or indirectly) right-recursive, and we assumed that it was not.
- Therefore, the algorithm will terminate. The terminals on top of the
- new graph after applying this `continuation' algorithm are exactly those
- that could follow the string $A sub 0 b sub 0$..$b sub n$ in a substring
- of a string in L.
- To see this, suppose we have `recognized' the rule
- .br
- A: $alpha a sub 0 beta$
- .br
- and $a sub 0 b sub 0 b sub 1$...$b sub n$ is the string produced from
- $a sub 0 beta$ by the algorithm. Now, using rule:
- .br
- B: $gamma$ A $delta$
- .br
- and supposing that S $->$ $zeta$ B $eta$ we get
- .br
- S $->$ $zeta$ B $eta$ $->$ $zeta gamma$ A $delta$ $eta$ $->$ $zeta gamma a sub 0 b sub 0 b sub 1$ ... $b sub n$ $delta$ $eta$
- .br
- .LP
- and thus any string produced by a derivation starting from
- $delta$ can come right after $a sub 0 b sub 0$...$b sub n$ in a substring
- of some string in L. The algorithm will proceed to generate all these
- strings starting from $delta$. If $delta$ produces empty, the above
- is just repeated. Because in the `continuation' part
- all possible rules are considered, the whole algorithm will recognize
- all substrings of any string in L. In order to determine if we
- have actually recognized a suffix of some string in L, we need to
- remember if within a predicting phase the `continuation' part of the algorithm has been run
- on an END marker containing the start-symbol S;
- if this is the case, then the input seen until now is a suffix of some string in L.
- Formally, it means that there is a derivation starting from start symbol
- $S$ such that if the
- input seen until now is $a sub 0 a sub 1$..$a sub n$, then:
- .br
- S $-> sup * alpha beta$ $-> sup * alpha a sub 0 a sub 1$..$a sub n$
- .br
- .LP
- where $alpha$ can be empty, $beta$ is not empty.
- .NH 2
- The prediction graph data structure
- .LP
- The graphs that are produced by the suffix recognizer may grow extremely
- large; to facilitate an efficient
- implementation we have devised a way of keeping the size of the
- data structure under control, in a way that is very similar to
- the way described in [TOMITA].
- .LP
- The basic idea is, that in a prediction phase of the algorithm, it is not
- necessary to explicitly substitute each nonterminal every time it
- turns up as a `top'; it is sufficient to do it once, because the
- second substitution will produce exactly the same subgraph starting at
- the substituted nonterminal. Here is an example:
- .PS
- down;box "$a$";arrow;box "A";arrow dashed;box "[B]";arrow
- box "C";arrow dashed;box "END" "[X]"
- move right from last box.e;
- box "END" "[Y]";
- arrow <- dashed up from last box.top;
- box "D";arrow <- up from last box.top
- box "B"
- .PE
- .LP
- Here, in the left component of the graph, nonterminal B has been
- substituted. Now, in the same prediction phase, the algorithm again runs into
- B, now in the right component. There is no need to compute again
- what the substitution will produce, it is exactly the part on top
- of B in the left component. Therefore, all that is needed is:
- .PS
- down;box "$a$";arrow;box "A";arrow dashed;
- B1: box "[B]";arrow
- box "C";arrow dashed;box "END" "[X]"
- move right from last box.e;
- box "END" "[Y]";
- arrow <- dashed up from last box.top;
- box "D"
- arrow from B1.bottom to last box.top
- .PE
- So, when, in a prediction phase of the algorithm, a nonterminal is substituted,
- the nonterminal is placed on a list, together with a pointer to
- the substituted nonterminal. If in the same prediction phase a nonterminal that
- is on the list becomes a top, all we need to do is place an edge
- between the already substituted one and the successor of the top we are currently
- processing. When a prediction phase is finished, the list is cleared.
- There is one catch: if we consider again the last picture,
- note that if nonterminal B can (directly or indirectly) produce empty,
- it is also necessary to substitute D. However, it is not difficult to
- determine if a nonterminal can produce empty. LLgen already computes
- this information for each nonterminal.
- .LP
- Without this `joining together' of graph components, each
- element in the graph has exactly one successor, except the END marker,
- which has none.
- Now that components get joined as described, an element can have any
- number of successors. The recognizer algorithm now has to consider all
- successors of a graph element instead of one.
- .NH 2
- Handling right recursion
- .LP
- The only problem right-recursive grammars cause in the algorithm is in the
- `continuation' part; they can cause this part of the algorithm to loop
- forever. As an example, consider:
- .br
- A: $alpha$ B
- .br
- B: $beta$ C
- .br
- C: $gamma$ A
- .LP
- Now suppose the `substitution' part of the algorithm has turned up
- an END marker with nonterminal A in it. The continuation algorithm will
- now produce:
- .PS
- box "END" "[A]";move;box "END" "[C]";move;box "END" "[B]";move
- box "END" "[A]";move;box "END" "[C]"
- .PE
- .LP
- etc. etc. However, a slight modification to the algorithm suffices
- to eliminate this problem; within each prediction phase of the algorithm, we
- simply maintain a list of nonterminals that have turned up in an
- END marker. As soon as an END marker turns up whose nonterminal is
- already in the list, we stop the `continuation' algorithm; the part
- of the graph that would be produced by it already has been generated
- by an earlier invocation of the algorithm in the same prediction phase.
- At the end
- of a prediction phase, when all heads are terminals, we clear the list.
- This way, no looping can occur; even if the right recursion is
- indirect, for instance if in the above example the rule for A had been
- .br
- A: $alpha$ B $delta$
- .br
- .LP
- where $delta$ can produce empty, the algorithm still works; the substitution
- of $delta$ will yield an END marker on top, and when trying to find
- a continuation for LHS A the algorithm notices A is already on the list.
- .NH 2
- Handling left recursion
- .LP
- Left-recursion is, unfortunately, a much tougher problem than
- right-recursion. The result of left-recursive grammar rules is that
- the substitution algorithm never stops, because it can keep on building
- the graph with the same set of rules without ever turning up a terminal.
- One course of action would be to pre-process the grammar rules to
- eliminate left-recursion; there are algorithms that eliminate direct
- and indirect left-recursion. However, we have taken another course; by
- allowing the produced graphs to contain loops, we can handle left
- recursion without any modifications to the grammar. As soon as
- we come to the point that we want to substitute a nonterminal
- which was already substituted earlier on the same path and in
- the same prediction phase, we can
- make a link from the `older' nonterminal to the successor of
- the `new' nonterminal. In this way we have constructed a loop
- in the graph. As an example, suppose we have the following rules:
- .br
- D: A
- A: B a
- B: A | x
- .br
- Suppose also that we have nonterminal `D' on top of a stack. We
- now start substituting `D':
- .PS
- A: box "A"
- move
- X: box "x"
- move to 0.5 <A.se, X.sw>
- down
- move
- B: box "[B]"
- arrow
- box "a"
- arrow
- box "[A]"
- arrow
- box "[D]"
- arrow dashed
- box "END" "[S]"
- arrow from A.s to B.n
- arrow from X.s to B.n
-
- .PE
- .LP
- We now have an `A' on top of of the stack which was already
- substituted on the same path and also in the same prediction phase. To avoid
- never ending substitution we make a loop as follows:
- .PS
- A: box "A" dashed
- move
- X: box "x"
- move to 0.5 <A.se, X.sw>
- down
- move
- B: box "[B]"
- arrow
- box "a"
- arrow
- A2: box "[A]"
- arrow
- box "[D]"
- arrow dashed
- box "END" "[S]"
- arrow dashed from A.s to B.n
- arrow from X.s to B.n
- arc <- from B.w to A2.w
- .PE
- .LP
- The dashed box with `A' in it means that it can be deleted, because
- there is already an occurrence of it in the loop.
- .LP
- The most beautiful result of loops in graphs is
- that the original parsing algorithm needs only one minor change.
- When the algorithm visits an element which has more than one
- outgoing edge the algorithm starts tracking down both paths,
- just like before, only now there may be one or more backedges among
- these edges, but the algorithm needs not to be aware of this fact.
- The only difficulty with loops is that the algorithm might go into
- a loop; it continues searching for terminals but it might happen
- that there are no valid terminals in the loop. The solution to this
- problem is not very difficult; just set a flag at all elements we
- visit. When we reach an element which has this flag turned on, we
- don't have to search any further. At the end of the prediction phase, when we
- have found all possible new heads, all flags are cleared.
- Even if there are no loops in the
- prediction graph, setting flags may be used as an optimization:
- it is possible that two paths come together at one point. In that situation
- it is useless to scan for the second time the part of the graph which
- both paths have in common.
- .NH 2
- Some optimizations using reference counts
- .LP
- As explained in section 2.2, it is sometimes necessary to copy a
- prediction graph element before substituting it. In order to determine
- if a certain element has to be copied, it is convenient to maintain
- a reference count in each graph element. This reference count keeps
- track of the number of edges that enter an element. Now, when we want
- to substitute an element with reference count not 0, we need to
- copy it, because there is another path in the prediction graph that
- contains the element we want to substitute, and on this other path
- the element cannot be substituted yet.
- .LP
- Maintaining reference counts also enables us to perform another
- optimization: remember that if, in a prediction phase, a terminal
- is predicted that does not match the current inputsymbol, we from
- then on just ignore the path in the graph starting at the terminal.
- However, we can safely delete the terminal from the graph; furthermore,
- all its successors in the prediction graph that have reference count
- 0 can be deleted as well, as can their successors with reference
- count 0, etc. This way, we delete from the prediction graph
- most elements that are no longer accessible, but not all of them; as will
- be explained in the next section, loops in the prediction graph
- can cause problems.
- .NH 2
- The algorithm to delete inaccessible loops
- .LP
- Deleting graph elements which are no longer reachable is not as easy
- as it looks when there are loops in the graph, introduced by
- the extension to the algorithm that handles left recursive grammars.
- Suppose for example that we have a very simple loop as in the left
- picture below:
- .PS
- down
- X: box "x" "(0)"
- arrow
- box "[B]" "(2)"
- arrow
- box "a" "(1)"
- arrow
- box "[A]" "(1)"
- arrow
- box "[D]" "(1)"
- arc <- from 2nd box.w to 2nd last box.w
- move right from X.ne
- move
- move
- move
- move
- move
- move
- down
- box "x" "(0)" dashed
- arrow dashed
- B: box "[B]" "(1)"
- arrow
- box "a" "(1)"
- arrow
- box "[A]" "(1)"
- arrow
- box "[D]" "(1)"
- arc <- from B.w to 2nd last box.w
- .PE
- .LP
- The number below each symbol indicates the reference count of that element.
- Suppose now that we delete `x', then we have the situation depicted in the
- picture on the right. The loop consisting of `[B]', `a' and `[A]' is now
- unreachable, so all these elements can be deallocated.
- The reference count of `[B]' is 1, so it will not be deleted. To be precise
- all elements in the loop have their reference counts on 1, and
- consequently none of these will be deleted. But we stated earlier
- that all elements of the loop cannot be reached anymore and that the
- loop had to be deleted! In this example the reference counts of the
- loop elements are all 1, but in more complex situations it is also
- possible that some of the elements have a reference count of more
- than 1.
- .LP
- To solve this problem we present an algorithm, devised by E. Wattel, that
- determines whether a loop can be deleted or not.
- The algorithm consists of two parts. The first part of the algorithm goes as
- follows: it presumes that all elements of the loop will indeed be
- deleted. Every time it deletes an element it decreases the reference
- count of all the successors of the element that are also member of the same
- loop. How the algorithm knows which elements belong to the loop and which
- do not will be explained later. The situation of the example above will now
- look like this:
-
- .PS
- down
- box "[B]" "(0)"
- arrow
- box "a" "(0)"
- arrow
- box "[A]" "(0)"
- arrow
- box "[D]" "(1)"
- arc <- from 1st box.w to 2nd last box.w
- .PE
- .LP
- The number below each symbol indicates again the reference count
- after we have applied the first part of the algorithm.
- .LP
- The second part of the algorithm checks and restores the
- reference counts of all members of the loop . When it finds
- out that one or more reference counts are not 0, it concludes
- that it is still possible to enter the loop in some way, and
- that it cannot be
- deleted yet. In the other case it reports that the loop can be
- deleted, which is also true in our example.
- .LP
- We will now formally describe the first part of the algorithm
- that finds all directed circuits from a given vertex, and determines if
- the vertices on those circuits can be deleted.
- The algorithm works on prediction-graphs in which every edge that
- is in a circuit is marked. Note that a marked edge may be in more than one circuit.
- We will call this mark `C'.
- The input to the algorithm is such a prediction graph, and a start vertex,
- say A. The first part of the algorithm is:
- .IP 1
- Put the start vertex A on a list L; mark all edges `unused'
- .IP 2
- If L is empty, stop
- .IP 3
- For each vertex in list L, check if there are edges marked both C' and
- `unused'. For each edge found, mark it `used', and traverse it to its
- other endpoint; put this endpoint on a new list M, initially empty
- .IP 4
- Decrease the reference count of all vertices on M by 1
- .IP 5
- L := M; go to 2
- .LP
- It is clear that the algorithm will terminate: each edge is only traversed once,
- and the number of edges is finite. We will now prove some properties of this
- part of the algorithm.
- .LP
- .I
- An edge is traversed by the algorithm if and only if it is on some
- directed circuit $A ->$...$->A$.
- .R
- .br
- The if-part is easy; if an edge $e$ connecting vertices $W$ and $V$ is on some directed circuit starting in
- $A$, then there is a path $A ->$...$-> W -> V$; let $A ->$...$-> W -> V$ be a path
- of minimum length from $A$ to $V$. If the length of the path from $A$ to
- $W$ is $k$, then after turn $k$ of the algorithm $W$ will be on list L. To see
- that this is the case, suppose that $W$ is not on list L after turn $k$;
- this means that the edge entering $W$ was already marked used in a
- previous turn, but then there would be a shorter path from $A$
- to $W$, contradicting the assumption that the path is of
- minimum length. The edge
- $e$ is marked `C', because it is in a circuit; it is marked `unused', for if
- it were marked used, there would be a shorter path from $A$ to $V$. So,
- in turn $k + 1$, the edge $e$ will be traversed.
- .LP
- On the other hand, suppose that an edge $e$ is traversed by the algorithm;
- we will show by induction on the number of turns the algorithm has made
- that $e$ is on a directed circuit $A->$..$->A$. In the first turn, all
- edges from $A$ that are marked `C' are traversed, and clearly, if an edge
- from $A$ is part of a circuit then that edge is part of a circuit from $A$ to $A$.
- Now suppose that in turn $n+1$ an edge $e$ connecting vertices $W$ and
- $V$ is traversed. This means the edge is
- marked `C', so it is part of some circuit. If there is a path from $V$ to $A$,
- we can simply trace a circuit
- $A->$...$-> W -> V -> $...$-> A$, and clearly $e$ is on a circuit from
- $A$ to $A$. Now, suppose there is no path from $V$ to
- $A$. We can always trace a circuit $W -> V ->$...$-> W$ because the
- edge from $W$ to $V$ is part of a circuit; and by the
- induction hypothesis there is a circuit $A ->$...$-> W ->$...$-> A$. We can
- now make a `detour' at $W$, yielding a circuit $A->$...$-> W -> V$...
- $-> W ->$...$-> A$. This case is shown in the picture below.
- So in either case $e$ is on a circuit from $A$ to $A$.
- .PS
- down;
- B1: box "A";
- arrow dashed;
- B3: box dashed;
- arrow dashed;
- B2: box "W";
- arrow dashed; box dashed;
- arc <- from B1.w to last box.w
- arrow right "$e$" "C" from B2.e
- box "V"; arrow dashed; box dashed;
- arrow dashed -> from last box.n to B3.e
- .PE
-
- .LP
- .I
- A vertex appears on list L if and only if it is on some directed
- circuit from $A$ to $A$.
- .R
- .br
- .LP
- If a vertex is in such a circuit, there is an edge that enters it, which
- is part of a circuit form $A$ to $A$; we already showed that this edge
- is traversed by the algorithm, and thus the vertex will appear on list
- L. Conversely, if a vertex appears on list L, then an edge entering
- that vertex has been traversed by the algorithm; we showed that this
- edge is part of a circuit from $A$ to $A$, and thus the vertex is
- part of a circuit from $A$ to $A$.
- .LP
- .I
- When the algorithm is finished, each vertex that is part of some
- directed circuit from $A$ to $A$ has its reference count decreased by exactly
- the number of edges entering it that are part of a directed circuit from $A$ to $A$.
- .R
- .br
- .LP
- Each edge that is part of some circuit from $A$ to $A$ is traversed
- exactly once; the reference count of the endpoint is decreased
- by one after an edge has been traversed. Thus, if a vertex is endpoint
- of $k$ such vertices, its reference count is decreased by $k$.
- .LP
- .I
- If the reference count of each of the vertices visited by the algorithm
- is 0 after the algorithm has finised, all these vertices can be deleted;
- if the reference count is not zero for one or more of the visited
- vertices, then none of them can be deleted.
- .R
- .br
- .LP
- Suppose all visited vertices have reference count 0; this means that
- each of the vertices is only entered by edges that are on a circuit
- from $A$ to $A$. Therefore, it holds that any path leading to any
- of the visited vertices has to start in one of the visited vertices; there
- is no path starting in an unvisited vertex to a visited one. Thus,
- all the visited vertices are unreachable.
- Conversely, if one of the visited vertices has reference count not zero,
- then there is a path from an unvisited vertex to this vertex. Because from
- the vertex with reference count non zero, we can get to $A$, and from $A$
- we can get to any of the other vertices, all visited vertices are
- reachable.
- .LP
- The second part of the algorithm now checks if all reference counts are
- zero, and if they are, it deletes all visited vertices.
- .NH 2
- Marking loop elements
- .LP
- One point we have omitted so far is how the edges in the prediction
- graph that are part of a loop get marked.
- Basically, a loop can be detected:
- a. when it is made;
- .br
- b. when we want to know about it.
- .LP
- The first approach checks if a loop is constructed
- as soon as we join two paths in the graph, and if so, marks all
- edges of the loop. The other approach does not do any checking when two
- paths are joined together; it starts looking for loops when we want
- to delete an element with reference count not 0, marking all edges
- belonging to the loops it discovers. In practice it turns out that
- we very often encounter elements that we would like to delete, but that have
- reference count not 0, whereas the joining of paths occurs relatively
- infrequently. We therefore have chosen to check if a loop is created
- when two paths in a prediction graph are joined.
- .LP
- Now the question arises how to find and mark all edges of
- the loop. For this problem we devised also an algorithm.
- Because we already know that there is an edge from the element on which
- the new path is connected to the successor of the joined element, the
- algorithm only has to find a path from this last element back to the first one.
- This can be done by a backtracking depth first search; to find a path from
- one element to another we have to find a possible empty path
- from one of the successors of the first element to the last element. As
- soon as we have found a path, we can mark all the edges on the path and also
- the backedge as loop edges. In case that there is more than one path
- back to the first element it is necessary that the algorithm continues
- searching after it has found one path.
- .LP
- To avoid looping of this algorithm we have to set a flag at the elements
- which are on the path already. When the algorithm is backtracking it can
- clear the flags at the elements it is leaving.
- .LP
- To speed up the searching process we can set flags at the edges we have already
- visited but did not lead back to the first element. When the algorithm
- encounters such an edge it already knows that this edge is not worth
- searching again and can be skipped. At the end of the algorithm these
- flags have to be cleared again.
- .LP
- One might propose another optimization: as soon as
- we reach an edge that is already marked as a loop edge, we
- can stop searching for other loop edges. There is, however,
- a case in which this can go wrong. Imagine the following situation:
- .PS
- down
- E: box "[E]"
- arrow " C" ljust
- D: box "[D]"
- arrow " C" ljust
- C: box "c"
- arrow " C" ljust
- box "b"
- arrow " C" ljust
- A: box "[A]"
- arrow
- box "a"
- move right from D
- move right
- J: box "[J]"
- down
- arrow from J.s " C" ljust
- I: box "i"
- arrow " C" ljust
- H: box "[H]"
- arrow from H.s to A.e
- arc <- from E.w to A.w
- move left from C
- move left
- "C"
- arc -> from H.e to J.e
- move right from I
- move right
- "C"
- arrow dashed from E.s to J.n
- .PE
- What we have here is a prediction graph with two loops; all edges that belong
- to a loop are again marked with an `C'. Note that the edge between `[H]'
- and `[A]' is not a loop edge. Suppose that `[J]' is not yet
- completely substituted, i.e. there is another production rule for
- J:
- .br
- J: E
- .br
- The `E' on top of the right path is now joined with the `[E]'
- on the left path, which is depicted by the dashed arrow
- between `[E]' and `[J]'. When we take a good look at the graph
- we see that the two loops are merged into one. But that is not
- the most important observation we have to make: not only the
- edge between `[E]' and `[J]' must be marked as a loop edge, but
- also the edge between `[H]' and `[A]'! So it is not possible
- to stop searching for loop edges as soon as we have found an
- edge which was already marked as a loop edge. We have to continue
- until we reach the element at which we started: `[E]'. So the
- optimization proposed above is incorrect.
- .NH 2
- Optimizations using FIRST and FOLLOW sets
- .LP
- In the algorithm as we have described it, every nonterminal on top of the graph
- is substituted until only terminals remain on top; these terminals are
- then matched against the current input symbol. However, by using
- FIRST sets, we can save considerably on the number of computations
- necessary. Suppose one of the top elements of the graph is nonterminal A,
- and the current inputsymbol is $a$. Then, it is of no use to substitute
- A if terminal $a$ is not in FIRST(A), because then substituting A will
- never produce $a$ on top of the graph. So, before substituting a
- nonterminal we check if the current inputsymbol is in its FIRST set; if
- it is not, we can declare the path the nonterminal is on a dead end, and
- delete it, without having to perform the actual substitution. Of course, if
- A can produce empty, we still have to consider its successor in the graph.
- .LP
- Similarly, when we have an END marker on top, with nonterminal B in
- it, and we consider using rule
- .br
- D: $alpha$ B C $gamma$
- .br
- We first check if the current inputsymbol is in FIRST(C); if this is
- not the case, there is no need to start a graph component with this
- rule, because it will never produce the next inputsymbol on top.
- Again, if C produces empty, we still have to evaluate the part of the
- rule following C.
- .LP
- To circumvent the problems caused in the FIRST set optimization by
- nonterminal that produce empty, we can also make use of FOLLOW-sets.
- When substituting, if we encounter a nonterminal whose FIRST set does
- not contain the current inputsymbol but which can produce empty,
- we check if the current inputsymbol is in its FOLLOW set. If it is not,
- there is no need to process its successor. Similarly, in case we
- are processing an END marker as explained above, there is no need
- to process the part of the rule following C if FIRST(C) does not
- contain the input symbol, or C produces empty but the inputsymbol
- is not in FOLLOW(C).
- .bp
- .nr PS 12
- .nr VS 14
- .NH
- Test results
- .nr PS 10
- .nr VS 12
- .RS
- .LP
- In this chapter, we discuss some test results that were obtained
- by recompiling existing ACK compilers with the modified LLgen.
- We tried several combinations of possible optimizations, including
- `dumb' ones, like no optimization at all, not even deleting unreachable
- prediction graph elements.
- The incorporation of LLgen with non-correcting error recovery went
- smoothly; only minor modifications to the Make-files were necessary.
- Specifically, these modifications consisted of passing an extra
- flag to LLgen, and including the new generated C-file Lncor.c in
- the list of generated C-files. Also, the LLmessage error reporting
- routine had to be adapted. We successfully recompiled the C, Modula-2
- and Occam compilers; in the next sections, we discuss some test results
- that were obtained with the Modula-2 and C compilers.
- .RE
- .LP
- .NH 2
- Performance
- .LP
- We will now present and discuss, with the aid of some
- diagrams, time and space measurements on the non-correcting error
- recovery. We have measured the effect of various optimizations.
- These optimizations include the first-set optimization and the follow-set
- optimization. We also measured the effect of leaving out the loop-deletion
- algorithm, regarding both time and space. We performed out measurements using
- C- and Modula-2-programs of three different sizes; one of approximately
- 750 tokens, one of appr. 5000 tokens and one of appr. 15000 tokens. We have
- chosen to represent the sizes of programs in the number of tokens instead of
- number of lines, because the number of tokens more realistically
- reflects the load the programs put on the error recovery mechanism. Also we give
- our time measurements in usertime instead of realtime, because realtime
- depends heavily on the load of the system, which usertime does not.
- Our space measurements are based on the size of the prediction graphs.
- Note that all files are entirely recognized by the non-correcting error
- recovery technique. We achieved this by putting a `1' at the beginning
- of each file; because then each file starts with a syntax error LLgen
- is forced to continue with the non-correcting error recovery.
- .NH 3
- Time and space measurements on the effect of the first-set optimization
- .LP
- In the diagram below we show our time measurements we got from recognizing
- the C-programs both with and without first-set optimization.
- .G1
- coord x 0, 17000 y 0, 65
- ticks bot out at 750, 5000, 15000
- label bot "Number of tokens"
- label left "User Time" "(sec)" left .3
- draw no_opt dashed
- draw first_opt dashed
- copy thru X
- times size +2 at $1, $2
- times size +2 at $1, $3
- next no_opt at $1, $2
- next first_opt at $1, $3
- X until "XXX"
- 742 2.5 .9
- 5010 16.3 5.8
- 14308 54.2 16.8
- XXX
- copy thru X "$1 $2" size -2 at 11000, $3 X until "XXX"
- No optimization 55
- First-set optimization 20
- XXX
- .G2
- .I
- .ce
- Time measurements of three C-programs with and without first-set optimization
- .R
- .LP
- Notice the considerable time savings we
- get when the first-set optimization is turned on; a factor of slightly more than
- 3. Obviously this is an extremely useful optimization. On the other hand
- we found there were no measurable time savings when using the follow-set
- optimization; for that reason we did not chart the result of this optimization.
- It seems that the time savings gained by the optimization are
- waisted again by the extra processing time needed. We conclude that
- this optimization is of little or no use when we want to save on time.
- .LP
- In the following picture the time measurements of three Modula-2 programs
- are given, again with and without first-set optimization.
- .G1
- coord x 0, 17000 y 0, 65
- ticks bot out at 750, 5000, 15000
- label bot "Number of tokens"
- label left "User Time" "(sec)" left .3
- draw no_opt dashed
- draw first_opt dashed
- copy thru X
- times size +2 at $1, $2
- times size +2 at $1, $3
- next no_opt at $1, $2
- next first_opt at $1, $3
- X until "XXX"
- 823 1.3 .6
- 4290 7.6 3.5
- 16530 30.5 14.3
- XXX
- copy thru X "$1 $2" size -2 at 13000, $3 X until "XXX"
- No optimization 30
- First-set optimization 15
- XXX
- .G2
- .I
- .ce
- Time measurements of three Modula-2-programs with and without first-set optimization
- .R
- .LP
- From this picture we can conclude mainly the same as above; considerable
- time savings when we use the first-set optimization;
- the factor is somewhat less, but still more than 2. Again we have omitted
- the results of the follow-set optimization, for the same reason as before.
- .LP
- There is however one remarkable difference between the two languages: parsing
- C-programs needs almost twice the time as parsing programs of comparable
- sizes written in Modula-2. This can be explained by the fact that the
- C-grammar is far more complicated than that of Modula-2, and also the
- production rules are longer in C, so building, deleting and definitely
- traversing the graph will consume more time.
- .LP
- Now we come to the space measurements of both C- and Modula-2 programs.
- In the picture below we present the maximum sizes of the prediction graphs,
- during the recognition of the three C-programs.
-
- .G1
- coord x 0, 17000 y 0, 18000
- ticks bot out at 750, 5000, 15000
- label bot "Number of tokens"
- label left "Maximum size of" "the prediction graph" "(bytes)"left .3
- draw no_opt dashed
- draw first_opt dashed
- copy thru X
- times size +2 at $1, $2
- times size +2 at $1, $3
- next no_opt at $1, $2
- next first_opt at $1, $3
- X until "XXX"
- 742 5568 10444
- 5010 7668 12664
- 14308 13636 17308
- XXX
- copy thru X "$1 $2" size -2 at 8000, $3 X until "XXX"
- No optimization 16000
- First-set optimization 7000
- XXX
- .G2
- .I
- .ce
- Maximum sizes of the prediction graphs when recognizing three C-programs
- .R
- .LP
- From this diagram we see that, although the prediction graphs
- are smaller when the first-set optimization is used, the space savings are
- not as spectacular as the time savings achieved by this optimization.
- .LP
- In Modula-2 the first-set optimization also causes a decrease in memory
- usage. The savings are less than in C, but still about 1.5 Kb. Again
- this can be explained by the fact that the rules of the Modula-2 grammar
- are shorter than that of C.
- .G1
- coord x 0, 17000 y 0, 12000
- ticks bot out at 750, 5000, 15000
- label bot "Number of tokens"
- label left "Maximum size of" "the prediction graph" "(bytes)" left .3
- draw no_opt dashed
- draw first_opt dashed
- copy thru X
- times size +2 at $1, $2
- times size +2 at $1, $3
- next no_opt at $1, $2
- next first_opt at $1, $3
- X until "XXX"
- 823 5056 3292
- 4290 6420 4664
- 16530 11388 9632
- XXX
- copy thru X "$1 $2" size -2 at 8000, $3 X until "XXX"
- No optimization 10000
- First-set optimization 4000
- XXX
- .G2
- .I
- .ce
- Maximum sizes of the prediction graphs when recognizing three Modula-2-programs
- .R
- .NH 3
- Input that is recognized in quadratic time
- .LP
- The measurements presented may suggest that the time required to
- recognize input depends linearly on the length of the input; however,
- this is not always the case. When there are recursive rules in the
- grammar, the time needed to recognize input that is produced by this
- rules can become proportional to the square of the input length.
- Consider this set of grammar rules:
- .br
- .nf
- S: '{' A '}'
- A: 'a' A | $epsilon$
- .fi
- .LP
- When the input is `{aaa....', the algorithm will produce the following
- prediction graphs:
- .PS
- up; B1: box "END" "S"; arrow <- ;box "}";arrow <- ;box "A";arrow <- ;box "{";
- move right from B1.se; move
- up; B2: box "END" "S"; arrow <-; box "}"; arrow <-; box "[A]";
- arrow <-; box "A"; arrow <-; box "a";
- move right from B2.se; move
- up; B3: box "END" "S"; arrow <-; box "}"; arrow <-; box "[A]";
- arrow <-; box "[A]"; arrow <-; box "A"; arrow <-; box "a";
- move right from B3.se;move
- up; B4: box "END" "S"; arrow <-; box "}"; arrow <-; box "[A]";
- arrow <-; box "[A]"; arrow <-; box "[A]"; arrow <- ; box "A"; arrow <-;box "a";
- .PE
- .LP
- In each prediction phase, a new [A] appears on the prediction graph. However,
- since A also produces empty, the prediction algorithm has to traverse all the
- elements [A] until it finds the element `}'. In the first prediction phase,
- there is one element [A], in the second there are two, etc, so in all
- 1 + 2 + 3 + ... + k = $k(k+1) over 2$ elements have to be traversed if
- there are k prediction phases, making this proportional to the square
- of the input length. We constructed a parser with this simple input grammar
- and measured the processing time the error recovery mechanism used.
- In the following diagram the dashed line shows the processing time needed;
- the dotted line is the curve $t = 13 n sup 2$. Clearly the processing time
- is proportional to the square of the number of tokens.
- .G1
- coord x 0, 2100 y 0, 60
- ticks bot out at 500, 1000, 1500, 2000
- label bot "Number of tokens"
- label left "User Time" "(sec)" left .3
- draw quad dashed
- copy thru X
- times size +2 at $1, $2
- next quad at $1, $2
- X until "XXX"
- 500 3.0
- 1000 12.4
- 1500 28.6
- 2000 51.4
- XXX
- draw dotted
- for i from 0 to 2100 by 25 do { next at i, 0.000013 * i * i }
- .G2
- .LP
- In the grammar used for the C compiler, array initializations are handled by a recursive
- rule, so we would expect that the error recovery mechanism needs quadratic
- processing time to recognize such an initialization; we made measurements on
- the processing time and indeed, the
- processing time needed grows proportionally to the square of the size of the input, as the
- next figure shows. Here, the processing times are about half of those in
- the previous example; this is so because the recursion appears after two
- tokens are recognized. Note that the algorithm only takes quadratic time
- when it is recognizing input that is generated by a recursive grammar rule.
- Other input is still recognized in linear time, regardless of the fact that
- there are recursive grammar rules.
- .G1
- coord x 0, 5000 y 0, 85
- ticks bot out at 1150, 2400, 3600, 4800
- label bot "Number of tokens"
- label left "User Time" "(sec)" left .3
- draw quad dashed
- copy thru X
- times size +2 at $1, $2
- next quad at $1, $2
- X until "XXX"
- 1150 5.1
- 2400 20.3
- 3600 43.7
- 4800 78.6
- XXX
- .G2
- .LP
- Unfortunately, there is no easy way to speed up the recognition of these
- recursively defined language elements; they are caused by the substituted
- tokens that are left in the prediction graph, and we cannot just delete those
- `dummies' from the graph during a prediction phase because the `join' part of the
- prediction algorithm depends on them. One could traverse the graph after
- a prediction phase to delete the dummies, but then the processing
- time needed to recognize non-recursively defined language elements would
- increase dramatically. However, we feel that in practice things
- like large array initializations will not occur in hand-made programs; when
- they occur, it is probably in computer-generated programs, which normally
- will be correct anyway, meaning that the error recovery never sees them.
- When testing such generated programs, one is likely
- to use small test-cases, which are handled well by the error recovery.
- .NH 3
- Time measurements on the effect of leaving out the loop-deletion algorithm
- .LP
- We now show what effect the loop-deletion algorithm has on processing time.
- To put it another way: how much time can be saved when we turn off the
- loop-deletion algorithm. In the diagram below we give the measurements of
- the three C-programs; note that we do use the first-set optimization.
- .G1
- coord x 0, 17000 y 0, 22
- ticks bot out at 750, 5000, 15000
- label bot "Number of tokens"
- label left "User Time" "(sec)" left .3
- draw no_loop dashed
- draw loop dashed
- copy thru X
- times size +2 at $1, $2
- times size +2 at $1, $3
- next no_loop at $1, $2
- next loop at $1, $3
- X until "XXX"
- 742 .9 .4
- 5010 5.8 6.8
- 14308 16.8 20.5
- XXX
- copy thru X "$1 $2" size -2 at 11300, $3 X until "XXX"
- With loop-deletion 20
- Without loop-deletion 9
- XXX
- .G2
- .I
- .ce
- Time measurements on processing three C-programs with and without the loop-deletion algorithm
- .R
- The diagram shows that the loop-deletion algorithm
- does not dramatically slow down the recognizing process. There is, however,
- a measurable time loss of \(+-25%. As we will see later, the loop-deletion
- algorithm will turn out to be extremely useful in efficient use of memory
- when there are many loops in the graph.
- The effect of the loop-detecion algorithm on parsing Modula-2 programs
- is even less than with C-programs; in fact there is no measurable
- time loss:
- .G1
- coord x 0, 17000 y 0, 15
- ticks bot out at 750, 5000, 15000
- label bot "Number of tokens"
- label left "User Time" "(sec)" left .3
- draw no_loop dashed
- draw loop dashed
- copy thru X
- times size +2 at $1, $2
- times size +2 at $1, $3
- next no_loop at $1, $2
- next loop at $1, $3
- X until "XXX"
- 823 .6 .6
- 4290 3.5 3.8
- 16530 14.3 14.3
- XXX
- copy thru X "$1 $2" size -2 at 11800, $3 X until "XXX"
- With loop-deletion 13
- Without loop-deletion 7
- XXX
- .G2
- .I
- .ce
- Time measurements on processing three Modula-2-programs with and without a loop-deletion algorithm
- .R
- There are at least two reasons for this; both result from the relative
- simplicity of the Modula-2 grammar. The distance from a head to an
- end of stack marker is shorter than in C, and secondly Modula-2
- causes fewer joins to occur than C, meaning that the loop marking algorithm
- is run less often and when it is run it has fewer paths to search.
- .NH 3
- Space measurements on the effect of leaving out the loop-deletion algorithm
- .LP
- Clearly, to make any measurements on the space-usage effects of leaving out
- the loop-deletion algorithm we need a program that causes the prediction
- graph to contain loops; however, we have not been able to devise a C
- or Modula-2 program that does this. In order to be able to make measurements,
- we added an extra alternative to a rule of the C compiler grammar, making
- it directly left-recursive. To make LLgen accept this new grammar, we
- put a `%if' directive in the rule.
- .LP
- We have input our standard C test program consisting of 800 tokens to
- the error recovery routine for this `doctored' C compiler,
- and compared the storage needed for the prediction graphs with the
- loop deletion algorithm enabled with the storage needed when the
- algorithm is disabled. With the loop-deletion algorithm enabled, the
- maximum size of the prediction graph was 5576 bytes. When the loop
- algorithm was disabled, the maximum size of the prediction graph
- grew to 12676 bytes; furthermore, 12676 bytes of heap were allocated
- for the prediction graph, but not deallocated again, because they were
- in use by graph elements that were in inaccessible loops. The user-time
- the program needed decreased only slightly, from 0.9 to 1.0 seconds. Given the
- relatively small input program, this data suggests that when loops
- are actually being made, the loop deletion algorithm is definitely
- worth the extra overhead it costs, considering the space
- that would otherwise be occupied by inaccessible loops. To verify this,
- we input the C program consisting of 15000 tokens to the compiler;
- execution time increased from 17.3 to 21.1 seconds after enabling
- the loop deletion algorithm, while the maximum size of the prediction graph
- shrunk from 328664 to 13664 bytes. With the loop-deletion algorithm
- disabled, 326720 bytes allocated for the graph were not deallocated again.
- Again, given the relatively small increase in execution time and the
- large reduction of memory usage, we feel that the loop-deletion
- algorithm is useful enough to justify the overhead it creates.
- .NH 2
- Problems encountered
- .LP
- In this section we describe some of the problems we encountered
- while testing the non-correcting error recovery.
- .NH 3
- The LLgen error reporting mechanism.
- .LP
- The parsers generated by LLgen call a user-supplied error reporting
- routine, usually called LLmessage. This routine is called with an
- integer parameter that is positive, zero or negative. When the parameter
- is positive the parser has just inserted a token, whose
- number is equal to the parameter; if it is zero, the parser
- has deleted a token whose number is in a global variable called LLsymb; if
- it is negative, it means that LLgen expected end-of-file, but did not
- find it. The routine LLmessage is supposed to print an error message,
- and when a token is inserted, it should set all necessary attributes.
- .LP
- However, when non-correcting error recovery is used, the situation becomes slightly
- different; when the parser inserts a token, it is only to keep the
- semantic actions consistent, and does no longer signify an error.
- However, the LLmessage routine still has to be called because the
- attributes of the inserted token need to be set. Therefore, when
- non-correcting error recovery is used, the LLmessage routine should not
- print an error message when the parameter is positive, or else it will
- print highly confusing error messages indeed. Furthermore, the
- LLmessage routine will usually print a message like `token ... deleted' when
- it is called with parameter equal to zero; however, when the non-correcting
- error recovery is used, it is more appropriate to report something
- like `token ... illegal', as the non-correcting error recovery does
- not delete tokens. Finally, when an unexpected end-of-file is encountered,
- LLgen normally just inserts the missing tokens and calls
- LLmessage with the parameter equal to the token number;
- when non-correcting error recovery is used we need a way to
- actually report we have encountered an unexpected end-of-file. The
- way we achieved this is by calling LLgen with parameter 0 and the
- global variable LLsymb set to EOFILE when this situation occurs; the
- routine LLmessage should print something like `unexpected end of file'
- when it is called with parameter 0 and LLsymb is EOFILE. To facilitate
- switching between correcting and non-correcting error recovery, the
- file Lpars.h contains a statement `#define LLNONCORR' if non-correcting
- error recovery is used.
- .NH 3
- Parsers being started in semantic actions
- .LP
- LLgen allows the programmer to define more than one nonterminal as the
- start symbol of the input grammar; it will generate a parsing routine
- for each of the start symbols. However, the error recovery code
- is generated only once; it is shared by all parsers.
- The programmer is free to call any
- of the generated parsers whenever he wants; for instance, in the C-compiler
- a separate parser for expressions in #if and #elsif statements is used. Whenever
- the lexical analyzer encounters such a statement, it calls the expression
- parser. It is also possible to call a parser in a semantic action of
- another parser; in the MODULA-2 compiler a separate parser for
- definition modules is used. When the main parser encounters a
- FROM defmod IMPORT statement a semantic
- actions opens the definition module defmod and starts the parser for
- definition modules.
- .LP
- The fact that subparsers can be started just about anywhere causes
- problems when non-correcting error recovery is used.
- Suppose a parser calls another parser in a semantic action
- to parse a separate input file. In the Modula-2 compiler, after
- seeing the FROM defmod IMPORT statement a semantic action opens
- defmod and parses it; now, if a syntax error occurred before the
- FROM IMPORT statement, the non-correcting error recovery will not
- execute the action that opens and parses the definition module, but
- it will not report an error either, because the statement
- FROM defmod IMPORT is part of the input language of the main parser.
- However, suppose that during the parsing of a definition module
- an error occurs; then, some semantic actions that would normally
- be executed during parsing of the definition module will not have
- taken place. When normal parsing is now resumed by the main parser,
- after the non-correcting error recovery has finished with the
- definition module, a lot of spurious semantic errors are likely to be
- reported, because the semantic actions that would normally have been
- executed during the definition module parsing have not been executed
- by the error recovery. Therefore, it is desirable that the main parser
- does not resume normal parsing, but instead continues with the non-correcting
- error recovery as well. Any syntactic errors in the main program will
- still be reported, but no spurious semantic errors will be reported
- that way.
- .LP
- When the lexical analyzer calls other parsers, as is the case in
- the ACK C compiler, recursive invocations of the non-correcting error
- recovery routine can occur. This will happen if a parser starts the
- error recovery, the error recovery calls the lexical analyzer, which
- starts another parser that finds a syntax error and calls the
- error recovery again. This is not really a problem, but is has
- consequences for the implementation of the error recovery routine.
- .LP
- The worst case
- occurs when two parsers are involved in parsing one input file, and
- the secondary parser (e.g. an inline assembly parser) is called in a semantic
- action of the main parser. Suppose now that the input text contains
- a syntax error; after detecting this error, the parser starts the
- non-correcting error recovery. This recovery does not execute any
- semantic actions; therefore it will not start the subparser at those points
- where the original LLgen generated parser would. As a result, parts
- of the program that would be accepted by the subparser will now probably
- be rejected as illegal, because the error recovery does not know it
- should use another grammar to check these parts. This is a serious
- problem, and we have devised and implemented two ways to solve it.
- .LP
- The first solution is based on the assumption that whenever a semantic
- action occurs in the grammar, another parser can be started at that
- point. Obviously, we have no way of knowing which semantic actions start
- a parser and which don't, so we assume the worst.
- Now, assume that in the grammar there are k symbols defined as
- start symbols, say $W sub 1 , W sub 2 , ..., W sub k$. Each of these symbols
- will cause LLgen to generate a parser that can be called in any
- of the semantic actions of the grammar. We now introduce a new
- symbol $X$, and a new grammar rule $X -> W sub 1 X | W sub 2 X | ... |
- W sub k X |
- epsilon$.
- In the grammar the error recovery algorithm uses, we insert this symbol
- X at all positions where there are semantic actions in the original grammar,
- so a rule $A -> alpha$ { action } $beta$ becomes $A -> alpha X beta$. As a
- result, at each position in a grammar rule where a semantic action
- occurs, we now accept any input that would be accepted by any of the
- parsers. Clearly, this solution is somewhat of a kludge, as it will
- accept a lot of input that is not accepted by the original parser.
- However, it is guaranteed to never give spurious error messages, because
- whenever a parser would be started by the original parser, there now
- is an $X$ in the grammar that produces all the strings that would be
- accepted by that parser. We have implemented this solution, and found
- it to be extremely slow, which of course was to be expected given the
- number of semantic actions in the average grammar. Furthermore,
- because each time a semantic action occurs in the grammar
- a string accepted by any of the generated parsers is accepted, including
- strings recognized by the currently running parser, error messages
- become hard to interpret. As an example, consider the following
- C program:
- .br
- .nf
- main()
- {
- int i, j;
- while (i < j
- j++;
- i = 1;
- j = 2;
- }
- .fi
- .LP
- Clearly, there is a `)' missing in the while-statement;
- however, if this program is input to the error recovery it will complain
- "} illegal", since after recognizing the
- expression controlling the while the original parser starts a
- semantic action, so the non-correcting recovery will accept a valid
- C program at that point; after recognizing the three statements
- following the while-statement as a separate program the
- recognizer expects the missing `)', but gets `}' instead.
- .LP
- Our second solution is based on the observation that if we knew
- which semantic actions can start other parsers, we would only
- have to introduce the new symbol $X$ at those places where parsers
- can get started. We have therefore extended LLgen with a new directive
- %substart, which is used to indicate to the parser generator that
- another parser may be started. The %substart is followed by the
- startsymbols that will produce the parsers that can be called,
- so %substart A, B, C; indicates that in the semantic action
- following the directive the parsers produced by startsymbols
- A, B, en C can be started. In the grammar used by the error
- recovery, a new symbol $X$ will be introduced at this point,
- along with a new rule $X -> AX | BX | CX | epsilon$. Of course, this
- solution can still accept input that would not have been accepted
- by original parser, for instance if a parser is started
- conditionally, based on other semantic information. However, it
- is a big improvement over the first solution, both in performance
- and the input it accepts.
- .NH 3
- Syntactic errors being handled in semantic actions
- .LP
- A programmer may decide to handle certain syntactic errors
- in semantic actions, for instance because he is not satisfied with
- the standard error recovery. However, since the non-correcting error
- recovery does not execute semantic actions, this may cause errors
- to remain undetected. We encountered the following example in the ACK
- Modula-2 compiler, in the grammar rule for assignment statement:
- .br
- .nf
- Assignment_statement: lvalue
- [
- '='
- {
- error(":= expected");
- }
- |
- ':='
- ]
- expression
- ;
- .fi
- .LP
- This works well in the original LLgen; however, statements like
- `j=9' are not treated as syntactic, but as semantic errors.
- The original LLgen generated parser
- will print the (semantic) error message, but the non-correcting recovery
- will not execute the semantic action and therefore the erroneous
- input will be accepted.
- .LP
- To facilitate the incorporation of non-correcting error recovery in parsers
- that use this kind of `trick', we extended LLgen with the %erroneous
- directive. The directive indicates to the non-correcting recovery
- mechanism that the token following it is not really part of the grammar.
- When recognizing input, the error recovery will ignore tokens in the
- grammar that have %erroneous in front of them. If in the example above,
- the '=' is replaced with %erroneous '=', the non-correcting mechanism will
- report an error when it sees a statement like 'j = 9'. See appendix B
- for details about the implementation of the %erroneous directive.
- .LP
- Another example is in the ACK C compiler. For some reason, the
- grammar accepts function definitions without `()', so according
- to the syntax a function definition can look like:
- .br
- .nf
- int func
- {
- ....
- }
- .fi
- .LP
- The absence of the `()', however, causes `func' to be entered in the
- symbol table as non-function, and when the parser encounters the body
- a semantic action will complain with the error message "Making function body
- for non-function". This again will cause the non-correcting error
- recovery to miss errors. Consider this piece of code:
- .br
- .nf
- int i int j = 1;
- {}
- .fi
- .LP
- where apparently there's a `;' missing between the declarations
- of i and j. The original LLgen-generated parser only gives semantic errors:
- .br
- .nf
- "Making function body for non-function"
- "j is not in parameter list"
- "Illegal initialization of formal parameter, ignored"
- .fi
- .LP
- As a result, the non-correcting error recovery will not report
- any errors in this piece of code, because it does not execute the
- semantic actions that recognize and report the error. Unfortunately,
- due to the way the C-grammar is written, it is not possible to solve
- this problem using a %erroneous directive; the part of the grammar
- that deals with declaratons would have to be rewritten so as to
- syntactically reject functions without `()'.
- .NH 3
- Semantic actions that read input
- .LP
- There are no restrictions on what a semantic action can do;
- there is nothing to stop the programmer from writing a parser in such
- a way that some of the input to the parser is processed by semantic
- actions. Obviously, because the non-correcting error recovery does not
- execute semantic actions, this kind of parser will not work at all
- with the new error recovery. Ironically, LLgen itself is written in
- such a fashion; {}-enclosed C-code in its input is processed by
- a semantic action in the LLgen grammar. We feel that it is bad
- practice to write parsers this way; the `eating' of parts of
- the input should be done in the lexical analyzer, not in the parser.
- After all, in the case of LLgen, one can regard a semantic action
- in the input as one token, and thus it should be handled by
- the lexical analyzer as such.
- .NH 2
- Examples of error recovery
- .LP
- We will now give some examples that compare non-correcting error
- recovery with the correcting error recovery used by parsers generated
- by `standard' LLgen.
- Consider the next C program, where there is a `)' missing in the
- header of function `test'.
- .br
- .nf
- 1 int test(a,b
- 2
- 3 int a,b;
- 4
- 5 {
- 6 if (a < b)
- 7 return(1);
- 8 else
- 9 return(0);
- 10 }
- .fi
- .LP
- This small error derails the `standard' parser; it produces the
- following error messages, where we have left out 7 messages reporting
- semantic errors:
- .br
- .nf
- line 3: , missing before type_identifier
- line 3: , missing before identifier
- line 3: ) missing before ;
- line 5: { deleted
- line 6: if deleted
- line 6: < deleted
- line 6: ) missing before identifier
- line 6: ) deleted
- line 7: identifier missing before return
- line 7: ; missing before return
- line 7: { missing before return
- line 8: else deleted
- .fi
- .LP
- In contrast, the parser using non-correcting error recovery produces
- only one error message:
- .br
- line 3: type_identifier illegal
- This error message correctly pin-points the error: there should
- have been a `)' at the position where type-identifier `int' is.
- .LP
- Now, an example with Modula-2; consider this program:
- .br
- .nf
- 1 MODULE test;
- 2
- 3 TYPES
- 4 ElementRecordType = RECORD
- 5 Element: ElementType;
- 6 Next,
- 7 Prior: ElementPointerType;
- 8 END;
- 9
- 10 VARS a,b,c: ElementRecordType;
- 11
- 12
- 13 BEGIN
- 14
- 15 a := b;
- 16
- 17 END test.
- .fi
- .LP
- There are two syntactic errors in this program; on line 3, TYPES should be TYPE, and
- on line 10, VARS should be VAR. We have left out the type declarations of
- ElementType and ElementPointerType; clearly this will generate semantic
- errors, but we are only interested in syntactic errors anyway.
- The correcting error recovery parser
- again derails on this program; it produces the following syntactic error messages:
- .br
- .nf
- line 3: CONST missing before identifier
- line 4: '=' missing before identifier
- line 4: RECORD deleted
- line 5: ':' deleted
- line 5: ';' missing before identifier
- line 5: '=' missing before ';'
- line 5: number missing before ';'
- line 6: ',' deleted
- line 7: '=' missing before identifier
- line 7: ':' deleted
- line 7: ';' missing before identifier
- line 7: '=' missing before ';'
- line 7: number missing before ';'
- line 8: ';' deleted
- line 10: identifier deleted
- line 10: ',' deleted
- line 10: identifier deleted
- line 10: ',' deleted
- line 10: identifier deleted
- line 10: ':' deleted
- line 10: identifier deleted
- line 10: ';' deleted
- line 13: BEGIN deleted
- line 15: identifier deleted
- line 15: := deleted
- line 15: identifier deleted
- line 15: ';' deleted
- line 17: END deleted
- line 17: identifier deleted
- .fi
- .LP
- The error correction mechanism clearly makes the wrong guess by inserting
- CONST on line 3; as a result, all that follows is rejected as incorrect.
- In contrast, the non-correcting error recovery mechanism only produces
- two error messages:
- .br
- .nf
- line 3: identifier illegal
- line 10: identifier illegal
- .fi
- .LP
- This again exactly pin-points the errors: the identifiers TYPES and
- VARS constitute the only errors in the program. Note that the
- presence of more than one error does not cause any problems to the
- non-correcting recovery mechanism.
- .bp
- .nr PS 12
- .nr VS 14
- .NH
- Conclusion
- .nr PS 10
- .nr VS 12
- .LP
- After implementing and testing a non-correcting error recovery mechanism
- we have come to the conclusion that it indeed is superior to correcting
- mechanisms in what regards the error messages it produces;
- the examples we have given clearly show this. However, there is a
- clear loss of performance when errors are present in a program,
- although we have found this performance
- degradation to be acceptable. We feel that the benefits of
- better error messages outweigh the loss of performance. In any case,
- correct programs do not suffer at all from the incorporation
- of a non-correcting recovery mechanism.
- The error recovery mechanism we implemented does not make
- unreasonable demands on resources; the size of the prediction
- graphs stays within reasonable limits.
- .LP
- The main problems we encountered had to do with recognizing
- `languages within languages', and semantic actions that did
- unreasonable things like eating input. The more `well-behaved' a
- parser is, the better the results the non-correcting error recovery
- mechanism gives. This is also true for the input grammars: with a
- language like Modula-2, whose syntax has been designed with parser
- generators in mind, the performance of the non-correcting mechanism
- is better than with C, whose syntax is extremely hard, if not
- impossible to describe with a LL(1) grammar.
- .bp
- .nr PS 12
- .nr VS 14
- .NH
- Bibliography
- .nr PS 10
- .nr VS 12
- .IP [CORMACK] 12
- Gordon V. Cormack, `An LR substring parser for noncorrecting syntax error
- recovery', ACM SIGPLAN Notices, vol. 24, no. 7, p. 161-169, July 1989
- .IP [GRUNE] 12
- Dick Grune, Ceriel J.H. Jacobs, `A programmer friendly LL(1) parser
- generator', Softw. Pract. Exper., vol. 18, no. 1, p. 29-38, Jan 1988
- .IP [RICHTER] 12
- Helmut Richter, `Noncorrecting syntax error recovery', ACM Trans. Prog. Lang.
- Sys., vol.7, no.3, p. 478-489, July 1985
- .IP [ROEHRICH] 12
- Johannes R\*:ohrich, `Methods for the automatic construction of error
- correcting parsers', Acta Inform., vol. 13, no. 2, p. 115-139, Feb 1980
- .IP [TOMITA] 12
- Masaru Tomita, Efficient parsing for natural language, Kluwer Academic
- Publishers, Boston, p.210, 1986
- .bp
- .SH
- Appendix A: Implementation Issues
- .nr PS 10
- .nr VS 12
- .RS
- .LP
- In this appendix we will describe some implementation issues;
- the data structure used to store the grammar during non-correcting
- error recovery, postponing deletions of graph elements until after
- the prediction phase, and the implementation of the %substart directive .
- .RE
- .SH
- A.1 The grammar data structure
- .LP
- The grammar data structure used by the non-correcting error recovery technique has
- to meet two conditions: easy access to a rule as a whole to make
- substituting nonterminals efficient and easy access to each symbol in the RHS
- of a rule to make starting error recovery and finding continuations
- efficient. To fulfill these conditions we decided to construct the
- storage of the grammar as follows.
- .LP
- A rule in the grammar is divided in two
- parts: a LHS and a RHS. The LHS is represented by a struct `lhs' and
- for each symbol in the RHS a struct 'symbol' is constructed.
- A struct `lhs' contains the number of the
- nonterminal forming the LHS of the rule, a pointer to the RHS, the
- first- and follow-sets of the nonterminal and a flag 'empty' which
- indicates whether the nonterminal produces empty or not. A struct
- `symbol' contains a field indicating the type of the symbol, i.e.
- a terminal or a nonterminal, the number of the symbol, a `link' pointer
- to a struct `symbol' that represents the same symbol, a `next' pointer
- to the rest of the RHS and a pointer back to the LHS.
- .LP
- A special struct `symbol' is added to the end of the RHS to indicate
- the end of a rule. The type of this struct is LLEORULE, the number
- is set to -1 and the pointers 'link' and `next' are nil.
- .LP
- In case that there is more than one RHS for a LHS, all the RHS's
- are put after each other and separated by another special struct
- `symbol'. The type of this struct is LLALT, the number is set to
- -1 and the 'link' pointer is nil. After the last RHS a `LLEORULE'-struct
- marker is added.
- .LP
- Finally, to make searching efficient there are two arrays: `terminals'
- and `nonterminals'. `terminals' is indexed by the number of a terminal
- and contains for each terminal a struct containing a 'link' pointer
- to a symbol, representing this terminal, in the RHS of a rule. Because
- this symbol has again a 'link' pointer to another symbol representing
- the terminal, it is possible by following this chain of pointers
- to find all rules containing such a terminal. In a similar way `nonterminals'
- is indexed by the number of a nonterminal and contains for each
- nonterminal a struct. This struct not only contains a 'link' pointer
- linking all rules with this nonterminal, but also contains a 'rule'
- pointer. This pointer points to the RHS or RHS's of the rules of which
- the nonterminal forms the LHS.
-
- .LP
- As an example, consider the following grammar:
- .br
- A: a B
- .br
- B: a | $epsilon$
- .br
- This will result in the picture below. Note that `pointer' fields
- without an arrow indicate nil pointers.
- .PS
- dx = 0.05
- down
- A_a: box ht boxht/2 "link"
- box invis "a" ljust with .e at A_a.w
- move to A_a.s
- move
- move
- A: box "link" "rule"
- B: box "link" "rule"
- line dashed from A.w to A.e
- line dashed from B.w to B.e
- box invis "A" ljust with .e at A.w
- box invis "B" ljust with .e at B.w
- move to A.ne
- right
- move
- move
- down
- LHS_A: box wid 1.2 * boxwid ht 2.5 * boxht "`A'" "rhs" "first" "follow" "empty 0"
- line dashed from 0.2 <LHS_A.nw, LHS_A.sw> to 0.2 <LHS_A.ne, LHS_A.se>
- line dashed from 0.4 <LHS_A.nw, LHS_A.sw> to 0.4 <LHS_A.ne, LHS_A.se>
- line dashed from 0.6 <LHS_A.nw, LHS_A.sw> to 0.6 <LHS_A.ne, LHS_A.se>
- line dashed from 0.8 <LHS_A.nw, LHS_A.sw> to 0.8 <LHS_A.ne, LHS_A.se>
- move to LHS_A.ne + (1,0)
- RHS_a1: box wid 2.0 * boxwid ht 2.5 * boxht "LLTERM" "`a'" "link" "next" "lhs"
- line dashed from 0.2 <RHS_a1.nw, RHS_a1.sw> to 0.2 <RHS_a1.ne, RHS_a1.se>
- line dashed from 0.4 <RHS_a1.nw, RHS_a1.sw> to 0.4 <RHS_a1.ne, RHS_a1.se>
- line dashed from 0.6 <RHS_a1.nw, RHS_a1.sw> to 0.6 <RHS_a1.ne, RHS_a1.se>
- line dashed from 0.8 <RHS_a1.nw, RHS_a1.sw> to 0.8 <RHS_a1.ne, RHS_a1.se>
- move to RHS_a1.ne + (1,0)
- RHS_B: box wid 2.0 * boxwid ht 2.5 * boxht "LLNONTERM" "`B'" "link" "next" "lhs"
- line dashed from 0.2 <RHS_B.nw, RHS_B.sw> to 0.2 <RHS_B.ne, RHS_B.se>
- line dashed from 0.4 <RHS_B.nw, RHS_B.sw> to 0.4 <RHS_B.ne, RHS_B.se>
- line dashed from 0.6 <RHS_B.nw, RHS_B.sw> to 0.6 <RHS_B.ne, RHS_B.se>
- line dashed from 0.8 <RHS_B.nw, RHS_B.sw> to 0.8 <RHS_B.ne, RHS_B.se>
- move to RHS_B.ne + (1,0)
- RHS_END1: box wid 2.0 * boxwid ht 2.5 *boxht "LLEORULE" "-1" "link" "next" "lhs"
- line dashed from 0.2 <RHS_END1.nw, RHS_END1.sw> to 0.2 <RHS_END1.ne,RHS_END1.se>
- line dashed from 0.4 <RHS_END1.nw, RHS_END1.sw> to 0.4 <RHS_END1.ne,RHS_END1.se>
- line dashed from 0.6 <RHS_END1.nw, RHS_END1.sw> to 0.6 <RHS_END1.ne,RHS_END1.se>
- line dashed from 0.8 <RHS_END1.nw, RHS_END1.sw> to 0.8 <RHS_END1.ne,RHS_END1.se>
- move to LHS_A.s - (0,1)
- LHS_B: box wid 1.2 * boxwid ht 2.5 * boxht "`B'" "rhs" "first" "follow" "empty 1"
- line dashed from 0.2 <LHS_B.nw, LHS_B.sw> to 0.2 <LHS_B.ne, LHS_B.se>
- line dashed from 0.4 <LHS_B.nw, LHS_B.sw> to 0.4 <LHS_B.ne, LHS_B.se>
- line dashed from 0.6 <LHS_B.nw, LHS_B.sw> to 0.6 <LHS_B.ne, LHS_B.se>
- line dashed from 0.8 <LHS_B.nw, LHS_B.sw> to 0.8 <LHS_B.ne, LHS_B.se>
- move to LHS_B.ne + (1,0)
- RHS_a2: box wid 2.0 * boxwid ht 2.5 * boxht "LLTERM" "`a'" "link" "next" "lhs"
- line dashed from 0.2 <RHS_a2.nw, RHS_a2.sw> to 0.2 <RHS_a2.ne, RHS_a2.se>
- line dashed from 0.4 <RHS_a2.nw, RHS_a2.sw> to 0.4 <RHS_a2.ne, RHS_a2.se>
- line dashed from 0.6 <RHS_a2.nw, RHS_a2.sw> to 0.6 <RHS_a2.ne, RHS_a2.se>
- line dashed from 0.8 <RHS_a2.nw, RHS_a2.sw> to 0.8 <RHS_a2.ne, RHS_a2.se>
- move to RHS_a2.ne + (1,0)
- RHS_ALT: box wid 2.0 * boxwid ht 2.5 * boxht "LLALT" "-1" "link" "next" "lhs"
- line dashed from 0.2 <RHS_ALT.nw, RHS_ALT.sw> to 0.2 <RHS_ALT.ne, RHS_ALT.se>
- line dashed from 0.4 <RHS_ALT.nw, RHS_ALT.sw> to 0.4 <RHS_ALT.ne, RHS_ALT.se>
- line dashed from 0.6 <RHS_ALT.nw, RHS_ALT.sw> to 0.6 <RHS_ALT.ne, RHS_ALT.se>
- line dashed from 0.8 <RHS_ALT.nw, RHS_ALT.sw> to 0.8 <RHS_ALT.ne, RHS_ALT.se>
- move to RHS_ALT.ne + (1,0)
- RHS_END2: box wid 2.0 * boxwid ht 2.5 *boxht "LLEORULE" "-1" "link" "next" "lhs"
- line dashed from 0.2 <RHS_END2.nw, RHS_END2.sw> to 0.2 <RHS_END2.ne,RHS_END2.se>
- line dashed from 0.4 <RHS_END2.nw, RHS_END2.sw> to 0.4 <RHS_END2.ne,RHS_END2.se>
- line dashed from 0.6 <RHS_END2.nw, RHS_END2.sw> to 0.6 <RHS_END2.ne,RHS_END2.se>
- line dashed from 0.8 <RHS_END2.nw, RHS_END2.sw> to 0.8 <RHS_END2.ne,RHS_END2.se>
- # Next pointers upper row
- .ps 30
- circle radius .01 at 0.75 <A.ne, A.se> - (dx, 0)
- circle radius .01 at 0.3 <LHS_A.ne, LHS_A.se> - (dx, 0)
- circle radius .01 at 0.7 <RHS_a1.ne, RHS_a1.se> - (dx, 0)
- circle radius .01 at 0.7 <RHS_B.ne, RHS_B.se> - (dx, 0)
- .ps 10
- arrow from 0.75 <A.ne, A.se> - (dx, 0) to 0.3 <LHS_A.nw, LHS_A.sw>
- arrow from 0.3 <LHS_A.ne, LHS_A.se> - (dx, 0) to 0.3 <RHS_a1.nw,RHS_a1.sw>
- arrow from 0.7 <RHS_a1.ne, RHS_a1.se> - (dx, 0) to 0.7 <RHS_B.nw,RHS_B.sw>
- arrow from 0.7 <RHS_B.ne, RHS_B.se> - (dx, 0) to 0.7 <RHS_END1.nw, RHS_END1.sw>
- # Next pointers lower row
- .ps 30
- circle radius .01 at 0.75 <B.ne, B.se> - (dx, 0)
- circle radius .01 at 0.3 <LHS_B.ne, LHS_B.se> - (dx, 0)
- circle radius .01 at 0.7 <RHS_a2.ne, RHS_a2.se> - (dx, 0)
- circle radius .01 at 0.7 <RHS_ALT.ne, RHS_ALT.se> - (dx, 0)
- .ps 10
- arrow from 0.75 <B.ne, B.se> - (dx, 0) to 0.3 <LHS_B.nw, LHS_B.sw>
- arrow from 0.3 <LHS_B.ne, LHS_B.se> - (dx, 0) to 0.3 <RHS_a2.nw,RHS_a2.sw>
- arrow from 0.7 <RHS_a2.ne, RHS_a2.se> - (dx, 0) to 0.7 <RHS_ALT.nw,RHS_ALT.sw>
- arrow from 0.7 <RHS_ALT.ne, RHS_ALT.se> - (dx, 0) to 0.7 <RHS_END2.nw, RHS_END2.sw>
- # Link pointers
- .ps 30
- circle radius .01 at 0.5 <RHS_a1.ne, RHS_a1.se> - (2*dx, 0)
- circle radius .01 at 0.5 <A_a.ne, A_a.se> - (dx, 0)
- circle radius .01 at 0.25 <B.ne, B.se> - (dx, 0)
- .ps 10
- arrow dashed from 0.5 <RHS_a1.ne, RHS_a1.se> - (2*dx, 0) to RHS_a2.ne - (2*dx,0)
- line dashed from 0.5 <A_a.ne, A_a.se> - (dx, 0) right 4.0 * boxwid then to RHS_a1.ne - (2*dx, 0) ->
- line dashed from 0.25 <B.ne, B.se> - (dx, 0) right then up .75 then right 7.0 * boxwid then to RHS_B.ne - (2*dx, 0) ->
- # LHS pointers upper row
- .ps 30
- circle radius .01 at 0.9 <RHS_a1.ne, RHS_a1.se> - (3*dx, 0)
- circle radius .01 at 0.9 <RHS_B.ne, RHS_B.se> - (3*dx, 0)
- circle radius .01 at 0.9 <RHS_END1.ne, RHS_END1.se> - (3*dx, 0)
- .ps 10
- line from 0.9 <RHS_a1.ne, RHS_a1.se> - (3*dx, 0) down ->
- line from 0.9 <RHS_B.ne, RHS_B.se> - (3*dx, 0) down ->
- line from 0.9 <RHS_END1.ne, RHS_END1.se> - (3*dx, 0) down then left 8.0 * boxwid then to LHS_A.se ->
- # LHS pointers lower row
- .ps 30
- circle radius .01 at 0.9 <RHS_a2.ne, RHS_a2.se> - (3*dx, 0)
- circle radius .01 at 0.9 <RHS_ALT.ne, RHS_ALT.se> - (3*dx, 0)
- circle radius .01 at 0.9 <RHS_END2.ne, RHS_END2.se> - (3*dx, 0)
- .ps 10
- line from 0.9 <RHS_a2.ne, RHS_a2.se> - (3*dx, 0) down ->
- line from 0.9 <RHS_ALT.ne, RHS_ALT.se> - (3*dx, 0) down ->
- line from 0.9 <RHS_END2.ne, RHS_END2.se> - (3*dx, 0) down then left 8.0 * boxwid then to LHS_B.se ->
- # Text above structs
- box invis ht boxht/2 "terminals" with .s at A_a.n
- box invis ht boxht/2 "nonterminals" with .s at A.n
- box invis ht boxht/2 "lhs" with .s at LHS_A.n
- box invis ht boxht/2 "lhs" with .s at LHS_B.n
- box invis ht boxht/2 "symbol" with .s at RHS_a1.n
- box invis ht boxht/2 "symbol" with .s at RHS_B.n
- box invis ht boxht/2 "symbol" with .s at RHS_END1.n
- box invis ht boxht/2 "symbol" with .s at RHS_a2.n
- box invis ht boxht/2 "symbol" with .s at RHS_ALT.n
- box invis ht boxht/2 "symbol" with .s at RHS_END2.n
- .PE
- .LP
- Note that the empty alternative for `B' is represented in the
- data structure by the `LLEORULE-struct' immediately following
- the `LLALT'-struct. When there are still other alternatives
- the `LLEORULE'-struct is replaced by a `LLALT'-struct followed
- by the other alternatives and a `LLEORULE'-struct.
- Finally, when the empty rule is the only rule for a
- nonterminal the RHS will consist only of a `LLEORULE'-struct.
- .SH
- A.2 Delayed deletes
- .LP
- We encountered a problem with deleting elements during the
- prediction phase. Imagine that we have a nonterminal `B' on top of
- the graph, and `B' has two alternatives. Now suppose that we
- apply the first alternative and we find out that this alternative leads
- to a `dead end', i.e. a head that does not match the input symbol, so we want
- to get rid of it. When we delete it immediately the deletion algorithm
- will also deallocate `[B]' and possibly some elements below `[B]'.
- However, there was another alternative for `[B]' which was not yet
- developed and maybe this alternative leads to a head which is legal.
- But `[B]' has already been deleted and thus cannot be used anymore. A similar
- situation can occur when we want to delete a joined element;
- the substitution of a nonterminal
- that only produces empty and thus has no element above it in the graph
- can also lead to such a situation. We therefore decided to put `dead ends'
- on a list, `cleanup_arr[]', and after the prediction phase has
- finished we delete all elements on this list, and all their descendants
- that become unreachable of course.
- .SH
- A.3 Clearing flags
- .LP
- We implemented two different ways to clear the flags set by the prediction
- phase of the algorithm; the first recursively tracks down the whole graph
- following the flags, the second puts all elements visited by
- the prediction phase
- on a list; after the prediction phase has finished the algorithm walks
- through this list clearing the flags of all elements on it. We took measurements
- on both algorithms and found out that with small programs the times
- did not differ much but large programs were processed faster by the
- second algorithm. Therefore we decided to use the second algorithm.
- .LP
- To speed up the algorithm even more, we do not deallocate the list
- after a prediction phase has finished. We just set the number of
- elements on the list to 0. This saves considerably on the number
- of `Malloc'-calls.
- .SH
- A.4 Implementation of %erroneous directive
- .LP
- As explained in chapter 3, the user can put a %erroneous directive
- in front of a terminal, making the non-correcting error recovery
- mechanism ignore that terminal. However, implementing this directive
- was not entirely straightforward; consider, for example, the rule
- .br
- .nf
- A: 'a' | %erroneous 'b' | 'c';
- .fi
- .LP
- Just leaving out terminal 'b' will not do, because then nonterminal
- A produces empty all of a sudden, which it did not before.
- The rule should become
- .br
- .nf
- A: 'a' | 'c';
- .fi
- but this is hard to implement in LLgen. We took a different approach:
- we introduce a new terminal 'ERRONEOUS', and substitute it for all
- terminals with an %erroneous directive in front of them. Thus, the
- example rule becomes
- .br
- .nf
- A: 'a' | ERRONEOUS | 'c';
- .fi
- .LP
- Since the terminal ERRONEOUS will never be in the input to the parser,
- this has exactly the desired effect; when a predicting phase produces
- ERRONEOUS as head of a prediction graph this head will never match the
- input. In particular, it will not match the terminal that was
- originally there (in this case 'b') so that terminal is no longer
- regarded as part of the input language at that point.
- .bp
- .SH
- Appendix B: Using the non-correcting error recovery
- .LP
- To use the new non-correcting error recovery mechanism, LLgen has to
- be called with the new flag -n. LLgen will then create an extra file
- called `Lncor.c' which contains the code for the non-correcting recovery
- mechanism. This file has to be compiled and linked with the rest
- of the program, just like the file `Lpars.c'.
- .LP
- The user-supplied error reporting routine `LLmessage' will have to be
- modified slightly; when it is called with a positive parameter, it
- should only set the attributes of the inserted token, but not report an
- error. Note that the lexical analyzer still must return the same token
- as it did the last time it was called. When LLmessage is called with
- parameter 0, it should report that the token in global variable LLsymb
- is illegal; if the value of LLsymb is `EOFILE', the routine should
- report an unexpected End-of-file. When LLmessage is called with parameter
- -1, it should report that end-of-file was expected. To facilitate
- switching between correcting and non-correcting error recovery,
- the file Lpars.h contains a statement `#define LLNONCORR'
- which indicates that the non-correcting
- mechanism is enabled.
- Here is a
- skeleton for the modified LLmessage routine:
- .nr PS 8
- .nr VS 10
- .LP
- .br
- .nf
- #include "Lpars.h"
- extern int LLsymb;
- LLmessage(flag)
- int flag;
- {
- if (flag < 0)
- {
- /* Error message "end-of-file expected" */;
- }
- else if (flag)
- {
- /* flag equals the number of the inserted token */
- #ifndef LLNONCORR
- /* Error message "token inserted" */;
- #endif
- /* Code to set attributes for inserted token */
- /* Code to make lexical analyzer return same token as before */
- else
- {
- /* The number of the illegal or deleted token is in LLsymb */
- #ifndef LLNONCORR
- /* Error message "token deleted" */;
- #else
- if (LLsymb == EOFILE)
- {
- /* Error message "unexpected end of file" */
- }
- else
- {
- /* Error message "token illegal" */;
- }
- #endif
-
- }
- }
- .fi
- .nr PS 10
- .nr VS 12
- .LP
- For best results, one should check if the parser calls other parsers
- in semantic actions; if this is the case, and the called parser
- processes the same input file as the calling parser, then a %substart
- should be put in front of the semantic action that starts a parser.
- If a semantic action calls parsers defined by startsymbols say
- A and B, then `%substart A, B;' should be put in front of the action.
- As an alternative, one can use the -s flag of LLgen; this has the
- same effect as putting `%substart X, Y, ....;' in front of all
- semantic actions, where X, Y, .... are the startsymbols of the grammar.
- Clearly, it is preferable to analyze the grammar and put %substart
- directives only where appropriate.
- Finally, beware of syntactic errors being handled in semantic
- actions; eg, one could have a rule like
- .nr PS 8
- .nr VS 10
- .LP
- .br
- .nf
- Assignment_statement: lvalue
- [
- '='
- {
- error(":= expected");
- }
- |
- ':='
- ]
- expression
- ;
- .fi
- .nr PS 10
- .nr VS 12
- .LP
- To ensure that the non-correcting mechanism will recognize the
- `=' as a syntactic error, a `%erroneous' directive should be
- put in front of it.
|