Newsgroup: it.comp.lang.pascal
Titolo: [Code|Long] Parser ( incompleto )

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

 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)

 
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?


 
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 )


 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 :)
