Google Groups no longer supports new Usenet posts or subscriptions. Historical content remains viewable.
Dismiss

[Code|Long] Parser ( incompleto )

1 view
Skip to first unread message

Il Razziatore

unread,
Feb 6, 2008, 10:45:36 PM2/6/08
to
Salve a tutti!
Per la serie "Enormi blocchi di codice senza senso" del Razziatore ecco
la mia ultima fatica. Il codice che segue è un parser di sintassi
Pascal, mancano tre cose:
- Il supporto delle stringe
- Il supporto dei commenti
- Il supporto per il simbolo :=

ma bando alla chiacchere ecco il codice. Il codice contiene due oggetti
il primo di esso è TTextFile che è un semplice wrapper delle funzioni
basilari di I/O su file di testo.

type
TTextFile = object
private
Handle : Text;
public
procedure Open( FileName : String );
procedure Close;
function ReadLn : String;
function Eof : Boolean;
end;

procedure TTextFile.Open( FileName : String );
begin
Assign( Handle, FileName );
Reset( Handle );
end;

procedure TTextFile.Close;
begin
System.Close( Handle );
end;

function TTextFile.ReadLn : String;
var Line : String;
begin
System.ReadLn( Handle, Line );
ReadLn := Line;
end;

function TTextFile.Eof : Boolean;
begin
Eof := System.Eof( Handle );
end;

Non credo che ci sia molto da dire. La cosa più "carina" di queste righe
è l'accesso proprio alle funzioni della libreria System che hanno lo
stesso nome del metodo da cui vengono chiamate. Avvendo lo stesso nome
il metodo "nasconde" la funzione della libreria System quindi l'unico
modo per chiamare le funzioni, lasciando inalterate i nomi, è quello di
aggiungere il "namespace" della unit System alla chiamata, in questo
modo escono fuori righe del tipo

System.Close( Handle );

che non è un oggetto o qualcosa di strano ma semplicemente la chiamata
alla funzione Close della unit System. Ricordo ( a chi non lo sapesse )
che la unit System viene automaticamente richiamata.

Da questo semplice oggetto wrapper ne ho esteso un altro molto semplice

type
TSourceFile = object( TTextFile )
private
Line : String; { Current line, used for GetToken }
public
function GetToken( var Token : String ) : Boolean;
end;

Il parser, propriamente detto, è il metodo GetToken che se riesce a
trovare un token restituisce True altrimenti False. Questo metodo
utilizza alcune funzioni accessorie che non centrano molto con essa che
potrebbero essere tranquillamente messa in una libreria, prima di
passare al parser vado a spiegare queste, semplici, funzioni. Ho inoltre
bisogno di introdurre un tipo di dato. Che non viene utilizato dal
Parser ma da una funzione interna. Una funzione non viene spiegata la
funzione Min visto che penso che sia abbastanza "banale" vi risparmio.

