Sign in Join
Wiki copytemporary

Plex and Pyacc: what changed

Revision 2 to 3. Removed lines have a minus sign; added lines have a plus sign.

OldNewLine
11Free Pascal comes with substitutes for the GNU projects Lex and YACC. They are called Plex and Pyacc and they can be used to generate compilers and regular expression analyzers in Pascal instead of C.
22
33## Library contents
44
55TP Lex and Yacc can be found here: <http://svn.freepascal.org/cgi-bin/viewvc.cgi/trunk/utils/tply/>
66
77## Documentation
88
99Download the manual here: <http://svn.freepascal.org/cgi-bin/viewvc.cgi/trunk/utils/tply/tply.doc?revision=1>
1010
1111## Simple example application
1212
1313This is a very simple calculator, which shows how to use plex and pyacc. The files in this simple project are:
1414
1515- build.sh
1616- lexer.l
1717- parser.y
1818
1919WARNING: The code below compiles but currently doesn't work **\<-- Because it is simply wrong. See below and learn why !**
2020
2121### build.sh
2222
2323``` pascal
2424plex lexer.l
2525pyacc -d parser.y
2626mv parser.pas calculadora.pas
2727fpc calculadora.pas
2828```
2929
3030### lexer.l
3131
3232``` pascal
3333{ Analisador léxico da calculadora para a disciplina de PCS
3434
3535 Aluno: Felipe Monteiro de Carvalho }
3636
3737%{
3838%}
3939%%
4040
4141[0-9]+[dD]? begin yylval.yyInteger := StrToInt(yytext); yyruleno := NUMBER; end;
4242
4343[0-9a-fA-F]+[hH] begin yylval.yyInteger := StrToInt(yytext); yyruleno := NUMBER; end;
4444
4545[ \t] begin end; { ignorar espaços em branco }
4646
4747[-] begin yyruleno := MENOS; end;
4848
4949[+*/()] begin yyruleno := Integer(yytext[0]); end;
5050
5151[=][hH] begin yyruleno := IGUALH; end;
5252
5353[=][dD]? begin yyruleno := IGUALD; end;
5454
5555\n begin yyruleno := Integer(yytext[0]); end;
5656
5757. begin yyerror('Caracter inexperado'); end;
5858
5959%%
6060function meuyywrap (): Integer;
6161begin
6262 Result := 1;
6363end;
6464```
6565
6666### parser.y
6767
6868``` pascal
6969/* Parser
7070
7171 Aluno: Felipe Monteiro de Carvalho */
7272%{
7373program calculadora;
7474
7575{$mode delphi}
7676
7777uses SysUtils, yacclib, lexlib;
7878
7979%}
8080
8181%start entrada
8282%token <Integer> NUMBER IGUALD IGUALH MENOS
8383%type <Integer> expressao termo fator
8484
8585%%
8686
8787entrada
8888 : /* linha vazia */ { }
8989 | entrada linha { }
9090 ;
9191linha
9292 : expressao IGUALD '\n' { WriteLn(Format('Resultado: %d', [$1])); }
9393 | expressao IGUALH '\n' { WriteLn(Format('Resultado: %H', [$1])); }
9494 ;
9595expressao
9696 : expressao '+' termo { $$ := $1 + $3; }
9797 | expressao MENOS termo { $$ := $1 - $3; }
9898 | termo { $$ := $1; }
9999 ;
100100termo
101101 : termo '*' fator { $$ := $1 * $3; }
102102 | termo '/' fator { if ($3 = 0) then
103103 yyerror('Divisao por zero!')
104104 else
105105 $$ := $1 div $3; }
106106 | fator { $$ := $1; }
107107 ;
108108fator
109109 : NUMBER { $$ := $1; }
110110 | MENOS NUMBER { $$ := -1 * $2; }
111111 | '(' expressao ')' { $$ := $2; }
112112 ;
113113
114114%%
115115
116116procedure meuyyerror(s: PChar); // Called by yyparse on error
117117begin
118118 WriteLn(Format('Erro: %s', [s]));
119119end;
120120
121121{$include lexer.pas}
122122
123123begin
124124 yywrap := @meuyywrap;
125125// yyerror := @meuyyerror;
126126 yyparse ();
127127end.
128128```
129129
130130### How to use it
131131
132132Type for example: 5+3=
133133
134134And it will answer with: 8
135135
136136You can also try 3*8=H to request an answer in Hexadecimal instead of decimal
137137
138138# How to do it the right way
139139
140140**Sorry that I have to do this, but this example is wrong !**
141141
142142First of all I recommend you to read the manual and try to understand it.\
143143Secondly take a look at the examples provided with original tply package by Albert Graef distributed at: <http://www.musikwissenschaft.uni-mainz.de/~ag/tply>\
144144There you will find a more sophisticated calculator example named 'expr'. If you take a look at it you will find out that this is a basic example for Lex/Yacc generated parsers and it works.\
145145
146-**`Now I will explain you why you can NOT do things that way !`**
146+```text
147+Now I will explain you why you can NOT do things that way !
148+```
147149
148150We start with Lex:\
149151\# Lex returns a token to the parser which is always an integer value. The token is defined as an constant value with the **%token** command. Tokens can only be returned to Yacc through two commands, **return** and **returnc**.
150152
1511531. 1. **return** : This is the normal return of a token = integer
152154 2. **returnc**: With this it is possible to return one character, it is needed for the literals in Yacc rules
153155
154156Now we correct the return of a token from example above:
155157
156158``` pascal
157159[0-9]+[dD]? begin yylval.yyInteger := StrToInt(yytext); yyruleno := NUMBER; end; <- nothing, because yyruleno ends up in nirvana
158160[0-9]+[dD]? begin yylval.yyInteger := StrToInt(yytext); return(NUMBER); end; <- good
159161```
160162
161163And the litteral return :
162164
163165``` pascal
164166[+*/()] begin yyruleno := Integer(yytext[0]); end; <- ouch !
165167[+*/()] returnc(yytext[1]); <- returns single characters we want
166168```
167169
168170Can you see the difference ?\
169171Why the hell one should use index NULL ? And no need for begins and ends.\
170172Last but not least the strange function:
171173
172174``` pascal
173175function meuyywrap (): Integer; <- ??? what is this for ??? this no overload for yywrap !
174176begin
175177 Result := 1;
176178end;
177179```
178180
179181The yywrap function is for loading multiple files, do we need this if we read from stdin and write to stdout ? No, we don't !\
180182
181183The working Lex file:
182184
183185``` pascal
184186{ Analisador léxico da calculadora para a disciplina de PCS
185187
186188 Aluno: Felipe Monteiro de Carvalho }
187189
188190%{
189191%}
190192%%
191193
192194[0-9]+[dD]? begin yylval.yyInteger := StrToInt(yytext); return(NUMBER); end;
193195
194196[0-9a-fA-F]+[hH] begin yylval.yyInteger := StrToInt(yytext); return(NUMBER); end;
195197
196198[ \t] ; { ignorar espaços em branco }
197199
198200[-] return(MENOS);
199201
200202[+*/()] returnc(yytext[1]);
201203
202204[=][hH] return(IGUALH);
203205
204206[=][dD]? return(IGUALD);
205207
206208\n returnc(yytext[1]);
207209
208210. yyerror('Caracter inexperado');
209211
210212%%
211213```
212214
213215Now we go on with Yacc:\
214216The main block:\
215217If we want to read a file we have to assign and open it before we can parse it. If we want to read more files we have to use yywrap.\
216218With a parser like this we need only stdio. So we need to do nothing because Lex assigns and opens the 'files' known as stdin and stdout.\
217219With this we can forget all but the yyparse() call.\
218220
219221Additional functionality:\
220222As a feature we want to write user defined error messages. For this we simply overload the yyerror procedure.\
221223
222224The working Yacc file:
223225
224226``` pascal
225227/* Parser
226228
227229 Aluno: Felipe Monteiro de Carvalho */
228230%{
229231program calculadora;
230232
231233{$mode delphi}
232234
233235uses SysUtils, yacclib, lexlib;
234236
235237procedure yyerror(s: string); // Called by yyparse on error <- overload !
236238begin
237239 WriteLn(Format('Erro: %s', [s]));
238240end;
239241
240242%}
241243
242244%start entrada
243245%token <Integer> NUMBER IGUALD IGUALH MENOS
244246%type <Integer> expressao termo fator
245247
246248%%
247249
248250entrada
249251 : /* linha vazia */ { }
250252 | entrada linha { }
251253 | entrada linha '\n' { writeln('DONE.'); yyaccept; } /* <- nice end with RETURN */
252254 ;
253255linha
254256 : expressao IGUALD '\n' { WriteLn(Format('Resultado: %d', [$1])); }
255257 | expressao IGUALH '\n' { WriteLn(Format('Resultado: %Xh', [$1])); } /* <- hex = X , add 'h' to identify this result as a hex number */
256258 ;
257259expressao
258260 : expressao '+' termo { $$ := $1 + $3; }
259261 | expressao MENOS termo { $$ := $1 - $3; }
260262 | termo { $$ := $1; }
261263 ;
262264termo
263265 : termo '*' fator { $$ := $1 * $3; }
264266 | termo '/' fator { if ($3 = 0) then
265267 yyerror('Divisao por zero!')
266268 else
267269 $$ := $1 div $3; }
268270 | fator { $$ := $1; }
269271 ;
270272fator
271273 : NUMBER { $$ := $1; }
272274 | MENOS NUMBER { $$ := -1 * $2; }
273275 | '(' expressao ')' { $$ := $2; }
274276 ;
275277
276278%%
277279
278280
279281{$include lexer.pas}
280282
281283begin
282284 yyparse ();
283285end.
284286```
285287
286288Hint: One does not need to type tokens as done in this example as the default yylval is always integer. If you do type tokens pyacc will generate a corresponding YYStype for you.\
287289**WOW. The calculator works as expected !**\
288290GVS 00:09, 9 March 2014 (CET)\
289291=Additional Information= If you are a beginner you may have more questions than answers after reading the above. Thus I will try to show you things to make live easier.\
290292We start with Lex and its regular expressions. I don't explain how to build them, read the documentation to find this out.\
291293I will try explain some implementation details.\
292294
293295A lexer can use multiple rules that start with the same character or set of characters. This is not recommended because it can cause a problem. The main problem is that the lexer uses the rule with the longest match and returns that token. Now if you return different tokens with these rules you may not be able to match the yacc rule you expected. This is the common error if you write wrong lex rules and wonder why yacc returns syntax errors. If you use such rules in a way they can be identified correctly this can be a benefit. But with such rules you should take care if you need the 'loop' as in (a\|b)* which can consist of a unknown number of 'a' or 'b' or even be empty. This is not a good start or end for your rule if the 'constant' characters do not cause a significant match.\
294296One common use is the detection of numbers where you have two rules, one for integers and one for reals. The rule for integer is then the beginning of the rule for real and there is no problem. There is also no problem if these rules return the same token. Then the only question is if you can find a better rule that includes both single rules in one rule.\
295297Example: Integer and Real numbers without sign
296298
297299``` pascal
298300[0-9]+ ReturnVerb(INUM,1)
299301'.'[0-9]+ ReturnVerb(RNUM,2)
300302[0-9]+"."[0-9]+ ReturnVerb(RNUM,3)
301303```
302304
303305Now you ask: Why should I parse numbers without sign ? This is a yacc problem, because the sign can be in an expression as a operator e.g. 'a+b' or as a prefix sign e.g. '-1'. Then you may have a token defined for MINUS and you don't need to know if the *sign* is a prefix sign or an operator, the yacc rules will handle it.\
304306Taking a look to the code you will see that I returned the token via the 'ReturnVerb' procedure. This is for debugging the lexer to see the token at the time it is matched. The procedure writes the token number and rule index and then calls the normal return procedure. This is useful because I see the token and the text representation of the matching rule. The text can be read from yytext which is always the current match.\
305307One issue you should keep in mind: The lexer is a function that is called from the parser and the token is returned at the point of time the function exits. The parser uses yylval only if he needs this value and one can not find out at which time this will be. The only valid way to return a value to your parser is to use yylval AND to set this value right before the call of return or returnc, otherwise the value is lost or you run into trouble. For details see below at the returnc explanation.\
306308
307309You can combine rules with predefined rule parts standing before the rule part in your file. This makes it easier to write longer rules and you can use them in multiple rules.\
308310Example: Rules consisting of predefined parts
309311
310312``` pascal
311313L [A-Za-z]
312314D [0-9]
313315
314316%%
315317
316318{D}+(\.{D}+)?([Ee][+-]?{D}+)? return(RNUM);
317319
318320{L} return(CH);
319321
320322" " ;
321323
322324. |
323325\n returnc(yytext[1]);
324326```
325327
326328One important thing you can see in this example is that one always has to catch all characters of input. Even if you don't need them like the SPACE. Typically in programming languages the so called WHITE SPACE is ignored at all. This is the most difficult thing to match all the unused characters to empty rules without breaking the rules for tokens (difficult because they do not appear in your output where wrong parsed and returned tokens can be found, things you don't see are hard to find). Empty rules do not call return or returnc. If you can not find a rule you can use built in functions to to read the characters yourself or write additional characters to input. This is commonly used with comments. After a initial match of a short rule to match the start of a comment the input is read until the end of the comment or EOF is found.\
327329Example: Line Comment
328330
329-`%{`\
330-`procedure CommentEOL;`\
331-` var c:char;`\
332-`begin`\
333-`  c:= get_char;`\
334-`  while (c <> #0)and(c <> #10) do  c:= get_char;`\
335-`end; `\
336-`%}`\
337-`"//"         CommentEOL;`
331+``` pascal
332+%{
333+procedure CommentEOL;
334+ var c:char;
335+begin
336+ c:= get_char;
337+ while (c <> #0)and(c <> #10) do c:= get_char;
338+end;
339+%}
340+"//" CommentEOL;
341+```
338342
339343With this all the characters the get_char function reads from input are lost. If one is reading a multi line comment the ending maker may have to be written back. This is done with unget_char. This characters can than be read in next call of yylex to be evaluated.\
340344Example: A language with '/' as an ID, '//' as a unused sequence until EOL or \#26
341345
342346``` pascal
343347"/" begin
344348 c := get_char;
345349 if c <> '/' then
346350 begin
347351 unget_char(c);
348352 ReturnC(yytext[1]);
349353 end
350354 else
351355 begin (* line comment *)
352356 while ((c<>#10)and(c<>#13)and(c<>#26)) do c := get_char;
353357 if c=#26 then unget_char(c);
354358 end;
355359 end;
356360```
357361
358362Lets talk about returnc.\
359363As I said above (at the correction part) that tokens are always integer but returnc gets an character as an argument you might say that is not true. But it is, returnc uses ORD to convert the character to integer. Now you will understand why your token constants (have you look at yacc generated pasacl code) are always higher than 256; its the full ASCII range for litteral tokens so other tokens are higher.\
360364If you use returnc you can deal with literals in yacc rules but you have to make sure to return ALL the characters you use in yacc. Yacc does not care about not returned characters and your rules will never match.\
361365
362366The short way to debug lex is to overload the return procedures.\
363367Example: Overload lex return
364368
365369``` pascal
366370procedure return ( n : Integer );
367371begin
368372 writeln('return >',yytext,'< = ',n);
369373 (* now do the 'inherited' code return would do *)
370374 yyretval := n;
371375 yydone := true;
372376end(*return*);
373377
374378procedure returnc ( c : Char );
375379begin
376380 writeln('returnc >',yytext,'< c = ',c,' , n = ',ord(c));
377381 (* now do the 'inherited' code returnc would do *)
378382 yyretval := ord(c);
379383 yydone := true;
380384end(*returnc*);
381385```
382386
383387The result of this is shown next for calculating 4*5 as a hex value with the calculator above:
384388
385389``` pascal
3863904*5=h
387391return >4< = 257
388392returnc >*< c = * , n = 42
389393return >5< = 257
390394return >=h< = 259
391395returnc >
392396< c =
393397 , n = 10
394398Resultado: 14h
395399```
396400
397401Remark: You see the NEWLINE from input causing the same on output (n = 10).\
398402
399403Now its time for Yacc.\
400404
401405Benefits of literals in rules are that you can use them with less trouble. Another thing is that the rules are more readable. The trouble with non literal tokens is that you easily generate conflicts in yacc rules. This is a problem with tokens that appear very often in your rules and thus the parser has to do different things with them. It is always a good idea to write rules in such a way that their beginning is significant. If you can not write such rules you might have to change your lexer.\
402406Using EMPTY with rules cause trouble too if you use them in combination with loops. In loops you can have only one rule that produces empty. The other rules have to have a significant beginning/ending or part (this needs to be unique in this loop of rules). It is also a problem if you have different rules that end with the same token and the loop separator is this token. Then the parser can not decide what to do if this token is found. Typically the use of SEMICOLON can cause this in a pascal parser (semicolons are not always allowed or close different rules that are not significant itself).\
403407Example: FPC Parser snippet to parse CLASS\
404408
405409``` pascal
406410Class_Typed_Type : _CLASSFORWARD /* 'Class ;' */
407411 | LClass_PRE _OF { LDisable(co_Class); } id
408412/* Class Type */
409413 | LClass_PRE Class_Component_List { LDisable(co_Class); }
410414 | LClass_PRE '(' Class_Implemented_Interface ')'
411415 Class_Component_List { LDisable(co_Class); }
412416 ;
413417LClass_PRE : _CLASS { LEnable(co_Class); } ;
414418Class_Implemented_Interface :
415419 id
416420 | id ',' Class_Interface_Identifiers
417421 ;
418422Class_Interface_Identifiers : Y_Id_Comma_List ;
419423Class_Component_List : OPT_Class_Components _END ;
420424OPT_Class_Components :
421425 /* EMPTY */
422426 | Class_Component_LOOP
423427 ;
424428Class_Component_LOOP : /* LOOP */
425429 Class_Component
426430 | Class_Component_LOOP Class_Component
427431 ;
428432Class_Component :
429433 Class_Visibility_Specifier
430434 | Class_Field_Definition
431435 | Class_Method_Definition { LClassModifiers(false); }
432436 | Class_Property_Definition
433437 ;
434438Class_Visibility_Specifier : /* 'private' | 'public' | 'published' | 'protected' | 'strict' 'private' ; */
435439 CVI_PRIVATE
436440 | CVI_PROTECTED
437441 | NKW_PUBLIC
438442 | CVI_PUBLISHED
439443 | CVI_STRICT CVI_PRIVATE
440444 ;
441445
442446Class_Field_Definition : /* all decl starting with id as a field */
443447 /* Const
444448 type */
445449 Class_Variable_Declaration
446450 | Class_ClassVariable_Declaration
447451 ;
448452Class_Variable_Declaration :
449453 Y_Id_Comma_List ':' { LEnable(co_ClassVarStatic); LHints(true); } RClass_Variable_Declaration ;
450454RClass_Variable_Declaration :
451455 Result_Type OPT_Hint ';' OPT_Static { LDisable(co_ClassVarStatic); LHints(false); }
452456 ;
453457OPT_Static :
454458 /* EMTY */
455459 | NKW_STATIC ';'
456460 ;
457461Class_ClassVariable_Declaration : _CLASS Class_Variable_Declaration ;
458462Class_Method_Definition :
459463 Class_PuF_Definition
460464 | Class_CuD_Definition
461465 | _CLASS Class_PuF_Definition
462466 ;
463467
464468Class_PuF_Definition : PuF_Definition ;
465469Class_CuD_Definition :
466470 _CONSTRUCTOR PLike_Definition
467471 | _DESTRUCTOR PLike_Definition
468472 ;
469473```
470474
471475This are context sensitive rules to switch between the different cases CLASS can be used in a class type declaration. The LEnable and LDisable switches the lexer to a different mode. To do this the line 'LClass_PRE : _CLASS { LEnable(co_Class); } ;' is a own rule that has to be completed first to give the lexer time to switch the modes (the next lookahead character of the parser triggers the rule end; the parser has to reduce the rule to get to the next state before the next token is evaluated which we want to switch to a different token).\
472476Remark: This example parser switches between different sets of KEYWORDS because not all tokens that are used are real RESERVED words of the language. This means that e.g. 'public' is not a keyword and is only enabled on context as 'NKW_PUBLIC'. The '_CLASSFORWARD' is a typical conflict where the string 'Class ;' terminates with SEMICOLON. Thus the lexer does a lookahead if a semicolon is found behind 'Class' and then returns the token '_CLASSFORWARD' instead of '_CLASS' and the problem is solved.\
473477
474478The rule in between pitfall:\
475479Example: Hello in between
476480
477481``` pascal
478482rule1 : ID rule2 { writeln($2); }
479483
480484rule2 : ID1 { $$:='Hello'; } ID3 { $$:= $2; }
481485```
482486
483487'Hello' can only be assigned with the last statement '{ \$\$:= \$2; }' as yacc generates a new state for the rule in between. The assignment must be at the end of this rule.\
484488
485489Additional explanation to this last two examples:\
486490This are different cases ! The first example is for switching the lexer and the second example is for parsers internal value handling to return the right value to the next rule.\
487491
488492# Useful Links
489493
490494[[Make your own compiler, interpreter, parser, or expression analyzer]]
491495
492496GVS 04:48, 9 March 2014 (CET)
493497
494498---
495499*From the Free Pascal wiki, page "Plex and Pyacc", as it stood on 2020-02-23 after a change by Trev, out of the wiki's dump of 2024-05-26. Copyright stays with the author who wrote it, according to the old wiki's [[Licensing]] page. If you have concerns about this material, [message the admin](/wiki-contact).*
496500Category: Code
497501Category: Compiler design
498502Category: Utilities

Back to history