function IsSpace( Chr : Char ) : Boolean;
begin
IsSpace := ( Chr = ' ' ) OR ( Chr = #09 )
end;

Questa funzione restituisce semplicemente TRUE se il carattere passato è
il carattere ' ' ( codice ascii $20 ) oppure il carattere TAB ( codice
ascii 9. Avrei potuto usare la funzione Chr( X ) ma ho preferito
generare il carattere tab con l'ausilio dell'operatore #. Il pascal non
ha i caratteri di escape tipo quelli del C, quindi una stringa '\t' non
aveva senso.

procedure DeleteSpaces( var Str : String );
var C : Byte;
begin
C := 0;
while IsSpace( Str[1 + C] ) AND ( C < Byte( Str[0] ) ) do begin
C := C + 1;
end;
if C > 0 then Delete( Str, 1, C );
end;

Questa funzione elimina tutti i caratteri all'inizio della stringa.
All'inzio avevo pensato di fare una serie di delete successive: se c'è
un carattere spazio elimino il primo carattere. Però in questo modo
avrei fatto fare alla funzione un sacco di lavoro inutile. Quindi ho
preferito contare gli spazi bianchi e poi eliminarli tutti in una volta.

type TCharSet = Set of Char;

Il tipo di tato è un semplice SET di caratteri. I set sono degli insiemi
di 256 valori. Si possono fare tutte le operazioni degli insiemi, io
ne gaccio una sola l'unione ( operatore + ).

Veniamo ad analizare il metodo. Il metodo usa 3 funzioni ausiliare
definite al suo interno. Queste tre funzioni sono: TokenLen_ che
determina la lunghezza di un token dato un set di caratteri che
definiscono il token, SymbolLen uguale a quella di prima ma senza
definire il set di caratteri ( sarebbero stati troppi ) e sinceramente è
quella che mi piace di meno... in ultimo la funzione SearchToken che mi
ha dato più filo da torcere, questa funzione va a leggere il sorgente e
si posiziona all'inzio del token annullando "lo stile" del programmatore
( iddentazione e accapo ). Questa funzione restituisce uno dopo l'altro
i token ( o meglio posiziona il cursore del file all'inzio dei token ).

function TokenLen_( Str : String; CharSet : TCharSet ) : Byte;
var Len : Byte; Done : Boolean;
begin
Len := 1;
Done := False;
while not Done do begin
if Len < Byte( Str[0] ) then
if Str[ Len + 1 ] in CharSet then
Len := Len + 1
else
Done := True
else
Done := True;
end;
TokenLen_ := Len;
end;

Il codice si commenta abbastanza da solo scorro semplicemente la stringa
che contiene il Token, e finché trovo un carattere "consono"
incremento un contatore ( Len ) altrimenti esco dalla funzione
ritornando il contatore. I set di caratteri sono definiti come costati
locali al metodo GetToken e le definizioni sono molto semplici

const
Numbers = ['0'..'9'] + ['$'] + ['#'];
Chars = ['a'..'z'] + ['A'..'Z'];
NumbersEx = Numbers + ['$'] + ['#'];


Passiamo ora alla seconda funzione

function SymbolLen( Str : String ) : Byte;
begin
if (
(
( Str[1] = '(' ) OR
( Str[1] = '.' )
) AND (
Str[2] = '.'
)
) OR (
(
Str[1] = '.'
) AND (
Str[2] = ')'
)
) then SymbolLen := 2 Else SymbolLen := 1;
end;

Questa funzione è verramente brutta ragazzi... pultroppo non sapevo come
altro. I simboli in pascal sono quasi tutti composti da 1 carattere ma
c'è ne sono alcuni che ne occupano 2 e questi sono pochi ( '..', '(.',
'.)' e ':=' che mi è sfugito :D ). Dopo la funzione più brutta arriva la
funzione di cui mi piace di più ( quella che mi ha dato più filo da
torcere ).

function SearchToken : Boolean;
var TokenFound : Boolean;
begin
TokenFound := False;
repeat
if Length( Line ) > 0 then begin
{ Elimino l'eventuale spazio prima del token }
if IsSpace( Line[1] ) then DeleteSpaces( Line );
{ Ho trovato un token? }
TokenFound := Not IsSpace( Line[1] );
end else if not Eof then
Line := ReadLn;
until ( Eof AND ( Line = '' ) ) OR TokenFound;
SearchToken := TokenFound;
end;

Mi ha dato tanto filo da torcere eppure è così lineare ( facile dire a
posteriore eh? ) E' un ciclo solo ma in realtà scorre in dure direzioni
il file ( e proprio qui stava il problema ). Prima estra un riga alla
volta e successivamente estra un token alla volta da questa righa, se la
righa e finita passa a quella successiva. Vorrei farvi notare
particolarmente alle chimata di "Eof" e ReadLn senza argomenti. Non è
sono le Eof e ReadLn della unit System, ma sono i metodi ereditati da
TTextFile ( ci troviamo dentro alla definizione del meto GetToken del di
un oggetto figlio ).

Passiamo al cuore vero e proprio:

begin
Token := '';
GetToken := False;
if SearchToken then begin
Token := Line;
if Token[1] in Chars then
TokenLen := TokenLen_( Token, Chars + Numbers )
else if Token[1] in NumbersEx then
TokenLen := TokenLen_( Token, NumbersEx + ['.'] )
else { if Token[1] in Symbols then }
TokenLen := SymbolLen( Token );
Delete( Token, TokenLen + 1, Length( Token ) );
Delete( Line, 1, TokenLen );
GetToken := True;
end;
end;

Si tutto qui :) Ovviamente utilizzo una varibile TokenLen definita
sempre localmente di tipo byte

var TokenLen : Byte;

E' molto semplice non credo che ci siano cose difficili, utilizzo le
funzioni fin qui definite per cercare ( tramite la SearchToken ) e per
prelevare ( tramite le due Delete ) un Token dalla Linea. Una cosa
"simpatica" da notare e che gli identificativi sono stati definiti come
Chars per il primo carattere ma come Chars + Numbers come altri
caratteri quindi se io ho un identificativo

abc123c

mi verrà correttamente ritornato. Stessa cosa vale per i numeri che
possono iniziare con un qualunque numero, con il carattere $ o # e che
nel corpo, solo nel corpo, possono avere il . ( per i reali )

Per testare il tutto ho scritto un semplice programma:

begin
Source := ParamStr( 1 );
if ( Source <> '' ) then begin
WriteLn( 'Compiling ', Source, '...' );
SourceFile.Open( Source );
while SourceFile.GetToken( Token ) do
WriteLn( Token );
SourceFile.Close;
end else WriteLn( 'No file input' );
ReadLn;
end.

e poi gli ho dato in pasto se stesso, ha funzionato alla grande :) [
anche se, ovviamente, ho visto le limitazioni di questo parser ma visto
che si è fatto tardi... ho deciso di postarlo così com'è... ci mettero
mano nei prossimi giorni e lo coreggero... ovviamente ogni comento è
gradito ]

--
Il Razziatore,
The Only Good Windows is an uninstalled Windows
-----------------------------------------------
MSN : IlRazz...@netscape.net
ICQ : 67552596
Yhaoo : Razziatore82
-----------------------------------------------
Founder of MediaPlayer Project
http://mpp.iwebland.com

Dreadnaut

unread,
Feb 7, 2008, 3:15:46 PM2/7/08
to
Il Razziatore <ilrazz...@gmail.com> ha scritto:

> e poi gli ho dato in pasto se stesso, ha funzionato alla grande :) [
> anche se, ovviamente, ho visto le limitazioni di questo parser ma visto
> che si č fatto tardi... ho deciso di postarlo cosě com'č... ci mettero
> mano nei prossimi giorni e lo coreggero... ovviamente ogni comento č
> gradito ]

per i pezzi che ti mancano, puoi spiare il mio scanner scritto esattamente
5 anni fa, che ancora RdC tiene online :)

http://utenti.lycos.it/pascallike/tools/pscan.rar

era parte del programma che convertiva l'help in html, se ben ricordo
--
Dreadnaut - http://dreadnaut.altervista.org/
(Life would be easier if I had the source code)

Il Razziatore

unread,
Feb 8, 2008, 2:20:55 AM2/8/08
to
Dreadnaut ha scritto:

> Il Razziatore <ilrazz...@gmail.com> ha scritto:
>> e poi gli ho dato in pasto se stesso, ha funzionato alla grande :) [
>> anche se, ovviamente, ho visto le limitazioni di questo parser ma visto
>> che si è fatto tardi... ho deciso di postarlo così com'è... ci mettero
>> mano nei prossimi giorni e lo coreggero... ovviamente ogni comento è
>> gradito ]
>
> per i pezzi che ti mancano, puoi spiare il mio scanner scritto esattamente
> 5 anni fa, che ancora RdC tiene online :)
>

Ho spiato... frettolosamente, ammetto... ma mi sembra un po' troppo
caotico. Oggi do un altra occhiata dopo l'esame di IA... pero' non mi
piace molto visto che infondo mi manca poco ( il := l'ho già
implementato ) me lo scrivo da me il resto :P

PS: Perché usi il PChar? String non ti piace?

Il Razziatore

unread,
Feb 8, 2008, 10:56:52 PM2/8/08
to
Il Razziatore ha scritto:

> Salve a tutti!
> Per la serie "Enormi blocchi di codice senza senso" del Razziatore ecco
> la mia ultima fatica. Il codice che segue è un parser di sintassi
> Pascal, mancano tre cose:
> - Il supporto delle stringe
> - Il supporto dei commenti
> - Il supporto per il simbolo :=
>

Ho finito il codice :) riesce a parserizare se stesso e questo credo che
sia un indice della sua "robustezza". Gli ho dato in pasto il suo stesso
sorgente e mi sono fatto stampare uno dopo l'altro tutti i token ( tutti
e 1189... è stato un piacere vederli scorrere a una velocità sostenuta ).

Oltre al supporto del simbolo := ne mancava i simboli matematici ( <=,
=> e <> ). Ma andiamo al codice.

Il supporto delle stringe è stato implementato in una funzione
accessoria in più ( StringLen ) che mi ha dato qualche problemino ( mi
troncava le stringe che contenevano il carattere ' )

il supporto per i commenti è stato implementanto direttamente dentro
"SearchToken", in presenza di un comento viene direttamente buttato
senza passarlo al codice princiapale di GetToken.

Il supporto dei simboli che mancavano è stato fatto ( banalmente )
aggiungendo altre condizioni alla funzione SymbolLen.

Ho preso l'occasione per dare un po' di restaling sposando ( cut-paste )
le funzioni accessorie in cima al file invece che mischiate al resto
del codice.

Analiziamo il codice ora. Il codice della funzione SymbolLen mi sembra
inutile analizarlo, quindi lo tralascio. Ho dovuto introdurre un altra
funzione ausiliaria che estendeva la funzione standard Pos ( che
restituisce la posizione della prima occorrenza di una sotto stringa
all'interno di una stringa ) per farmi ritornare non solo la prima
occorrenza ma di tutte le occorrenze. Il codice è questo:

function PosEx( Substr : String; S : String; Index : Byte ) : Byte;
var I, J : Byte; SubFound, Found : Boolean;
begin
I := Index;
J := 1;
Found := False;
while not found and ( I <= Byte( S[0] ) ) do begin
SubFound := S[I+J-1] = Substr[J];
if SubFound then begin
J := J + 1;
end else begin
I := I + 1;
J := 1;
end;
Found := ( J - 1 ) = Byte( Substr[0] );
end;
if Found then PosEx := I else PosEx := 0;
end;

Mi ha dato qualche filetto da torce devo dire, ma è venuta carina, non
credete? :D Funziona allo stesso modo di Pos, con in più il paramentro
Index che è l'indice da cui verrà iniziata la ricerca. Banalmente
potremmo ridefinire Pos utilizando la mia funzione come segue:

function Pos( Substr : String; S : String ) : Byte;
begin
Pos := PosEx( Substr, S, 1 );
end;

I cambiamenti da fare a GetToken sono minimi ( basta aggiungere la
chiamata alla funzione StringLen come negli altri casi:

if Token[1] in NumbersEx then
TokenLen := TokenLen_( Token, NumbersEx + ['.'] )

else if Token[1] = '''' then
TokenLen := StringLen( Token )


else { if Token[1] in Symbols then }
TokenLen := SymbolLen( Token );

Io l'ho messo tra i numeri e i simboli, ma si puo' mettere in qualunque
posizione ovviamente ( i simboli devono essere sempre gli ultimi pero' ).

Questa nuova funzione privata non è affatto banale, anche se molto
piccola ci sono stati diversi problemi di loop infinito che solo un
attenta opera di debug mi ha aiutato a sconfigere:

function StringLen( Str : String ) : Byte;


var Len : Byte; Done : Boolean;
begin
Len := 1; Done := False;

Repeat
Len := PosEx( '''', Str, Len + 1);
if ( 0 < Len ) AND ( Len < Byte( Str[0] ) ) then
if Str[Len+1] = '''' then


Len := Len + 1
else
Done := True
else
Done := True;

Until Done;
if Len = 0 then Len := Length( Str );
StringLen := Len;
end;

come si vede utilizzo la mia funzione PosEx per determinare dove finisce
la stringa ( altro ' ) una volta trovato controllo se la posizione
trovata e compresa tra l'inizio della stringa e la sua fine e in tal
caso vedo se il carattere successivo è un altro ' in questo caso vuol
dire che quello che ho trovato non è un carattere di fine stringa ma
bensì proprio il carattere ' interno alla stringa stessa. Se tutti i
controlli sono andati male il ciclo finisce... se Len è zero ( non ho
trovato il carattere di fine stringa, do come lunghezza l'intera riga.

L'ultimo pezzo che ci manca da analizare sono le modifica apportate a
SearchToken per scartare i commenti, avrei potuto fare una procedura (
sicuramente sarebbe venuto più elegnate ) ma al momento non l'ho fatto è
mi è venuto fuori così. Ho introdotto 3 varibilli:

CommentEnd : String[2];
CommentFound : Boolean;
P : Byte;

che vengono inizializate appena entriamo nella funzione ( è buona norma
sempre inizializare le varibili! )

CommentFound := False;
CommentEnd := '';

in cascata al controllo per cercare un token ho messo questo codice:

if TokenFound AND NOT CommentFound then begin
if ( Line[1] = '{' ) OR ( ( Line[1] = '(' ) AND ( Line[2] = '*' ) )
then begin
TokenFound := False;
CommentFound := True;
if Line[1] = '{' then
CommentEnd := '}'
else
CommentEnd := '*)';
end;
end;

come si vede questo codice semplicemente avvisa la funzione che non è
vero che ha trovato un token, ma un commento e si segana come dovrà
finire questo commento. Infatti un comento puo' essere comentato, basta
scegliere un stilo alternativo per il commento stesso.

if CommentFound then begin
P := Pos( CommentEnd, Line );
if P = 0 then
Line := ''
else begin
Delete( Line, 1, P + Byte( CommentEnd[0] ) - 1 );
CommentFound := False;
end;
end;

Questo ultimo blocco sempre in serie al precedente, fa l'effettivo
lavoro di eliminazione del commento. Se non trova la chiusura del
commento elimina semplicemente tutta la riga senza andare molto per il
sottile altrimenti se trova una chiusura per il commento elimina il
testo fino alla posizione del carattere deliminatore.

Se qualcuno volesse scaricare il sorgente ( senza doverlo ricostruire
per differenza tra questo e l'altro messaggio ) puo' reperirlo all'inderizzo

http://razziatore.no-ip.com/pascal/parser.pas ( 4.95 KiB )

Dreadnaut

unread,
Feb 21, 2008, 7:21:21 PM2/21/08
to
Il Razziatore <ilrazz...@gmail.com> ha scritto:

> PS: Perch‚ usi il PChar? String non ti piace?

già detto in privato, ma è giusto scriverlo anche qua. Le string hanno un
limite di 255 caratteri, PChar arrivano a quasi 64KB, e non si sa mai che
cosa ti possa ritrovare a leggere da un file :)

0 new messages