Sviluppo di un compilatore Sviluppo di un compilatore per un
Transcript
Sviluppo di un compilatore Sviluppo di un compilatore per un
Universita’ degli studi di Roma “Tor Vergata” Facoltà di Ingegneria Corso di laurea in ingegneria informatica Tesi di laurea Sviluppo di un compilatore per un linguaggio imperativo Relatore Laureando Prof. Alberto Pettorossi Fernando Iazeolla Anno Accademico 2003-2004 Indice 1. Principi di funzionamento dei compilatori ….…...……… 5 …...………………………………. 7 Il parser .…………………………………………. 7 Il back end .…………………………………………. 12 1.1 Il lexical analizer 1.2 1.3 2. Il punto di partenza ……………………………………….. 14 3. Progetto del compilatore…………………………………... 21 4. Uso e funzionamento del compilatore …………………… 32 5. Descrizione del codice sorgente ………………………….. 35 …………………………………………... 35 5.1 compiler 5.2 lexan 5.3 parser …………………………………………………. 39 5.4 symtab …………………………………………………. 45 5.5 encode …………………………………………………. 46 5.6 toIntel …………………………………………………. 53 5.7 assemble …………………………………………... 58 5.8 toStream …………………………………………... 64 5.9 toCode …………………………………………………. 66 ……….………………………………………… 35 6. Il codice sorgente ………………………………………….... 69 7. Conclusioni …………………………………………………... 81 2 Introduzione Ad appassionarmi a questa disciplina sono stati gli argomenti trattati in un corso di informatica del mio relatore. Il professore ha trattato argomenti teorici sugli algoritmi di parsing e sulle teorie dei linguaggi e grammatiche . Al giorno d’oggi possiamo trovare diversi generatori di grammatiche e di parser (come ad esempio Lex e Yacc [1]) in cui possiamo fornire in input una grammatica ed ottenere in uscita un parser per la stessa grammatica. Ad esempio se volessimo codificate la seguente grammatica con Yacc: EE+T|T TT*F|F F (E) | digit digit 0|1|2|...|9 dove E è un’espressione T è un termine ed F è un fattore, basterà fornire al generatore di parser, Yacc, il seguente programma: 3 %{ #include<ctype.h> %} %token DIGIT %% line: expr: term: factor: expr ‘\n’ expr ‘+’ term | term ; term ‘*’ factor | factor ; ‘(‘ expr ‘)’ | DIGIT ; { printf(“%d\n”,$1); } { $$=$1 + $3; } { $$ = $1 * $3 ; } { $$ = $2 ; } %% yylex() { int c; c=getchar(); if(isdigit(c)) { yylval=c-‘0’; return DIGIT; } return c; } . Tuttavia la cosa che mi interessava maggiormente era applicare le conoscenze acquisite sull’argomento, e quindi la cosa migliore era partire da zero: progettare quindi un compilatore dall’inizio alla fine, in tutte le sue componenti, una cosa che tempo fa consideravo difficilissima che solo una persona con doti particolari potesse essere in grado di fare. Certo rimane il fatto che per progettare e sviluppare un compilatore occorre avere una profonda conoscenza che spazia in quasi tutti i campi dell’informatica: sistemi operativi, architetture dei calcolatori, elementi di programmazione e codici macchina. 4 Capitolo 1 Principi di funzionamento dei compilatori Essenzialmente un compilatore è un programma che legge un programma scritto in un linguaggio (il linguaggio sorgente) e lo traduce in un programma equivalente in un altro linguaggio (di solito codice macchina o codice oggetto). Programma sorgente Compilatore Codice oggetto Figura 1. Compilatore visto come traduttore Oggi esistono moltissimi compilatori per diversi linguaggi di programmazione. Ogni linguaggio ha insita in sè una caratteristica principale che lo rende preferibile rispetto ad un altro a seconda del tipo di progetto che si deve sviluppare. Così ad esempio il Fortran ed il Pascal sono preferiti nello sviluppo di applicazioni scientifico-didattiche, il Prolog è utilizzato nell’intelligenza artificiale, il Basic dai neofiti che si avvicinano per la prima volta alla programmazione, il C e Java sono liguaggi general pur pose (il primo è usato per programmazione di sistemi anche a basso livello, il secondo per progetti commerciali basati sull’architettura inter-computer e platform independent). In principio programmare un computer significava essere in grado di programmare in linguaggio macchina (sequenze binarie {0,1}) su schede perforate da inserire 5 nell’elaboratore che li eseguiva a seconda di come era programmato il suo job scheduler (multi-programmato o meno). Il risultato era poi serializzato in output e se si presentava un errore il programmatore doveva analizzare il suo programma (che era una sequenza di 0 e 1) riperforare una nuova scheda e richiedere di nuovo un tempo di CPU da dedicare alla rielaborazione del job. Stiamo parlando degli anni ’40 ed i computer erano macchine molto grandi che occupavano intere stanze come ad esempio l’ENIAC (Electronic Numerical Integrator And Calculator) al cui progetto partecipò J. von Neumann. Nasce così l’esigenza di portare la fase di scrittura del codice verso linguaggi più vicini all’uomo che alla macchina in modo da poter ottenere una maggiore efficienza, robustezza, comprensibilità del codice e maggior facilità nel debugging. Nacque così negli anni ’50 il Fortran (FORmula TRANslation). L’ideatore di questo linguaggio fu John Backus. Lo scopo di questo linguaggio era quella di automatizzare calcoli matematici e scientifici. Questo linguaggio ebbe molto successo e sulla sua scia vennero progettati moltissimi altri linguaggi di alto livello. Si cominciarono così ad affinare le tecniche di progettazione dei compilatori. Diamo ora uno sguardo più approfondito ai compilatori. Un compilatore può essere visto come diviso in due parti: il cosiddetto ‘front end’ (costituito da un lexical analizer, o scanner, e da un parser) e il ‘back end’. Il front end legge il programma sorgente mentre il back end genera il programma equivalente in codice macchina. Compilatore Front end Back end Figura 2. Schema più approfondito del compilatore . 1.1 Il Lexical Analizer 6 Il primo passo è senz’altro quello di leggere il file di testo in input cercando i simboli e parole chiave del linguaggio. Questo è il compito del lexical analizer: leggere dall’input stream e restituire una sequenza di token che il parser può utilizzare per l’analisi sintattica. Gli elementi da riconoscere sono numeri, parole e simboli del linguaggio. Il riconoscimento di tali elementi può essere rappresentato tramite espressioni regolari o automi finiti. - Numeri Digit 0|1|2|3|4|5|6|7|8|9 Digits digit digit* Optional_fraction . digit | Optional_ exponent (E (+ | - | ) digits) | Number digits optional_fraction optional exponent - Identificatori Letter A|B|C|…|Z|a|b|c|…|z Digit 0|1|2|3|4|5|6|7|8|9 Id Letter (letter | digit)* - Relazioni e simboli aritmetici Sym rel_op | op | rest rel_op <= | >= | == | != op + | - | * | / rest ( | ) | ; | . | && | “ | ! dove ε e’ la stringa vuota. 1.2 Il Parser Una volta eseguita la scanzione del file di codice ed identificati tutti i token il controllo passa al parser, il vero cuore del compilatore. La sintassi del linguaggio di programmazione riconosciuta dal parser può essere descritta dalle grammatiche context-free o dalla notazione BNF (Backus-Naur Form) . Le grammatiche context-free hanno quattro componenti: 1. un insieme di simboli terminali (es. t) 2. un insieme di simboli non terminali (es. N) 3. un insieme di produzioni nella forma N N | t 4. un simbolo non terminale che prende il nome di simbolo di partenza. 7 Assumiamo il parser come una rappresentazione dell’albero di parsing per i token generati dal lexical analizer. Ad esempio sia data la seguente grammatica: list list + digit list list – digit list digit digit 0|1|2|3|4|5|6|7|8|9 Figura 3. Linguaggio che riconosce somma e sottrazione i simboli non terminali sono: list e digit, mentre i terminali sono + - 0 1 2 3 4 5 6 7 8 9 0 e list è detto simbolo iniziale perchè è definito per primo. Se ora vogliamo fare il parsing della stringa ’9-5+2’ otteniamo il seguente albero di parsing: List List List Digit Digit digit 9 - 5 + 2 Figura 4 . Albero di parsing della stringa 9-5+2. 8 gli alberi di parsing delle grammatiche context-free hanno le seguenti proprietà: • la radice dell’albero è il simbolo iniziale • ogni foglia finale è un terminale o ε (stringa vuota) • ogni nodo interno (cioè non una foglia finale) è un non terminale. • Se A è un non terminale e rappresenta un nodo all’interno dell’albero e se X1,X2,…,Xn sono i Figli di quel nodo (che possono essere sia simboli terminale che non terminali) da sinistra a destra allora A X1 X2 .. Xn è una produzione. Bisogna fare attenzione però perchè se definiamo male una grammatica corriamo il rischio di non poter determinare un solo albero di parsing. In questo caso siamo di fronte a grammatiche dette ambigue. Ad esempio, supponiamo di non distinguere tra list e digit come precedentemente fatto e consideriamo la seguente grammatica: string -> string + string | string – string | 0|1|2|3|4|5|6|7|8|9 e la seguente espressione: ‘9-5+2’. ora siamo di fronte a due alberi di parsing: (9-5)+2 e 9-(5+2) che danno risultati totalmente diversi. string string string string 9 string string - 5 string + 2 string string string 9 string - 5 string + 2 Figura 5 . Due alberi di parsing generati dalla grammatica ambigua Nella maggior parte dei linguaggi di programmazione le quattro operazioni aritmetiche (addizione, sottrazione, moltiplicazione e divisione) sono left associative, cioè data l’espressione 9+5+2 essa equivale a (9+5)+2 così come 9-5-2 equivale a (9-5)-2. C’è tuttavia da stabilire l’ordine di precedenza degli operatori in quanto data l’espressione 9+5*2 questa può dare luogo a due diverse interpretazioni: (9+5)*2 e 9 9+(5*2). L’associatività dell’addizione (+) e della moltiplicazione (*) non risolvono questa ambiguità. Bisogna stabilire due livelli di precedenza: la moltiplicazione e la divisione legano di piu’ dell’addizione e della sottrazione. Creiamo quindi due non terminali expr e term per i due livelli di precedenza piu’ un altro non terminale factor che sono atomi nelle espressioni (digit o espressioni racchiuse tra parentesi) . La grammatica risultante sarà quindi: expr expr + term | expr – term | term term term * factor | term / factor | factor factor digit | (expr) digit 0|1|2|3|4|5|6|7|8|9 Questa grammatica tratta un espressione come una lista di term separati da + o – e i termini come una lista di factor separati da * o da / . Le espressione racchiuse tra parentesi sono trattate come fattori così possiamo ottenere espressioni annidate. Il parser ha il compito di verificare se una certa sequenza di token, passategli dal lexical analizer, può essere generato da una grammatica. Ci sono tre tipi principali di parser di grammatiche. Gli algoritmi di parsing quali il Cocke-Younger-Kasami [3] e l’Earley [3] sono in grado di parsare qualsiasi tipo di grammatica. Ma questi parser sono poco efficienti per essere implementati su calcolatore. Così i compilatori vengono progettati con dei parser top-down , bottom-up o predictive parser. Il top-down parsing costruisce l’albero di parsing a partire dalla radice che è il simbolo iniziale della grammatica. data la seguente grammatica: S cAd A ab | a e il seguente input w=cad, vediamo col metodo top-down se tale stringa è generata dalla grammatica. La radice quindi corrisponde ad S e espandiamo tale radice in modo da ottenere l’albero in Figura 6. 10 S c A d Figura 6 . Albero di parsing dopo la prima produzione Visto che la foglia più a sinistra ‘c’ corrisponde con il primo simbolo di w, avanziamo nell’input e consideriamo il secondo simbolo di w e la seconda foglia ‘A’ . espandiamo A usando la prima alternativa nella grammatica ottenendo l’albero in Figura sottostante. S c A a d b Figura 7 . Albero di parsing dopo la seconda produzione e vediamo che abbiamo la corrispondenza per il secondo simbolo di w. Avanziamo quindi nell’input e consideriamo il suo terzo simbolo d. questo non corrisponde con la terza foglia dell’albero. Andando in backtracking consideriamo l’albero di parsing con la seconda alternativa della produzione di A come in Figura sottostante. S c A d a Figura 8 . Albero di parsing dopo il backtracking 11 Abbiamo così la corrispondenza tra la stringa di input e le foglie dell’albero il che ci dice che la stringa appartiene alla grammatica. Una grammatica left-recursive può causare un loop infinito perchè una produzione del tipo A Ab | a espande un non terminale con lo stesso non terminale senza dare luogo a nessun terminale. I bottom-up parser costruiscono l’albero di parsing dato una stringa di input a partire dalle foglie a salire fino alla radice. Possiamo pensare agli algoritmi bottom-up come delle riduzioni successive della stringa di input fino all’assioma di partenza della grammatica (al simbolo non terminale iniziale della grammatica). Ad ogni riduzione sostituiamo alla stringa di input la parte sinistra della produzione la cui parte destra coincide con una sottostringa dell’input. Ad esempio consideriamo la seguente grammatica: S aABe A Abc | b Bd E sia la stringa di input la seguente: abbcde . abbcde aAbcde aAde aABe S Alla stringa di partenza applichiamo la riduzione Ab e la stringa diventa: aAbcde. Poi dalla produzione AAbc la stringa diventa aAde. Dalla produzione Bd la stringa diventa aABe. Infine applichiamo la seguente riduzione SaABe. 1.3 Il Back End Una volta generato l’albero di parsing e le relative symbol table (le tabelle relative alle variabili globali e locali), il back end può essere visto come la visita dell’albero di parsing da cui poi si genera il codice oggetto (per i compilatori) e da cui vengono interpretate le singole istruzioni una alla volta (per esempio negli interpreti). Gli interpreti infatti eseguono una istruzione alla volta del codice sorgente senza tradurlo in nessun altro programma equivalente. Molti anni fa ad esempio nel interprete BASIC si leggeva una riga alla volta del programma che terminava con un 12 ritorno a capo, si analizzava la riga e la si eseguiva. Nei moderni interpreti di linguaggi come ad esempio il Perl si legge tutto il programma generando l’albero di parsing in memoria, e l’esecuzione del programma non è altro che la visita di tale albero. Ciò consente di rilevare eventuali errori sintattici prima dell’esecuzione della prima istruzione del programma sorgente nonchè consente una esecuzione più veloce del programma in quanto già parzialmente codificato in memoria. 13 Capitolo 2 Il punto di partenza Il punto di partenza di questa tesi è l’articolo di Sterling-Shapiro [2] nel quale si introduce brevemente la possibilità di progettare un semplice compilatore scritto in Prolog. Il linguaggio sorgente da loro considerato è un sottoinsieme del Pascal, il PL ideato da loro stessi a scopo dimostrativo. Gli statement presenti nel loro linguaggio sono pochissimi: abbiamo lo statement di assegnamento ad una variabile, uno statement condizionale (if-then-else), uno statement di loop (while), e due di I/O (read e write). Il PL ammette solo variabili globali, senza quindi la possibilità di definire istanze di variabili locali, non tipate visto che è presente solo il tipo intero e non è previsto l’utilizzo di funzioni e procedure. Il PL si riduce quindi ad uno script-language. program factorial; begin read value; count:=1; result:=1; while count < value do begin count:= count +1; result:=result*count; end; write result end. Figura 9. Programma fattoriale scritto in PL. La grammatica relativa al linguaggio PL è descritta qui sotto dove in grassetto sono indicati i simboli terminali. 14 pl_program program identifier ; statement statement begin statement rest_statement statement identifier := expression statement if test then statement else statement statement while test do statement statement read identifier statement write expression rest_statement ; statement rest_statement rest_statement end. expression pl_constant | pl_constant aritmetic_op expression aritmetic_op + | - | * | / pl_constant identifier | pl_integer identifier word pl_integer int_number test expression comparison_op expression comparison_op = | < | > | >= | <= | =\= word letter | letter word letter a | b |…| z int_number -> digit | digit int_number digit 1 | 2 | …| 9 | 0 . Il codice oggetto prodotto in uscita dal processo di compilazione è uno pseudoassembler sempre da loro stesso ideato. È un assembler che ha un solo registro (accumulatore) che è implicito nell’istruzione, mentre l’altro operando che può essere un intero, una costante, un indirizzo di una cella di memoria contenente dati o istruzioni del programma, è esplicito. Le istruzioni con il relativo significato sono elencate nella seguente tabella. . add mem istruzione operando sub addc mem cost mul subc mem cost div mulc mem cost load divc mem cost write loadc cost read store mem jump add label mem somma il contenuto della cella di memoria mem significato all’accumulatore sottrai il contenuto dellacost cellaall’accumulatore di memoria mem add constant: addiziona all’accumulatore moltiplica il contenuto della cella di memoria mem con sub constant: sottrae cost all’accumulatore l’accumulatore divide l’accumulatore concost il valore della cella di memoria mul constant: moltiplica all’accumulatore mem immetti nell’accumulatore il contenuto della cella di div constant: dividi cost all’accumulatore memoria mem scrive su standard il contenuto dell’accumulatore load constant: mettioutput il valore di cost nell’accumulatore immetti nell’accumulatore il valore dello standard input il valore dell’accumulatore nella cella di memoria puntata da mem salto incondizionato somma il contenuto della cella di memoria mem all’accumulatore 15 jumpeq label jump if equal jumpne label jump if not equal jumplt label jump if less then jumpgt label jump if greater then jumple label jump if less or equal jumpge label jump if greater or equal Figura 10 . Target language instructions Il programma fattoriale presentato in Figura 1 sarà quindi tradotto nel programma mostrato nella seguente Figura. symbol address instruction operand symbol 16 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 LABEL1 LABEL2 COUNT RESULT VALUE READ LOADC STORE LOADC STORE LOAD SUB JUMPGE LOAD ADDC STORE LOAD MUL STORE JUMP LOAD WRITE HALT BLOCK 21 1 19 20 20 19 21 16 19 1 19 20 19 20 6 20 0 0 3 VALUE COUNT RESULT COUNT VALUE LABEL2 COUNT COUNT RESULT COUNT RESULT LABEL1 RESULT Figura 11. Codice assembly del programma fattoriale Dove halt termina il programma e block alloca un blocco di celle di memoria per le variabili utilizzate dal programma pari al valore del suo operando. Il processo di compilazione è di solito eseguito in cinque passi: 1) analisi lessicale, eseguita dal lexical analizer, in cui si esegue la scansione del codice sorgente e si restituiscono al parser i token (identificatori di numeri , parole e simboli). 2) Analisi sintattica, in cui il parser esamina i token fornitegli dal lexical analizer e genera il relativo albero di parsing. 3) Generazione del codice oggetto, in cui tramite una visita dell’albero di parsing si produce un codice oggetto rilocabile, cioè in cui gli indirizzi delle variabili o celle di memoria non sono ancora definite in maniera assoluta. 4) Link, in cui si importano le eventuali funzioni di libreria del linguaggio e si risolvono tutte le assegnazioni di memoria. 5) Output, in cui si scrive finalmente il codice su file generando anche le opportune intestazioni per i relativi sistemi operativi qualora il tipo di file lo richieda. L’articolo di Sterling-Shapiro si focalizza completamente sui tre passi centrali del processo di compilazione, tralasciando del tutto gli altri aspetti. 17 Source text Token list Lexical analysis Parse tree Syntax analysis Object structure Code generation Object structure (absolut e) link output output Figura 12. Processo di compilazione Quindi al parser passiamo già una lista di token. Test_compiler(X,Y):- program (X,P), compile(P,Y). Program(test1,[program,test1,’;’,begin,write,x,’+’,y,’‘,z,’/’,2,end). Program(test2,[program,test2,’;’,begin,ig,a,’>’,b,then,max,’:=’,a, else,max,’:=’,b,end]). Program(factorial, [program,factorial,’;’ ,begin ,read,value,’;’ ,count,’:=’,1,’;’ ,result,’:=’,1,’;’ ,while,count,’<’,value,do ,begin ,count,’:=’,count,’+’,1,’;’ ,result,’:=’,result,’*’,count end,’;’ ,write,resutl ,end]). Una volta parsato l’input otteniamo i relativi output del processo di parsing: program test1: write(expr(x,name(x),expr(,name(y),expr(/,name(z),number(2)))));void program test2: 18 if(compare(>,name(a),name(b)),assign(max,name(a)),assign(max,name( b)));void program test3 (factorial): read(value);assign(count,number(1));assign(result,number(1)); while(compare(<,name(count),name(value)), (assign(count,expr(+,name(count),number(1))); assign(result,expr(*,name(result),name(count)));void)); write(name(result));void Questi alberi di parsing vengono poi dati in pasto all’encoder che genera il codice oggetto rilocabile. L’output generato dall’encoder per i relativi programmi di test è il seguente: program test1: ((((instr(load,Z);instr(divc,2));instr(store,Temp); instr(load,Y);instr(sub,Temp));instr(add,X));instr(write,0));no_op program test2: (((instr(load,A);instr(sub,B));instr(jumple,L1)); (instr(loadA);instr(store,Max));instr(jump,L2);label(L1); (instr(load,B);instr(store,Max));label(L2));no_op program test3 (factorial): instr(read,Value);instr(loadc,1);instr(store,Count)); (instr(loadc,1);instr(store,Result));label(L1); ((instr(load,Count);instr(sub,Value));instr(jumpge,L2)); (((instr(load,Count);intr(addc,a));instr(store,Count)); ((instr(load,Result);instrmul,Count));instr(store,Result)); no_op);instr(jump,L1);label(L2));(instr(load,Result);instr(write,0 ));no_op Ed infine l’assemblatore prende il codice oggetto non istanziato e genera il codice finale. program test1: instr(load,11);instr(divc,2);instr(store,12);instr(load,10); instr(sub,12);instr(add,9);instr(write,0);instr(halt,0);block(4) program test2: instr(load,10);instr(sub,11);instr(jumple,7);instr(load,10); instr(store,12);instr(jum,9);instr(load,11);instr(store,12); instr(halt,0);block(3) program test3 (factorial): instr(read,21);instr(loadc,1);instr(store,19);instr(loadc,1); instr(store,20);instr(load,19);instr(sub,21);instr(jumpge,16); instr(load,19);instr(addc,1);instr(store,19);instr(load,20); instr(mul,19);instr(store,20);instr(jump,6);instr(load,20); 19 instr(write,0);instr(halt,0);block(3) 20 Capitolo 3 Progetto del compilatore Partendo dall’articolo di Sterling-Shapiro [2], il mio compito era di progettare e realizzare un compilatore di un linguaggio imperativo stile C-like funzionante in tutte le sue parti e ovviamente scritto in Prolog. Il linguaggio da me creato ricalca quindi lo stile C ed ha le seguenti caratteristiche: • ha l’aritmetica dei puntatori • ha le funzioni con i parametri passati alle funzioni • ha variabili globali • ha variabili locali • supporta tipi di dato interi, puntatori e stringhe • è procedurale • adotta una sintassi C like L’aritmetica dei puntatori è implementata come in C attraverso i simboli & e * . Ad esempio se x è una variabile intera, allora con &x si intende l’indirizzo di memoria associato alla variabile, mentre con *x si intende la locazione di memoria puntata dal valore di x. Le funzioni si definiscono tramite la keyword sub seguita dall’identificatore (il nome della funzione) e da eventuali parametri racchiusi tra parentesi tonde. Ad esempio la funzione che somma due interi potrebbe essere così implementata: sub somma(a,b) { local(x); x=a+b; return(x); } Nel mio linguaggio non serve dichiarare una variabile o una funzione prima di utilizzarla, sarà il compilatore che risolve i nomi automaticamente. Per chiamare una funzione si fa precedere l’identificatore dal simbolo del dollaro, così per chiamare la funzione somma sopra implementata basterà fare: 21 val=$somma(val1,val2); Le variabili globali si usano in qualsiasi parte del codice senza bisogno di dichiararle. Es. x=12; while(x>0) { x=x-1; } y=x; Per usare le variabili locali invece bisogna dichiararle come tali tramite la keyword local all’inizio della funzione di riferimento. Es. sub sum1(a,b) { local(var); var=a+b; write(var); } main() { var=1; $sum1(5,6); } All’inizio dell’esecuzione del programma riportato qui sopra, la variabile var avrà valore 1. Viene poi chiamata la funzione sum1 in cui si assegna alla variabile var il valore della somma tra 5 e 6. La variabile locale che ha sede nello stack avrà valore 11 e manterrà tale valore fino al completamento della funzione sum1 e verrà poi distrutta e sarà l’unica con tale nome che può essere utilizzata nello scope della funzione in cui è dichiarata, mentre la variabile globale var che avrà un indirizzo di memoria nella zona dati avrà valore 1 e manterrà tale valore fino alla fine del programma e si potrà fare riferimento a tale variabile se in nessuna funzione in esecuzione sarà dichiarata una variabile locale con tale nome. Le due variabili sono quindi due entità distinte. Abbiamo quindi visto come utilizzare i tipi di dato interi e puntatori. Per quanto riguarda le stringhe invece, queste non sono altro che un puntatore ad una zona di memoria dove vi è memorizzata la stringa. Se ad esempio vogliamo avere la stringa ‘mia_stringa' nella variabile x, allora dobbiamo scrivere quanto segue: *x=”mia_stringa”; ed in memoria corrisponde ad avere una locazione assegnata alla variabile x di lunghezza 2 bytes (intero) che contiene il puntatore alla zona di memoria della stringa. 22 x mia_stringa In uscita il compilatore qui sviluppato produce i seguenti tre diversi tipi di output. 1) Output in pseudo assembler. È una versione ampliata rispetto allo pseudo assembler proposto da Sterling-Shapiro, in quanto si era di fronte alla necessità di dover gestire lo stack per le variabili locali, puntatori, e accumulo e rilascio di risorse necessarie alle funzioni. 2) Output in assembler per processori Intel 80x86. 3) Output direttamente in codice macchina per processori Intel 80x86. Pseudo-assembler compilatore sorgente Assembler sorgente Intel 80x86 Codice macchine Intel 80x86 Figura 13 . Output del mio compilatore Prendiamo ad esempio il seguente programma sorgente per il mio compilatore e vediamo come viene tradotto nei vari tipi di output. 23 main() { x=5; y=&x; *y=2; } Figura 14 . Programma sorgente 1 2 3 4 5 6 7 8 9 10 11 12 13 14 jump 2 init_funz loadc 5 store 12 loadc 12 store 13 loadc 2 storep 13 halt destroy_local(0) end_funz dw 0 dw 0 block(3) x y Figura 15. Output in pseudo assembler 00000100 00000101 00000102 00000105 00000106 00000107 00000109 0000010C 0000010F 00000112 00000115 00000118 0000011A 0000011D 0000011F 00000120 00000121 00000123 00000125 00000127 0000012B 0000012C 0000012D 0000012F 00000131 0E 1F E90000 90 55 89E5 B80500 A32D01 B82D01 A32F01 B80200 89C2 BB2F01 8B07 50 5B 89D0 8907 CD20 81C40000 5D C3 0000 0000 90 push cs pop ds jmp 0x5 nop push bp mov bp,sp mov ax,0x5 mov [0x12d],ax mov ax,0x12d mov [0x12f],ax mov ax,0x2 mov dx,ax mov bx,0x12f mov ax,[bx] push ax pop bx mov ax,dx mov [bx],ax int 0x20 add sp,0x0 pop bp ret add [bx+si],al add [bx+si],al nop 24 Figura 16. Output in codice e assembler Intel 80x86 Altro esempio , il programma ‘somma’. sub somma(a,b) { local(z); z=a+b; return(z); } main() { x=$somma(1,3); } jump 12 init_funz create_local(2) load stack_bp(6) add stack_bp(4) store stack_bp(-2) load stack_bp(-2) destroy_local(2) end_funz destroy_local(2) end_funz init_funz loadc 1 push 0 loadc 3 push 0 call 2 destroy_local(4) store 23 halt destroy_local(0) end_funz dw 0 block(2) Figura 17. Programma 'somma’ e il suo output in pseudo-assembler 00000100 00000101 00000102 00000105 00000106 00000107 00000109 0000010D 0000010F 00000113 00000115 0E 1F E93400 90 55 89E5 81EC0200 89EB 81C30600 8B07 89EB push cs pop ds jmp 0x39 nop push bp mov bp,sp sub sp,0x2 mov bx,bp add bx,0x6 mov ax,[bx] mov bx,bp 25 00000117 0000011B 0000011D 0000011F 00000123 00000125 00000127 0000012B 0000012D 00000131 00000132 00000133 00000137 00000138 00000139 0000013A 0000013B 0000013D 00000140 00000141 00000144 00000145 00000148 0000014C 0000014F 00000151 00000155 00000156 00000157 00000159 81C30400 0307 89EB 81C3FEFF 8907 89EB 81C3FEFF 8B07 81C40200 5D C3 81C40200 5D C3 90 55 89E5 B80100 50 B80300 50 E8BDFF 81C40400 A35701 CD20 81C40000 5D C3 0000 90 add bx,0x4 add ax,[bx] mov bx,bp add bx,0xfffe mov [bx],ax mov bx,bp add bx,0xfffe mov ax,[bx] add sp,0x2 pop bp ret add sp,0x2 pop bp ret nop push bp mov bp,sp mov ax,0x1 push ax mov ax,0x3 push ax call 0x5 add sp,0x4 mov [0x157],ax int 0x20 add sp,0x0 pop bp ret add [bx+si],al nop Figura 18. Output in assembler intel del programma 'somma’ La prima istruzione del codice prodotto in pseudo-assembler è un salto incondizionato alla funzione di ingresso (main). Ogni funzione inizia con init_funz che ha lo scopo di settare lo stack per le eventuali allocazione future di variabili locali. La funzione main in questo caso, immette nello stack i valori da passare alla funzione somma. Quindi esegue un push degli argomenti da sinistra a destra come nella convenzione adottata dal linguaggio Pascal. A questo punto esegue una chiamata alla funzione somma, tramite l’istruzione call. Una volta terminata la funzione somma, a differenza della convenzione adottata nel linguaggio Pascal dove è la funzione chiamata a liberare lo stack dai parametri di ingresso, qui è la funzione chiamante che fa tale operazione tramite la pseudo istruzione destroy_local(n) dove n rappresenta il numero di bytes da liberare. Nel caso dell’applicazione di esempio avevamo due variabili intere da passare alla funzione somma, quindi una volta terminata la funzione chiamata dobbiamo 26 eliminare 4 bytes dallo stack, visto che nei processori intel in modalità reale ogni intero è codificato con due bytes usando la convenzione little-endian ossia scrivendo in memoria prima il byte meno significativo e poi quello più significativo. A questo punto viene salvato il valore di ritorno nella apposita variabile e viene invocato l’uscita dal programma tramite la pseudo istruzione halt. Poi il programma termina immediatamente. Diamo uno sguardo più da vicino a quello che succede quando viene passato il controllo alla funzione somma e vediamo come viene usato lo stack e come viene tradotto lo pseudo codice in codice Intel 80x86. Un indirizzo assoluto in modalità reale di un processore Intel 80x86 viene rappresentato tramite una coppia di registri detti segmento (segment register SR) e offset (offset register OR) entrambi di 16 bit. Il calcolo di tale indirizzo può essere trovato tramite la formula SR*16+OR. Lo stack è rappresentato tramite la coppia di registri SS (segmento) e SP (offset). registro significato ax bx cx dx si di bp sp ds es ss cs ip registro accumulatore registro generico “ “ “ “ indice sorgente indice destinazione base pointer stack pointer data segment extra segment stack segment code segment instruction pointer Figura 19. I registri del processore intel 80x86 in modalità reale È una convenzione scrivere la coppia segmento e offset separati da due punti: SS:SP che quindi punta alla posizione attuale dello stack. Se inseriamo un nuovo valore nello stack (ad esempio push ax) verranno eseguiti dal processore i seguenti passi: sottrai un byte a SP: SP=SP-2 immetti nella nuova posizione il valore di ax. 27 Se eliminiamo un valore dallo stack (ad esempio pop ax) verranno eseguiti dal processore i seguenti passi: immetti in ax il valore puntato dallo stack SS:SP SP=SP+2. Quindi lo stack è una pila il cui valore puntato da SP decrementa ogni volta che si immette un valore e incrementa ogni volta che ne eliminiamo uno. Seguiamo quindi l’andamento dello stack con l’evolversi del tempo così da poter capire come il compilatore usa lo stack per le variabili locali ed i parametri passati alle funzioni. Notiamo innanzi tutto che stiamo lavorando in modalità reale del processore e che tutte le chiamate alle funzioni del programma sono di tipo near, cioè tutto il programma è racchiuso in un solo segmento e può essere al massimo di 64kb. Questo significa che nell’identificare l’istruzione corrente in esecuzione possiamo dare per scontato il segmento codice (CS) ed utilizzare solamente il puntatore all’istruzione (IP). Essendo tutto il programma racchiuso in un solo segmento il segmento dati (DS) sarà lo stesso del segmento codice (CS) e così anche per lo stack (SS). Lo stack pointer (SP) parte dalla cima del segmento (FFFEh) mentre l’instruction pointer (IP) parte all’indirizzo 100h. Infatti il compilatore produce in output files di tipo .COM che sono file eseguibili in MS-DOS (o la shell di windows) che stanno in un solo segmento. Ma questi vengono mappati in memoria a partire dall’offset 100h in quanto il sistema operativo necessita di inserire in memoria alcune informazione per il processo che stà per lanciare: il PSP (Program Segment Prefix) che occupa 255 bytes. 28 Figura 20. Mappatura del file in memoria Per referenziare lo spazio nello stack delle variabili locali legate al ciclo di vita della propria funzione di appartenenza si usa il registro bp, il base pointer. All’inizio della funzione si fa puntare il base pointer all’attuale stack pointer (SP), si decrementa il valore dello stack pointer (SP) in modo da riservare spazio fisico per le variabili, e dal base pointer si utilizzano degli offset per accedere alle proprie variabili. Il base pointer viene poi ripristinato al valore precedente, che è quello della funzione chiamante, una volta che termini la subroutine in esecuzione. Appena fatto eseguire il programma la situazione del segmento relativo al programma è mostrata in Figura 20. Lo stack è vuoto e lo stack pointer ha valore FFFEh. La pseudo istruzione init_funz allinea il base pointer (BP) in modo da puntare alla finestra per accedere alle eventuali variabili locali. Questa corrisponde alle istruzioni intel: push bp (salva il vecchio valore) mov bp,sp (lo spazio delle variabili locali della funzione main parte da FFFEh) . Si salvano da questo indirizzo i parametri per la funzione somma (loadc 1; push; loadc 3;push, che sono tradotti in assembler intel 80x86 in mov ax,1; push ax; mov ax,3; push ax) e si chiama la funzione somma (call) . Nella chiamata alla funzione il processore mette nello stack l’indirizzo di ritorno, cioè l’indirizzo successivo all’istruzione call (dal programma in Figura 18 è 148h). Essendo tutte le 29 chiamate a funzioni di tipo near necessitiamo solo di 2 bytes per il registro IP, tralasciando il registro CS. Figura 21. Istanza dello stack dopo la chiamata 'call'. A questo punto siamo nella funzione somma. E qui di nuovo init_funz setta lo stack per le variabili locali della funzione (push bp; mov bp,sp). Ora siccome la funzione ha due variabili locali, il compilatore ha generato la pseudo istruzione create_local(2), che siccome lavoriamo solo con variabili a 16 bit si traduce semplicemente in sub sp,2, cioè sottrai 2 allo stack pointer. Ora i parametri delle funzioni sono sopra il base pointer mentre le variabili locali sono sotto. Quindi per accedere ai parametri bisogna addizionare un offset al base pointer, mentre per accedere alle variabili locali bisogna sottrarre un offset al base pointer. Questo è quanto succede quando prendiamo i valori dei parametri della funzione somma per addizionarli: load stack_bp(6) e load stack_bp(4) in pseudo assembler che si traducono in assembler intel 80x86 in mov bx,bp; add bx,6; mov ax,[bx] e mov bx,bp; add bx,4; mov ax,[bx]. Per quanto riguarda le variabili locali, invece, queste si trovano sotto il base pointer. Quindi l’istruzione pseudo assembler sarà: load stack_bp(-2). 30 Figura 22. Istanza dello stack dopo l'inizializzazione della funzione somma. I processori dei calcolatori lavorano in modulo 10000h, e per default sono tutti interi positivi. Come rappresentare i numeri negativi dunque? Facciamo un esempio con tre bit e supponiamo che il processore lavori in modulo 8 e quindi troncherà i bit superiori al quarto. Perciò i numeri 101 e 1101 per il processore sono la stessa cosa, anche se in realtà non è così. Noi sappiano che -1 +1=0 . Trasportando il tutto in modulo 8 a tre bit come il nostro processore ipotetico avremo che 111+1=0 ossia 7+1=0 perciò 7 equivale a -1 in modulo 8. Quindi per lo stesso motivo il numero -2 sarà tradotto dal processore come FFFEh. Quindi la pseudo istruzione per accedere alla variabile locale del programma di esempio di Fig 23 , load stack_bp(-2) sarà tradotta nella seguente sequenza di istruzioni intel 80x86: mov bx,bp; add bx,FFFEh; mov ax,[bx]. Al termine della funzione somma (e di qualsiasi altra funzione in generale) il compilatore pone la coppia di pseudo istruzioni: destroy_local(n) e end_funz. destroy_local(n) , come si può intuire dal nome, elimina le variabili locali dallo stack e n sta ad indicare il numero di bytes da liberare. Questa viene tradotta in assembler intel 80x86 semplicemente con la seguente: add sp,n. 31 end_funz termina la funzione e il suo scopo è di ripristinare il vecchio base pointer. Questa si traduce banalmente in assembler intel 80x86 con la seguente: push bp. Nel nostro programma di esempio a questo punto siamo tornati alla funzione main e vediamo che necessitiamo di liberare la memoria dello stack dai parametri passati alla funzione somma. Tale operazione come detto prima, viene svolta dalla funzione chiamante e si risolve banalmente come se si dovessero eliminare le variabili locali, quindi con un destroy_local(n) dove n rappresenta sempre il numero di bytes da liberare e che si traduce sempre in assembler intel 80x86 con un semplice add sp,n. A questo punto il programma prosegue la sua esecuzione fino ad incontrare l’istruzione che termina il programma (halt in pseudo assembler) che richiama l’interrupt del DOS che esegue tale compito (int 20h in assembler intel 80x86). Capitolo 4 Funzionamento ed uso del compilatore Vediamo ora come usare il compilatore. Il compilatore è stato scritto in linguaggio Prolog ed è composto dai seguenti files: 1. compiler: è il modulo principale. 2. lexan: è il lexical analizer del compilatore. 3. parser: è il parser del compilatore. 4. encode: è il primo modulo del back end. Produce codice oggetto rilocabile in cui variabili fanno riferimento ancora alle symbol table e le etichette devono ancora essere risolte. 32 5. assemble: dal codice rilocabile risolve tutti gli indirizzi di memoria e di variabili producendo codice assoluto in doppio formato (pseudo assembler e assembler intel 80x86). 6. tostream: formatta l’output a video dei sorgenti assembler. 7. tointel: traduce il codice rilocabile prodotto da encode in codice rilocabile per intel 80x86 che poi deve essere sottoposto al processo di assemble. 8. tocode: traduce il sorgente di intel 80x86 assoluto prodotto da tointel+assemble nel corrispondente codice macchine per processori intel 80x86 e scrive in output su file di tipo .COM . 9. symtab: per la gestione dei parametri delle funzioni e variabili locali. Come prima cosa fare eseguire l’interprete Prolog (io ho usato lo SWI-Prolog). Caricare il file principale compiler. Richiamare il goal ‘load’. Questo non fa altro che una serie di ‘consult’ di tutti gli altri files elencati poco sopra. Cioè carica in memoria tutti i predicati necessario per la compilazione con un solo input. A questo punto siamo pronti per la compilazione. Sia ‘prog1’ il programma sorgente che dobbiamo compilare. Il predicato ‘compile’ può eseguire quattro differenti tipi di output su standard output (schermo) o su file. La sintassi è: compile(File,TipoOutput,Strem). Dove File è ovviamente il file sorgente da compilare (prog1 nel nostro caso). TipoOutput può assumere i seguanti valori: pseudoasm: l’output sarà il codice in linguaggio pseudo assembler che abbiamo visto nei capitoli precedenti, intelSrc: l’output sarà in linguaggio assembler per processori intel 80x86. questa opzione ci farà dunque vedere un sorgente in assembler per processori intel, intelCode: l’output sarà direttamente il codice macchina per processori intel 80x86. Stream può assumere i seguanti valori: debug: solamente associato a pseudoasm. Questa opzione permette di visualizzare l’albero di parsing e i codici intermedi nei vari passi del processo di compilazione. Questa opzione è stata creata da me per un più facile debug in fase di creazione del compilatore ed è stata poi lasciata per scopi didattici, toScreen: l’output stream avviene su standard output , quindi se non è reindirizzato viene stampato a video, 33 FileOut: specifica un nome di file per la scrittura del codice prodotto. Se il file specificato esiste già, viene troncato a zero e riscritto dall’inizio. Se il processo di compilazione va a buon fine l’interprete Prolog risponderà yes. Se invece il programma sorgente non è scritto correttamente, allora il compilatore non lo riconoscerà come un linguaggio generato dalla sua grammatica e terminerà con un messaggio “not a valid program”. A questo punto se si è scelto di generare un codice macchina (intelCode) il file generato sarà un eseguibile MS-DOS (o shell di windows) in formato .COM che come sappiamo non necessita di alcun heather su file e può essere lungo al massimo 64kb perchè dovrà essere contenuto in un solo segmento di memoria. Sarà quindi l’immagine di ciò che verrà caricato in memoria a partire dall’offset 100h del segmento scelto dal sistema operativo in quanto i primi 100h bytes sono riservati dallo stesso sistema operativo. Nota Ho riscontrato un’anomalia nell’interprete SWI-Prolog in ambiente windows. L’unica (ma letale) anomalia riguarda la scrittura del codice macchina. Più nello specifico se eseguiamo un raw-write scrittura a basso livello del numero 10, l’interprete aggiunge (di sua spontanea volontà) in numero 13 a seguire. Ciò non avviene in ambiente Linux. Immagino che ciò abbia a che fare su come i sistemi operativi gestiscano i ritorni a capo. In ambiente Linux questo è codificato tramite un byte (10 appunto) , mentre in ambiente windows questo è codificato tramite due bytes (10 line feed, e 13 carridge return). Quindi immagino che la scrittura del byte 10 venga interpretato dall’interpreto Prolog in ambiente windows come un tentativo di ritorno a capo e di conseguenza l’interprete stesso aggiunge il byte 13 a seguire. Questo comporta la non corretta esecuzione del codice macchina prodotto in quanto tutti i riferimenti a locazione di memoria risulteranno sfasati. Invece in ambiente linux tutto fila liscio: se gli si dice di scrivere su file il byte 10…magicamente viene scritto solo 10! Questo errore comparirà sicuramente se nel codice sorgente vi è una istruzione per stampare a video il valore di una variabile ad esempio write(x). Infatti come vedremo in seguito nel codice macchina corrispondente si eseguirà un ciclo dove di dividerà la variabile per 10 e si mette il risultato nello stack e poi finito il ciclo delle divisioni, preleveremo dallo stack cifra per cifra il valore della variabile stampandolo a 34 schermo. Quindi nell’operazione di divisione per 10 avremo un problema di sfasamento. Questo avviene sempre ovunque compaia il numero 10 (x=10; y<10; ecc..). Ho quindi scelto di lavorare al progetto in ambiente linux su ci ho installato un emulatore di MS-DOS anzichè fare ogni volta il reboot per testare i programmi compilati. Capitolo 5 Descrizione del codice sorgente 5.1 Il file ‘compiler’ Abbiamo già visto nel capitolo precedente il funzionamento e l’utilità dei predicati contenuti in tale file. 35 Riassumendo il predicato load carica tutti i predicati in memoria. I predicati compile(File,Type,Out) compilano il file sorgente (File) producendo differenti output (Out) secondo il tipo di compilazione desiderato (Type). 5.2 Il file ‘lexan’ Il lexical analizer. Il lexical analizer prende come input il file di caratteri che è il sorgente del programma da compilare, e ritorna una lista di token da parsare. Prendiamo ad esempio come sorgente il file somma di Figura 23. sub somma(a,b) { local(z); z=a+b; return(z); } main() { x=$somma(1,3); } Figura 23. Programma di esempio: 'somma’. Questo verrà letto come un carattere alla volta e una volta manipolato produrrà la seguente lista di token. [sub, somma, (, a, (,), b,), {, local, (, z,), (;), z, =, a, +, b, (;), return, (, z,), (;), }, main, (,), {, x, =, $, somma, (, 1, (,), 3,), (;), }] . Vediamo come. Si richiama il predicato toklist che ha due parametri File e Tl, rispettivamente il file da leggere e la lista da restituire. Toklist(File,Tl):- see(File),get0(C), doSent(C,Tl),seen. See(File) ridirige lo standard input in modo che il flusso di input sia il file File anzichè lo standard (la tastiera). Seen chiude la ridirezione creata da see e ripristina il flusso di input precedente, in questo caso la tastiera. Get0(C) prende un carattere dallo stream input e pone il suo valore nella variabile C. 36 doSent(C,Tl) prende il carattere di ingresso e restituisce la lista di token a partire da quel carattere fino alla fine del flusso di caratteri in ingresso, in questo caso fino alla fine del file. doSent(_,[]):- at_end_of_stream,!. doSent(Char,[W|W2]):- skip_blank(Char,X), readWord(X,W,C2), doSent(C2,W2). Il predicato doSent forma quindi la lista di token a partire dal carattere Char. Come prima cosa ignora tutti i caratteri di spaziatura tramite il predicato skip_blank(Char,X), dove Char è il carattere da analizzare ed X sarà il primo carattere utile che non sia una spaziatura (punteggiatura o alfanumerico). A questo punto a partire dal carattere X, il predicato readWord legge la parola W e ritorna C2 che è il carattere subito dopo la parola appena processata. E quindi ripete il tutto con il secondo doSent della riga che a partire dal carattere C2 ritorna il resto della lista dei token tramite un massiccio uso della ricorsione. A questo punto la lista completa dei token sarà la concatenazione del token W e della lista dei rimanenti token W2: [W|W2] che è il valore a toklist. blank_char(32). blank_char(8). blank_char(9). blank_char(10). blank_char(13). skip_blank(_,32):- at_end_of_stream,!. skip_blank(A,B):- blank_char(A),get0(V),skip_blank(V,B). skip_blank(A,B):- blank_char(A),get0(B). skip_blank(A,A). Vediamo quindi come readWord riconosce i token. Come detto nei capitoli precedenti il compilatore riconosce tre tipi di token: punteggiatura (o simboli), numeri (interi) e identificatori (alfanumerici) . La punteggiatura nel mio linguaggio prevede solo simboli a singolo o doppio carattere (altri linguaggi come ad esempio il php prevedono anche tre simboli) . Quindi c’è il rischio di sbagliare se per esempio incontriamo l’uguale (=). Come interpretarlo? Come operatore di assegnazione che ha un singolo simbolo o come il primo dell’operatore di comparazione che ha invece due simboli (==) ? La tecnica più comunemente adottata analizza prima la punteggiatura con il maggior numero di simboli, e se fallisce il pattern matching allora decrementa il numero di simboli da analizzare fino ad arrivare ad un solo simbolo. Così il mio compilatore prima analizza tutti i doppi simboli e poi in caso fa la stessa cosa con i singoli simboli. 37 double_char(33,W,R):peek_byte(61),get0(_),get0(R),name(W,[33,61]). % != double_char(43,W,R):peek_byte(43),get0(_),get0(R),name(W,[43,43]). % ++ double_char(45,W,R):peek_byte(45),get0(_),get0(R),name(W,[45,45]). % -double_char(45,W,R):peek_byte(62),get0(_),get0(R),name(W,[45,62]). % -> double_char(60,W,R):peek_byte(61),get0(_),get0(R),name(W,[60,61]). % <= double_char(61,W,R):peek_byte(61),get0(_),get0(R),name(W,[61,61]). % == double_char(62,W,R):peek_byte(61),get0(_),get0(R),name(W,[62,61]). % >= double_char(42,W,R):peek_byte(42),get0(_),get0(R),name(W,[42,42]). % ** Il predicato double_char analizza la punteggiatura a doppio carattere. Gli argomenti del predicato sono: il primo carattere della punteggiatura (scritto esplicitamente) come parametro di ingresso, mentre come parametri di uscita avremo il token W e il prossimo carattere da analizzare R. Notiamo che nel vedere se il secondo carattere appartiene alla punteggiatura doppia utilizziamo il predicato peek_byte della libreria dell’interprete Prolog e solo nel caso questo corrisponda allora lo preleviamo dall’input stream. Il predicato name anch’esso della libreria standard del Prolog ha due parametri X,L e ritorna vero se L è la lista dei caratteri ASCII che formano l’atomo X. Ad esempio: name(ab,[96,97]) è yes. Oppure name(ab,[X,Y]) da X=96 e Y=97, oppure name(X,[69,97]) da X=ab. single_char(33,W,R):single_char(34,W,R):single_char(35,W,R):single_char(36,W,R):single_char(37,W,R):single_char(38,W,R):single_char(39,W,R):single_char(40,W,R):single_char(41,W,R):single_char(42,W,R):single_char(43,W,R):single_char(44,W,R):single_char(45,W,R):single_char(46,W,R):single_char(47,W,R):single_char(91,W,R):single_char(92,W,R):single_char(93,W,R):single_char(94,W,R):single_char(58,W,R):single_char(59,W,R):single_char(60,W,R):single_char(61,W,R):- get0(R),name(W,[33]). get0(R),name(W,[34]). get0(R),name(W,[35]). get0(R),name(W,[36]). get0(R),name(W,[37]). get0(R),name(W,[38]). get0(R),name(W,[39]). get0(R),name(W,[40]). get0(R),name(W,[41]). get0(R),name(W,[42]). get0(R),name(W,[43]). get0(R),name(W,[44]). get0(R),name(W,[45]). get0(R),name(W,[46]). get0(R),name(W,[47]). get0(R),name(W,[91]). get0(R),name(W,[92]). get0(R),name(W,[93]). get0(R),name(W,[94]). get0(R),name(W,[58]). get0(R),name(W,[59]). get0(R),name(W,[60]). get0(R),name(W,[61]). % ! % " % # % $ % % % & % ' % ( %) % * % + % , % % . % / % [ % \ % ] % ^ %: % ; % < % = 38 single_char(62,W,R):- get0(R),name(W,[62]). % single_char(63,W,R):- get0(R),name(W,[63]). % single_char(123,W,R):- get0(R),name(W,[123]). single_char(125,W,R):- get0(R),name(W,[125]). > ? % { % } Mentre nei predicati che verificano la punteggiatura singola mettiamo il carattere del simbolo direttamente nella lista di un solo elemento in quanto ne verifichiamo l’appartenenza direttamente nella firma del predicato e ritorniamo il carattere successivo da analizzare R. Quindi la lettura del token eseguita dal predicato readWord relativo alla punteggiatura sarà: readWord(C,W,C2):- double_char(C,W,C2). readWord(C,W,C2):- single_char(C,W,C2). Come abbiamo detto tale predicato analizzerà prima l’appartenenza alla punteggiatura a doppio carattere e poi a un solo carattere. Vediamo ora come il compilatore riconosce i numeri e gli identificatori. Innanzi tutto dobbiamo istruire la base di conoscenza del Prolog e dirgli che cosa sono le cifre e i caratteri: isAlNum(C):- isNum(C). isAlNum(C):- isChar(C). isNum(X):- X>47,X<58. % 0-9 isChar(X):- X>64,X<91. % A-Z isChar(X):- X>96,X<123. % a-z Il predicato isNum(X) ritorna vero se l’argomento X è compreso tra 47 e 58 che sono i codici ASCII delle cifre zero e nove. Un carattere è definiti da isChar(X) e ritorna vero se X è compreso tra 64 e 91 o 96 e 123, ossia i codici ASCII delle lettere maiuscole e minuscole. Definiamo ora un alfanumerico in quanto un identificatore è un carattere seguito da zero o più alfanumerici: un alfanumerico è o un carattere o una cifra. I token che sono numeri sono riconosciuti da: readWord(C,W,C2):isNum(C),get0(C3),getNum(C3,W2,C2),name(W,[C|W2]). e getNum(A,[],A):- \+isNum(A). getNum(A,[A|Num],F):- isNum(A),get0(B),getNum(B,Num,F). Quindi partendo da readWord, se il primo carattere è una cifra allora prendi il secondo carattere C3. il predicato getNum ritorna la lista delle cifre eventuali adiacenti alla cifra C3 che formano il numero meno la prima cifra C. ed infine il 39 predicato name crea il token desiderato a partire dalla concatenazione della lista formata dalla prima cifra C e il resto della lista formante il numero ([C|W2]). La stessa cosa avviene per il riconoscimento degli identificatori. readWord(C,W,C2):isChar(C),get0(C3),getWord(C3,W2,C2),name(W,[C|W2]). e getWord(A,[],A):- \+isAlNum(A). getWord(A,[A|Word],F):- isAlNum(A),get0(B),getWord(B,Word,F). Viene preso il primo carattere che deve essere una lettera. Poi viene costruita la lista degli alfanumerici adiacenti al carattere iniziale e il predicato name ritorna l’identificatore a partire dalla concatenazione della lista che ha come head il carattere C e come tail la lista dei successivi alfanumerici W2: [C|W2]. 5.3 Il file ‘parser’ Il parser prende in ingresso la lista di token generata dal lexical analizer e costruisce la struttura che rappresenta l’albero di parsing. Il parser è stato scritto usando una definite clause grammar (DCG) che è una particolare sintassi riconosciuta dall’interprete Prolog con cui possiamo definire una grammatica velocemente e in maniera semplice. La sintassi della DCG è molto intuitiva ed è molto simile alla Backus-Naur Form, ed è particolarmente adatta a codificare semplici grammatiche di tipo context-free. Il simbolo di partenza della grammatica l’ho denominato iaz_proram ed è la prima dell’insieme delle produzioni dichiarate. Quindi per quanto visto nei primi capitoli, il linguaggio appartiene alla grammatica se partendo dal simbolo iniziale della grammatica, applicando le produzioni, otteniamo che nelle foglie dell’albero di parsing che ricordiamo è formato solo da simboli terminali, l’input del programma sorgente. parse(Token,Structure):- iaz_program(Structure,Token,[]),!. parse(_,_):- write('not a valid program'),nl,halt. Quindi il predicato parse prende come parametro di ingresso la lista dei token Token e se il linguaggio appartiene alla grammatica ritorna la rappresentazione dell’albero di parsing in Structure, altrimenti stampa a video il messaggio “not a valid program” e termina l’esecuzione del compilatore. iaz_program((S;Ss;Sss)) --> subp(S),mainp(Ss),subp(Sss). iaz_program((S;Ss;Sss)) --> subp(S),mainp(Ss),subp(Sss),[void]. 40 Ogni programma deve quindi avere la struttura di iaz_program e sarà composto da delle subroutine (che vedremo poi poter essere opzionali) da una funzione main, che è il punto di ingresso del programma e deve essere sempre presente, e chiude con altre subroutine eventuali. mainp((funz(main,[]);S;halt;end_funz)) --> [main],['('],[')'],statement(S). mainp((funz(main,[]);S;halt;end_funz)) --> [main],['('],[')'],statement(S),[void]. La funzione di ingresso è così definita: il simbolo terminale main, una coppia di parentesi tonde () (il mio linguaggio non prevede il passaggio di argomenti alla funzione di ingresso a differenza del linguaggio C in cui potevamo passargli le opzioni della riga di comando argv), e da statement eventuali che vedremo tra breve come sono definiti. In uscita il parser ritorna la struttura del relativo albero. Il predicato mainp ritorna alla struttura del parsing la funzione main con zero argomenti (funz(main,[])) una serie di statement eventuali (S), e la terminazione del programma (halt) e della funzione (end_funz) che abbiamo visto libera lo spazio che aveva allocato alle eventuali variabili locali. Quest’ultima cosa come vediamo non viene mai eseguita perchè il programma termina prima di giungere a tale codice. L’ho messo solamente per scopo didattico perchè ogni funzione al proprio termine del ciclo di vita deve liberare la memoria stack associata alle variabili locali. subp(void)-->[]. subp((funz(X1,X2);S;end_funz;Ss))--> [sub],identifier(X1),['('],listargs(X2),statement(S),subp(Ss). con listargs([])--> [')']. listargs([A|X])--> [','],identifier(A),listargs(X). listargs([A|X])--> identifier(A),listargs(X). identifier(X)-->[X],{atom(X)}. Qui sopra vediamo come vengono definite le subroutines. Possono non esserci nessuna subroutine (subp(void)-->[]), oppure se sono presenti sono così definite: simbolo terminale sub, identificatore del nome della subroutine, terminale apri parentesi tonda ‘(‘ una lista di argomenti eventuali (listargs), degli statement eventuali, e infine ricorsivamente zero o più subroutine (subp(Ss)). 41 Listargs come possiamo vedere mette in una lista Prolog tutti gli argomenti (che sono solo identificatori di variabili)passati alla funzione che sono intervallati da una virgola e termina con la chiusura della parentesi che indica la fine della lista di variabili. Alla struttura del parsing vengono passati: funz(X1,X2) dove X1 è il nome della subroutine mentre X2 è la lista degli eventuali argomenti, poi vengono passati gli statement della funzione (S), le operazioni della terminazione della funzione (end_funz), e le eventuali altre subroutine (Ss). Cominciamo a vedere i vari statement supportati dal mio linguaggio. Abbiamo lo statement vuoto: statement(void)-->[void]. e statement(void)-->[';']. Un blocco di statement racchiusi tra parentesi graffe è trattato come unico statement: statement((S;Ss))--> ['{'],statement(S),rest_statements(Ss). rest_statements((S;Ss))--> statement(S),rest_statements(Ss). rest_statements(void)-->['}']. rest_statements(void)-->[void]. Degli statement di assegnazione: statement(assign(X,V))--> identifier(X),['='],expression(V),[';']. statement(assign(p(X),V))--> ['*'],identifier(X),['='],expression(V),[';']. e expression(X)--> iaz_constant(X). expression(expr(Op,X,Y))--> iaz_constant(X), arithmetic_op(Op), expression(Y). arithmetic_op('+')-->['+']. arithmetic_op('-')-->['-']. arithmetic_op('*')-->['*']. arithmetic_op('/')-->['/']. string(mstring(X))--> identifier(X). iaz_constant(name(X))--> identifier(X). iaz_constant(number(X))--> iaz_integer(X). identifier2(X)--> identifier(X). identifier2(X)--> iaz_integer(X). identifier(X)-->[X],{atom(X)}. iaz_integer(X)-->[X],{integer(X)}. Il primo statement di assegnazione assegna li valore di una espressione alla variabile, mentre il secondo statement assegna il valore di una espressione all’indirizzo puntato dalla variabile. 42 Una espressione è definita ricorsivamente e può essere una costante (cioè un identificatore o un intero) o una operazione aritmetica tra una costante e una espressione. Viene ritornato alla struttura del parsing o la costante o expr(Op,X,Y) dove Op è l’operatore aritmetico, X è una costante e Y è una espressione codificata ricorsivamente di expr(…). Una semplice aritmetica dei puntatori: statement(assign(X,p1(name(V))))--> identifier(X),['='],['&'],identifier(V),[';']. statement(assign(X,p2(name(V))))--> identifier(X),['='],['*'],identifier(V),[';']. Lo statement per le chiamate a funzioni: statement(callx(X1,X2);assignret(R))--> identifier(R),['='],['$'],identifier(X1),['('],listargs2(X2),[';' ]. statement(callx(X1,X2))--> ['$'],identifier(X1),['('],listargs2(X2),[';']. e listargs2([])--> [')']. listargs2([A|X])--> [','],identifier2(A),listargs2(X). listargs2([A|X])--> identifier2(A),listargs2(X). identifier2(X)--> identifier(X). identifier2(X)--> iaz_integer(X). che ritorna alla struttura del parsing callx(X1,X2) dove X1 è il nome della funzione e X2 è la lista di eventuali parametri da passare alla funzione, e assignret(R) che assegna alla variabile R l’eventuale valore di ritorno della funzione. Il secondo statement di chiamata differisce dal primo solo per il fatto che ignoriamo l’eventuale parametro di ritorno. Notiamo che a differenza della dichiarazione delle funzioni in cui necessitavamo del predicato listagrs per la lista di parametri che accetta solo identificatori, qui abbiamo bisogno di un altro predicato in quanto nel richiamare una funzione, gli possiamo passare sia variabili sia numeri. Il predicato listargs2, a differenza di listagrs, accetta variabili e numeri. Costrutti condizionali e di loop: statement(if(T,S1,S2))--> [if],['('],test(T),[')'],statement(S1),[else],statement(S2). statement(if(T,S,void))--> [if],['('],test(T),[')'],statement(S). statement(while(T,S))--> [while],['('],test(T),[')'],statement(S). 43 e test(compare(Op,X,Y))->expression(X),comparison_op(Op),expression(Y). comparison_op('==')-->['==']. comparison_op('!=')-->['!=']. comparison_op('>')-->['>']. comparison_op('<')-->['<']. comparison_op('>=')-->['>=']. comparison_op('<=')-->['<=']. Gli statement di output: statement(write_crlf)-->[write],['('],[crlf],[')'],[';']. statement(write(X))--> [write],['('],expression(X),[')'],[';']. statement(write_str(X))--> [write],['('],['"'],string(X),['"'],[')'],[';']. statement(write_str(X))->[write_str],['('],['"'],string(X),['"'],[')'],[';']. dove il primo è il ritorno a capo, il secondo stampa a video il valore di una variabile in base decimale, il terzo e il quarto stampano a video una stringa puntata dalla variabile. Lo statement che ritorna un valore di una espressione da una funzione: statement(return(X))--> [return],['('],expression(X),[')'],[';']. Ed infine lo statement che dichiara variabili locali: statement(local(X))--> [local],['('],listargs(X),[';']. Che sfrutta il predicato listargs il quale raccoglie la lista di variabili che sono solo identificatori. Vediamo ora come viene codificata la struttura del programma somma di Figura 23. (funz(somma, [a, b]); (local([z]);assign(z, expr(+, name(a), name(b)));return(name(z));void);end_funz;void); (funz(main, []); ((callx(somma, [1, 3]);assignret(x));void);halt;end_funz);void Vediamo ora un programma più completo e vediamo di nuovo cosa produce il parser. sub foo() { return(5); } sub fattoriale(a) { local(x,y,z); if(a==1) return(1); z=a-1; y=$fattoriale(z); x=y*a; return(x); } main() { 44 local(x,z); write("prog1"); write(crlf); x=$fattoriale(3); write(x); write(crlf); if(x>100) { z=4; } else { z=$foo(); } while(z>0) { write(z); write(crlf); z=z-1; } } Figura 24. Programma di esempio: 'prog1' (fattoriale). darà (funz(foo, []); (return(number(5));void);end_funz;funz(fattoriale, [a]); (local([x, y, z]);if(compare(==, name(a), number(1)), return(number(1)), void);assign(z, expr(-, name(a), number(1))); (callx(fattoriale, [z]);assignret(y));assign(x, expr(*, name(y), name(a)));return(name(x));void);end_funz;void); (funz(main, []); (local([x, z]);write_str(mstring(prog1));write_crlf; (callx(fattoriale, [3]);assignret(x));write(name(x));write_crlf;if(compare(>, name(x), number(100)), (assign(z, number(4));void), ((callx(foo, []);assignret(z));void));while(compare(>, name(z), number(0)), (write(name(z));write_crlf;assign(z, expr(-, name(z), number(1)));void));void);halt;end_funz);void . 5.4 Il file ‘symtab’ In questo file sono presenti quattro predicati tre dei quali sono utilizzati dal file encode per la codifica dell’albero di parsing. tdstrip((Code1;Code2),(X,Y)):- tds(Code1,X),tds(Code2,Y). tds(no_op,''). tds(C,C). È l’unico a non essere utilizzato dall’encoder e serve solo a pulire il codice dai no_op (no operation) istruzioni che non fanno nulla. Migliora la leggibilità del codice. setlocalpar(X,Funz):- length(X,N), N1 is (N+1)*2,slp(X,Funz,N1,_). 45 slp([],_,N,N). slp([X|L],Funz,N,N2):- slp2(X,Funz,N,N1),slp(L,Funz,N1,N2). slp2(X,Funz,N,N1):- assert(lvar(X,Funz,N)), N1 is N-2. Il predicato setlocalpar costruisce una tabella in memoria relativa ai parametri passati alla funzione. La tabella di nome lvar ha tre parametri: in nome della variabile X, la funzione di riferimento Funz e l’indirizzo dello stack relativo al base pointer (BP). Siccome come abbiamo visto al cap 3, i parametri delle funzioni si trovano ad un indirizzo più grande del base pointer relativo alla funzione, viene calcolato lo spiazzamento massimo che verrà assegnato alla prima variabile della lista (N+1)*2, dove all’indirizzo BP della funzione viene sommato uno spiazzamento che è dovuto al fatto che sono presenti nello stack anche il BP e l’indirizzo di ritorno della funzione chiamante, e poi si moltiplica per 2 in quanto ogni variabile (sempre di tipo intero) necessita di due bytes. La tabella lvar viene riempita per ogni variabile e l’indirizzo relativo alle variabili viene decrementato 2 due ad ogni inserimento nella tabella. setlocalvar(X,Funz):- length(X,N), N1 is -N*2,slv(X,Funz,N1,_). slv([],_,N,N). slv([X|L],Funz,N,N2):- slv2(X,Funz,N,N1),slv(L,Funz,N1,N2). slv2(X,Funz,N,N1):- assert(lvar(X,Funz,N)), N1 is N+2. getlocalvar(_). Lo stesso discorso vale per le variabili locali che condividono la stessa tabella lvar. In questo caso però gli spiazzamenti relativi al BP sono negativi in quanto come abbiamo visto al cap 3 le variabili locali si trovano ad indirizzi più bassi rispetto al base pointer. Il calcolo del massimo spiazzamento viene fatto semplicemente moltiplicando il numero delle variabili per 2 (due bytes per ogni variabile) e preso in valore negativo (-N*2). Ad ogni inserimento in tabella di una variabile della lista l’indirizzo associato verrà incrementato di due. codepush([],_,no_op). codepush([X|L],Gv,(X1;X2)):codepush2(X,Gv,X1),codepush(L,Gv,X2). codepush2(X,_,(instr(loadc,X);instr(push,0))):- number(X). codepush2(X,Gv,(instr(load,Addr);instr(push,0))):lookup2(X,Gv,Addr,_,_). Il predicato codepush genera il codice che salva nello stack le variabili da passare alla relativa funzione prima di chiamarla. Per ogni elemento della lista di variabili se questa è un numero allora si carica nel registro accumulatore tale numero (loadc num) altrimenti se è un identificatore si 46 carica nell’accumulatore il valore di tale variabile (load Addr), ed infine si immette il valore desiderati nello stack (push). Per sapere il valore di una variabile si fa uso del predicato lookup2 che spiegherò tra brevissimo nel prossimo paragrafo. 5.5 Il file ‘encode’ Prima di vedere e di commentare il codice sorgente del file dell’encoder vorrei soffermarmi a spiegare il funzionamento degli alberi binari incompleti che è la tecnica utilizzata dal mio compilatore per la memorizzazzione e l’utilizzo delle variabili globali e nomi di funzioni. Introduciamo ora una struttura dati incompleta basata su dizionario di cui vediamo un codice di esempio: lookup(Key,[(Key,Value)|Dict],Value):- !. lookup(Key,[(Key1,Value1)|Dict],Value):Key\==Key1,lookup(Key,Dict,Value). Questo è un semplice database basato su dizionario, la variabile Dict che tiene traccia delle varie coppie Key e Value. Consideriamo la seguente istanza di Dict: [(mark,122),(john,101),(susy,222)|Xs]. Se noi interroghiamo il database secondo il dizionario Dict con la seguente query: la risposta sarà X=122. Possiamo inserire una nuova voce lookup(mark,Dict,X) nella tabella con la query: lookup(frank,Dict,333). Sintatticamente sembrerebbe cercare il valore associato alla key frank, ma gli effetti sono tuttaltri: la query ritorna true e la nuova istanza del dizionario [(mark,122),(john,101),(susy,222),(frank,333)|Xs] e Dict possiamo sarà quindi inserire una nuova voce nella tabella se la Key della query non è già presente, altrimenti possiamo ottenerne il valore associato. Consideriamo allora la query: lookup(john,Dict,102). Il valore dalla chiave è già presente ma il valore è diverso. In questo caso la query fallisce a causa della condizione Key\==Key1. Questo tipo di memorizzazione dati che sfrutta strutture dati incomplete viene utilizzato dal mio compilatore per le tabelle delle variabili globali e delle funzioni. Le due tabelle sono tenute separate per mezzo di dizionari diversi: per le variabili globali utiliziamo il dizionario Gv (global variable), mentre per le funzioni utiliziamo il dizionario Fv. 47 Nel codice utilizzato nel mio compilatore ho preferito adottare una variante del codice sopra descritto perchè se le tabelle diventano molto grandi diventa poco efficiente la ricerca di elementi in una lista lineare. Ho utilizzato quindi una variante che memorizza la tabella tramite un albero binario ordinato di cui vediamo qui sotto l’equivalente del codice visto per le liste lineari. Lookup(Key,dict(Key,X,Left,Right),Value):- !,X=Value. Lookup(Key,dict(Key1,X,Left,Right),Value):Key<Key1,lookup(Key,Left,Value). Lookup(Key,dict(Key1,X,Left,Right),value):Key>Key1,lookup(Key,Right,Value). Ma i risultati sono gli stessi. Il codice utilizzato nel mio compilatore per la gestione delle tabelle delle variabili globali e delle funzioni è il seguente: lookup(Key,dict(Key,X,Y,Z,_,_),Value,Type,Val):- !,X = Value,Y=Type,Z=Val. lookup(Key,dict(Key1,_,_,_,Left,_),Value,Type,Val):- Key@<Key1, lookup(Key,Left,Value,Type,Val). lookup(Key,dict(Key1,_,_,_,_,Right),Value,Type,Val):- Key@>Key1, lookup(Key,Right,Value,Type,Val). Infatti non essendoci nessuna dichiarazione di variabili globali ne di intestazioni di funzioni nel mio linguaggio, necessitavo di questo tipo di struttura per la loro gestione così potevo accedere al relativo valore in tabella se questo era presente oppure creava una nuova voce in caso contrario, con una sola chiamata del predicato senza dovermi preoccupare di verificare se la voce desiderata era già creata. Per le variabili locali e i parametri delle funzioni questo non andava bene perchè avendo una modalità di accesso e una ubicazione in memoria differente necessitavano di essere dichiarati prima e quindi vi accediamo tramite il predicato lvar creato da setlocalpar e setlocalvar in cui ritorna vero solo se la voce desiderata è presente, e falso altrimenti senza creare nessuna nuova voce implicitamente. Per automatizzare il processo di recupero delle informazioni relative alle variabili (globali o locali che siano) nonchè l’inserimento di nuove variabili globali usiamo il predicato lookup2. lookup2(Nome,_,stack_bp(Addr),nostr,_):- lvar(Nome,_,Addr). lookup2(Nome,Alf,Addr,Type,Val):- lookup(Nome,Alf,Addr,Type,Val). Questo semplicemente controlla nell’ordine prima la tabella delle variabili locali (lvar) e poi la tabella delle variabili globali (Gv) tramite lookup. 48 Mentre per le variabili globali il predicato ritorna l’indirizzo assoluto della locazione di memoria, per le variabili locali sarà ritornato stack_bp(Addr) dove Addr, già calcolato in tabella dal predicato setlocalvar, sarà uno spiazzamento positivo o negativo a seconda che si tratti di un parametro di funzione o variabile locale. Il predicato di ingresso dell’encoder è: encode2(X,Gv,Fv,(instr(jump,A);Y)):lookup(main,Fv,A,nostr,_),encode(X,Gv,Fv,Y). Come prima cosa crea nella tabella delle funzioni (Fv) la voce per la funzione main a cui associa l’indirizzo A che verrà risolto dall’assemblatore, e poi codifica il resto del programma ricorsivamente tramite il predicato encode. Quindi X è la struttura passategli dal parser Gv è la tabella delle variabili globali, Fv delle funzioni e Y è il codice rilocabile prodotto. La funzione encode2 ritorna Y alla cui testa pone un salto incondizionato al punto di ingresso del programma (main) perchè come abbiamo visto dall’analisi della grammatica la funzione di ingresso può anche non essere la prima nell’ordine. Il predicato encode quindi è nella forma encode (X,Gv,Fv,Y) dove X è l’input da codificare e Y è il codice codificato. encode((X;Xs),Gv,Fv,(Y;Ys)):- encode(X,Gv,Fv,Y), encode(Xs,Gv,Fv,Ys). Il primo encode che troviamo si richiama ricorsivamente in modo da poter processare un nodo della struttura dell’albero di parsing alla volta. encode(void,_,_,no_op). alla foglia vuota dell’albero corrisponderà nessuna operazione (no_op). encode(halt,_,_,halt). encode(write_crlf,_,_,instr(write_crlf,0)). Ad halt corrisponde halt. La stessa cosa con il ritorno a capo. encode(funz(Funz,X2),_,Fv,(label(Address);init_funz)):setlocalpar(X2,Funz),lookup(Funz,Fv,Address,nostr,_),assert(lvarn (0)). Ad una dichiarazione di funzione, ritorniamo un’etichetta (label) relativa al punto di ingresso della funzione e init_funz che come abbiamo visto nel cap 3 inizializza l’indirizzo dello stack per le eventuali variabili locali. Viene anche aggiornata la tabella delle variabili locali con gli eventuali parametri della funzione (setlocalpar), viene aggiornata la tabella delle funzioni con il nome 49 della funzione e la relativa label (lookup(Funz,Fv,Address,nostr,_)), e si suppone per il momento nessuna variabile locale (assert(lvanr(0))). encode(local(X),_,_,create_local(N1)):- length(X,N),N1 is N*2,setlocalvar(X,_),retract(lvarn(0)),assert(lvarn(N1)). Questo predicato sopra viene chiamato se si incontrano dichiarazioni di variabili locali e ritorna all’encoder create_local(N1) dove N1 è la dimensione in bytes per l’allocazione delle variabili nello stack. Il predicato aggiunge le voci necessarie alla tabella delle variabili locali tramite setlocalvar e aggiorna il predicato lvarn che tiene traccia della dimensione in bytes delle variabili. encode(end_funz,_,_,(destroy_local(X);end_funz)):lvarn(X),retractall(lvarn(_)),retractall(lvar(_,_,_)). Questo predicato sopra viene chiamato quanto incontriamo la fine di una funzione. All’encoder viene ritornato destroy_local(X) che come abbiamo visto cap 3 recupera lo spazio dallo stack delle variabili locali, e end_funz che recupera il base pointer della funzione chiamante. Il predicato cancella tutte le variabili dalla tabella della variabili locali lvar. encode(assign(E,Ex),Gv,_,(Code;instr(storep,Address))):E=..[p,R|_],lookup2(R,Gv,Address,nostr,0),encode_expression(Ex,Gv ,Code). encode(assign(Name,E),Gv,_,(Code;instr(store,Address))):E=..[p1,R|_],lookup2(Name,Gv,Address,nostr,0),encode_ptr1(R,Gv,Co de). encode(assign(Name,E),Gv,_,(Code;instr(store,Address))):E=..[p2,R|_],lookup2(Name,Gv,Address,nostr,0),encode_ptr2(R,Gv,Co de). encode(assign(Name,E),Gv,_,(Code;instr(store,Address))):lookup2(Name,Gv,Address,nostr,0),encode_expression(E,Gv,Code). I predicati sopra elencati codificano le assegnazioni di espressioni a variabili. I primi tre sono dedicati ai puntatori mentre l’ultimo alle variabili semplici. Vediamo come funziona la codifica per le assegnazioni a variabili semplici visto che i principi sono gli stessi anche per i puntatori. assign(Name,E) assegna a Name il valore dell’espressione E. Come prima cosa recuperiamo l’indirizzo della variabile tramite il predicato lookup2 codifichiamo la sequenza di operazioni per risolvere l’espressione tramite encode_expression(E,Gv,Code) dove Code sarà tale codice. 50 Il predicato ritorna all’encoder la sequenza di istruzione per risolvere l’espressione seguito dall’assegnazione di tale valore (che stà implicito nel registro accumulatore) nella locazione della variabile (store Address). Riportiamo di seguito i predicati che generano la sequenza di codice per la risoluzione delle espressioni: encode_expression(mstring(X),Gv,instr(loadc,Address)):lookup2(X,Gv,Address,mstring,X). encode_expression(number(C),_,instr(loadc,C)). encode_expression(name(X),Gv,instr(load,Address)):lookup2(X,Gv,Address,nostr,_). encode_expression(expr(Op,E1,E2),Gv,(Load;Instruction)):single_instruction(Op,E2,Gv,Instruction),encode_expression(E1,Gv, Load). encode_expression(expr(Op,E1,E2),Gv,Code):\+single_instruction(Op,E2,Gv,_),single_operation(Op,E1,Gv,E2Code ,Code),encode_expression(E2,Gv,E2Code). single_instruction(Op,number(C),_,instr(OpCode,C)):literal_operation(Op,OpCode). single_instruction(Op,name(X),Gv,instr(OpCode,A)):memory_operation(Op,OpCode),lookup2(X,Gv,A,_,_). single_operation(Op,E,Gv,Code,(Code;Instruction)):commutative(Op),single_instruction(Op,E,Gv,Instruction). single_operation(Op,E,Gv,Code,(Code;instr(store,Address);Load;ins tr(OpCode,Address))):\+commutative(Op),lookup('$temp',Gv,Address,_,_),encode_expressio n(E,Gv,Load),op_code(E,Op,OpCode). op_code(number(_),Op,OpCode):- literal_operation(Op,OpCode). op_code(name(_),Op,OpCode):- memory_operation(Op,OpCode). literal_operation('+',addc). literal_operation('-',subc). literal_operation('*',mulc). literal_operation('/',divc). memory_operation('+',add). memory_operation('-',sub). memory_operation('*',mul). memory_operation('/',div). commutative('+'). commutative('*'). Di seguito riportiamo i predicati relativi ai costrutti condizionali e ai loop: encode(if(Test,Then,Else),Gv,Fv,(TestCode;ThenCode;instr(jump,L2) ;label(L1);ElseCode;label(L2))):encode_test(Test,L1,Gv,TestCode),encode(Then,Gv,Fv,ThenCode),enco de(Else,Gv,Fv,ElseCode). encode(while(Test,Do),Gv,Fv,(label(L1);TestCode;DoCode;instr(jump ,L1);label(L2))):- encode_test(Test,L2,Gv,TestCode), encode(Do,Gv,Fv,DoCode). e 51 encode_test(compare(Op,E1,E2),Label,Gv,(Code;instr(OpCode,Label)) ):- comparison_opcode(Op,OpCode),encode_expression(expr('',E1,E2),Gv,Code). comparison_opcode('==',jumpne). comparison_opcode('>',jumple). comparison_opcode('<',jumpge). comparison_opcode('!=',jumpeq). comparison_opcode('>=',jumplt). comparison_opcode('<=',jumpgt). La codifica del costrutto condizionale if viene codificato con un test sulla condizione, se questa è vera esegue ThenCode altrimenti esegue ElseCode. Anche per il costrutto di loop la codifica ritornata all’encode è molto chiara: finche la condizione Test è vera esegue il codice compreso tra label(L1) e instr(jump,L1) altrimenti salta al punto di uscita L2. L’output su schermo viene codificato semplicemente come vediamo dal codice sottostante: encode(write(mstring(E)),Gv,_,(instr(loadc,Address);instr(write_s tr,0))):name(E,W),append(W,[36],T),name(E2,T),lookup2(E2,Gv,Address,mstri ng,E2). encode(write(E),Gv,_,(Code;instr(write,0))):encode_expression(E,Gv,Code). encode(write_str(mstring(E)),Gv,_,(instr(loadc,Address);instr(wri te_str,0))):name(E,W),append(W,[36],T),name(E2,T),lookup2(E2,Gv,Address,mstri ng,E2). Se l’elemento da stampare è una stringa allora appendi il carattere dollaro alla fine della stringa (perchè nel codice macchina intel 80x86 utiliziamo l’interrupt 21 del dos che stampa a video una stringa terminante con il carattere dollaro) aggiorniamo le tabelle delle variabili con lookup2. in uscita all’encoder avremo lo pseudocodice loadc Address e write_str che stamperà il contenuto del puntatore Address. Se invece desideriamo stampare il valore di una variabile o di una espressione allora risolviamo tale valore tramite il predicato encode_expression a cui apponiamo in coda la pseudoistruzione per la stampa del valore di una variabile write. Per la chiamata a funzione utiliziamo il seguente predicato. encode(callx(X1,X2),Gv,Fv,(Codepush;instr(call,A);destroy_local(N ))):- lookup(X1,Fv,A,_,_),length(X2,M),N is M*2,codepush(X2,Gv,Codepush). Questo come prima cosa risolve il nome della funzione tramite la tabella delle funzioni (Fv) e calcolala lunghezza in bytes dei parametri da passare alla funzione (N). 52 Il predicato ritorna all’encoder la serie di istruzioni che mettono i parametri nello stack (codepush come abbiamo visto nella sezione precedente), la chiamata alla funzione (instr(call,A)) ed infine la funzione chiamante deve liberare lo spazio dello stack precedentemente assegnato ai parametri della funzione tramite destroy_local(N). Qualora la funzione debba ritornare un valore il seguente predicato viene invocato: encode(return(X),Gv,_,(Code;destroy_local(N1);end_funz)):encode_expression(X,Gv,Code),lvarn(N1). In tale predicato si risolve il valore dell’espressione da ritornare, si distruggono le variabili locali della funzione e si ripristina il valore del BP della funzione chiamante. Una volta tornati alla funzione chiamante il valore viene poi assegnato alla variabile tramite il seguente predicato: encode(assignret(X),Gv,_,(instr(store,Address))):lookup2(X,Gv,Address,nostr,0). Si risolve l’indirizzo associato alla variabile nella tabella della variabili (lookup2) e si assegna finalmente il valore a tale indirizzo (store Address). Vediamo ora come l’encoder codifica l’albero di parsing rispettivamente per i programmi somma e fattoriale di Figura 23 e 24: instr(jump, _G1053); ((label(_G1071);init_funz); (create_local(2); ((instr(load, stack_bp(6));instr(add, stack_bp(4)));instr(store, stack_bp(-2))); (instr(load, stack_bp(-2));destroy_local(2);end_funz);no_op); (destroy_local(2);end_funz);no_op); ((label(_G1053);init_funz); (((((instr(loadc, 1);instr(push, 0)); (instr(loadc, 3);instr(push, 0));no_op);instr(call, _G1071);destroy_local(4));instr(store, _G1250));no_op);halt;destroy_local(0);end_funz);no_op e instr(jump, _G2182); ((label(_G2200);init_funz); ((instr(loadc, 5);destroy_local(0);end_funz);no_op); (destroy_local(0);end_funz); (label(_G2254);init_funz); (create_local(6); (((instr(load, stack_bp(4));instr(subc, 1));instr(jumpne, _G2337)); (instr(loadc, 1);destroy_local(6);end_funz);instr(jump, _G2332);label(_G2337);no_op;label(_G2332)); ((instr(load, stack_bp(4));instr(subc, 1));instr(store, stack_bp(-2))); ((((instr(load, stack_bp(-2));instr(push, 0));no_op);instr(call, _G2254);destroy_local(2));instr(store, stack_bp(-4))); ((instr(load, stack_bp(-4));instr(mul, stack_bp(4)));instr(store, stack_bp(-6))); (instr(load, stack_bp(6));destroy_local(6);end_funz);no_op); (destroy_local(6);end_funz);no_op); ((label(_G2182);init_funz); (create_local(4); (instr(loadc, _G2551);instr(write_str, 53 0));instr(write_crlf, 0); ((((instr(loadc, 3);instr(push, 0));no_op);instr(call, _G2254);destroy_local(2));instr(store, stack_bp(-4))); (instr(load, stack_bp(-4));instr(write, 0));instr(write_crlf, 0); (((instr(load, stack_bp(4));instr(subc, 100));instr(jumple, _G2677)); ((instr(loadc, 4);instr(store, stack_bp(-2)));no_op);instr(jump, _G2672);label(_G2677); (((no_op;instr(call, _G2200);destroy_local(0));instr(store, stack_bp(2)));no_op);label(_G2672)); (label(_G2750); ((instr(load, stack_bp(-2));instr(subc, 0));instr(jumple, _G2764)); ((instr(load, stack_bp(-2));instr(write, 0));instr(write_crlf, 0); ((instr(load, stack_bp(-2));instr(subc, 1));instr(store, stack_bp(-2)));no_op);instr(jump, _G2750);label(_G2764));no_op);halt;destroy_local(4);end_funz);no_ op 5.6 Il file ‘toIntel’ Il file toIntel raccoglie output dell’encoder che è pseudoassembler rilocabile e lo trasforma in codice sorgente assembler per processori intel 80x86. Il codice generato è ancora di tipo rilocabile, cioè fa riferimento alla tabella delle variabili anzichè ai valori assoluti di zone di memoria che sono ancora da assegnare. Ad ogni pseudo istruzione in ingresso si faranno corrispondere in uscita una o più istruzioni intel 80x86. Il predicato di ingresso ha lo stesso nome del file di appartenenza toIntel. toIntel(Code,XCode2):- adj(Code,XCode),toIntel2(XCode,XCode2). adj(X,instr(push,cs);instr(pop,ds);X). Come prima cosa il predicato toIntel chiama il predicato adj che tramite le istruzione (push cs e pop ds) poste in cima al codice, assicura che il segmento dati (DS) e il segmento codice (CS) corrispondano. Ricordiamo che questo è necessario per il tipo di file prodotto dal mio compilatore. Successivamente viene richiamato ricorsivamente il predicato toIntel2 che converte tutto lo pseudo assembler in linguaggio assembler per processori della famiglia intel 80x86. toIntel2((Code1;Code2),(X1;X2)):toIntel2(Code1,X1),toIntel2(Code2,X2). Il codice scritto sopra richiama ricorsivamente il predicato toIntel2 in modo da processare una pseudo istruzione alla volta. Il seguente predicato viene chiamato nel caso si voglia conoscere l’indirizzo di una variabile locale, e tale valore è da ricercare nello stack tramite il base pointer. 54 toIntel2(instr(loadc,stack_bp(A)),(mov(ax,bp);add(ax,A))):number(A). Il predicato carica nel registro accumulatore il valore dell’indirizzo cercato. Il seguente predicato carica nell’accumulatore il valore della variabile locale desiderata: toIntel2(instr(load,stack_bp(A)),(mov(bx,bp);add(bx,A);mov(ax,[bx ]))):- number(A). dove A è un intero positivo o negativo ed è lo spiazzamento da sommare al registro base pointer (BP). Il seguente è lo stesso di quello sopra visto con la differenza che si lavora con variabili puntatori anzichè intere: toIntel2(instr(loadp,stack_bp(A)),(mov(ax,bp);add(ax,A);mov(bx,ax );mov(ax,[bx]);mov(bx,ax);mov(ax,[bx]))):- number(A). Il seguente predicato inserisce nella variabile locale il valore del registro accumulatore: toIntel2(instr(store,stack_bp(A)),(mov(bx,bp);add(bx,A);mov([bx], ax))):- number(A). I seguenti predicati convertono nell’ordine le seguenti pseudoistruzioni: addizionare il contenuto dell’accumulatore con l’indirizzo della variabile locale, addizionare il contenuto dell’accumulatore con il valore della variabile locale, sottrarre il contenuto dell’accumulatore con l’indirizzo della variabile locale, sottrarre il contenuto dell’accumulatore con il valore della variabile locale, moltiplicare il valore dell’accumulatore con l’indirizzo della variabile locale, moltiplicare il valore dell’accumulatore con il valore della variabile locale, dividere il valore dell’accumulatore con l’indirizzo della variabile locale, ed infine dividere il valore dell’accumulatore con il valore della variabile locale. toIntel2(instr(addc,stack_bp(A)),(mov(bx,bp);add(bx,A);add(ax,bx) )):- number(A). toIntel2(instr(add,stack_bp(A)),(mov(bx,bp);add(bx,A);add(ax,[bx] ))):- number(A). toIntel2(instr(subc,stack_bp(A)),(mov(bx,bp);add(bx,A);sub(ax,bx) )):- number(A). toIntel2(instr(sub,stack_bp(A)),(mov(bx,bp);add(bx,A);sub(ax,[bx] ))):- number(A). toIntel2(instr(mulc,stack_bp(A)),(mov(bx,bp);add(bx,A);mov(dx,0); instr(mul,bx))). toIntel2(instr(mul,stack_bp(A)),(instr(push,ax);mov(bx,bp);add(bx ,A);mov(ax,[bx]);mov(bx,ax);mov(dx,0);instr(pop,ax);instr(mul,bx) )):- number(A). 55 toIntel2(instr(divc,stack_bp(A)),(mov(bx,bp);add(bx,A);mov(dx,0); instr(div,bx))):- number(A). toIntel2(instr(div,stack_bp(A)),(instr(push,ax);mov(bx,bp);add(bx ,A);mov(ax,[bx]);mov(bx,ax);mov(dx,0);instr(pop,ax);instr(div,bx) )):- number(A). dove A è di tipo intero positivo o negativo ed è lo spiazzamento da sommare al registro base pointer (BP). I seguenti predicati convertono nell’ordine le seguenti pseudoistruzioni: carica nel registro accumulatore l’indirizzo di una variabile globale, carica nel registro accumulatore il valore di una variabile globale. toIntel2(instr(loadc,A),mov(ax,A)). toIntel2(instr(load,A),mov(ax,[A])). dove A è l’indirizzo di memoria della variabile. I seguenti predicati convertono in assembler intel l’istruzione di salto incondizionato e l’istruzione di chiamata a funzione: toIntel2(instr(jump,A),instr(jmp,A)). toIntel2(instr(call,A),instr(call,A)). dove A è un indirizzo di memoria. I seguenti predicati, dove A rappresenta un indirizzo di memoria relativo ad una variabile globale, convertono in assembler intel le seguenti istruzioni nell’ordine: caricare nel registro accumulatore il valore della locazione di memoria puntata dal contenuto della variabile, inserire nella locazione di memoria della variabile il valore dell’accumulatore, ed infine inserire nella locazione di memoria puntata dal valore della variabile il valore dell’accumulatore. toIntel2(instr(loadp,A),(mov(bx,[A]);mov(ax,[bx]))). toIntel2(instr(store,A),mov([A],ax)). toIntel2(instr(storep,A),(mov(dx,ax);mov(bx,A);mov(ax,[bx]);instr (push,ax);instr(pop,bx);mov(ax,dx);mov([bx],ax))). Il seguente predicato inserisce nello stack il valore dell’accumulatore: toIntel2(instr(push,0),instr(push,ax)). I seguenti predicati convertono semplici operazioni aritmetiche tra accumulatore ed indirizzo di una variabile globale. Il significato è identico alle operazione eseguite dalle variabili locali. toIntel2(instr(addc,A),add(ax,A)). toIntel2(instr(add,A),add(ax,[A])). toIntel2(instr(subc,A),sub(ax,A)). toIntel2(instr(sub,A),sub(ax,[A])). toIntel2(instr(mulc,A),(mov(dx,0);mov(bx,A);instr(mul,bx))). toIntel2(instr(mul,A),(mov(dx,0);mov(bx,[A]);instr(mul,bx))). 56 toIntel2(instr(divc,A),(mov(dx,0);mov(bx,A);instr(div,bx))). toIntel2(instr(div,A),(mov(dx,0);mov(bx,[A]);instr(div,bx))). Il seguente predicato converte in assembler intel l’istruzione di stampa a video del valore di una variabile. toIntel2(instr(write,_),(mov(bx,10);xor(cx,cx);label(W1);xor(dx,d x);instr(div,bx);instr(push,dx);instr(inc,cx);or(ax,ax);instr(jum pne,W1);label(W2);instr(pop,dx);add(dl,48);mov(ah,2);instr(int,33 );instr(loop,W2))). Il principio su cui si basa è molto semplice. Tramite una seria di divisioni successive per 10 (perchè vogliamo l’output in formato decimale) finche il dividendo è maggiore di zero, mettiamo il resto della divisione nello stack. A questo punto ci troveremo nello stack il numero da stampare cifra per cifra dalla più significativa a quella meno significativa, che è proprio l’ordine in cui dobbiamo stamparle. Ora con una serie di prelievi dallo stack stampiamo una cifra alla volta. Il seguente predicato stampa a video una stringa: toIntel2(instr(write_str,_),(mov(dx,ax);mov(ah,9);instr(int,33))) . Basterà porre nel registro dx l’indirizzo di memoria da cui inizia la stringa da stampare (che ricordiamo deve terminare con un carattere dollaro) e poi fare una chiamata a funzione del sistema operativo MS-DOS tramite l’apposito int21h. Ed infine le conversione necessarie alla gestione delle variabili locali nello stack nonchè le istruzioni da eseguire ad ogni inizio e fine di una funzione sono già state ampiamente spiegate nel cap 3: toIntel2(destroy_local(N),add(sp,N)). toIntel2(end_funz,(instr(pop,bp);instr(ret,0))). toIntel2(create_local(N),sub(sp,N)). toIntel2(init_funz,(instr(push,bp);mov(bp,sp))). L’operazione di terminazione del programma halt viene tradotta anch’essa tramite una chiamata a funzione del sistema operativo MS-DOS: l’int 20h che termina immediatamente l’esecuzione del processo in corso. toIntel2(halt,instr(int,32)). Vediamo qui di seguito l’output del predicato toIntel rispettivamente per i programmi somma e fattoriale di Figura 23 e 24: instr(push, cs);instr(pop, ds);instr(jmp, _G1164); ((label(_G1182);instr(push, bp);mov(bp, sp)); (sub(sp, 2); (((mov(bx, bp);add(bx, 6);mov(ax, [bx]));mov(bx, bp);add(bx, 4);add(ax, [bx]));mov(bx, bp);add(bx, -2);mov([bx], ax)); ((mov(bx, bp);add(bx, -2);mov(ax, [bx]));add(sp, 2);instr(pop, 57 bp);instr(ret, 0));no_op); (add(sp, 2);instr(pop, bp);instr(ret, 0));no_op); ((label(_G1164);instr(push, bp);mov(bp, sp)); (((((mov(ax, 1);instr(push, ax)); (mov(ax, 3);instr(push, ax));no_op);instr(call, _G1182);add(sp, 4));mov([_G1361], ax));no_op);instr(int, 32);add(sp, 0);instr(pop, bp);instr(ret, 0));no_op e instr(push, cs);instr(pop, ds);instr(jmp, _G2293); ((label(_G2311);instr(push, bp);mov(bp, sp)); ((mov(ax, 5);add(sp, 0);instr(pop, bp);instr(ret, 0));no_op); (add(sp, 0);instr(pop, bp);instr(ret, 0)); (label(_G2365);instr(push, bp);mov(bp, sp)); (sub(sp, 6); ((((mov(bx, bp);add(bx, 4);mov(ax, [bx]));sub(ax, 1));instr(jumpne, _G2448)); (mov(ax, 1);add(sp, 6);instr(pop, bp);instr(ret, 0));instr(jmp, _G2443);label(_G2448);no_op;label(_G2443)); (((mov(bx, bp);add(bx, 4);mov(ax, [bx]));sub(ax, 1));mov(bx, bp);add(bx, 2);mov([bx], ax)); (((((mov(bx, bp);add(bx, -2);mov(ax, [bx]));instr(push, ax));no_op);instr(call, _G2365);add(sp, 2));mov(bx, bp);add(bx, -4);mov([bx], ax)); (((mov(bx, bp);add(bx, -4);mov(ax, [bx]));instr(push, ax);mov(bx, bp);add(bx, 4);mov(ax, [bx]);mov(bx, ax);mov(dx, 0);instr(pop, ax);instr(mul, bx));mov(bx, bp);add(bx, -6);mov([bx], ax)); ((mov(bx, bp);add(bx, -6);mov(ax, [bx]));add(sp, 6);instr(pop, bp);instr(ret, 0));no_op); (add(sp, 6);instr(pop, bp);instr(ret, 0));no_op); ((label(_G2293);instr(push, bp);mov(bp, sp)); (sub(sp, 4); (mov(ax, _G2662);mov(dx, ax);mov(ah, 9);instr(int, 33)); (mov(dl, 13);mov(ah, 2);instr(int, 33);mov(dl, 10);mov(ah, 2);instr(int, 33)); ((((mov(ax, 3);instr(push, ax));no_op);instr(call, _G2365);add(sp, 2));mov(bx, bp);add(bx, 4);mov([bx], ax)); ((mov(bx, bp);add(bx, -4);mov(ax, [bx]));mov(bx, 10);cx xor cx;label(_G3588);dx xor dx;instr(div, bx);instr(push, dx);instr(inc, cx);or(ax, ax);instr(jumpne, _G3588);label(_G3629);instr(pop, dx);add(dl, 48);mov(ah, 2);instr(int, 33);instr(loop, _G3629)); (mov(dl, 13);mov(ah, 2);instr(int, 33);mov(dl, 10);mov(ah, 2);instr(int, 33)); ((((mov(bx, bp);add(bx, -4);mov(ax, [bx]));sub(ax, 100));instr(jumple, _G2788)); ((mov(ax, 4);mov(bx, bp);add(bx, 2);mov([bx], ax));no_op);instr(jmp, _G2783);label(_G2788); (((no_op;instr(call, _G2311);add(sp, 0));mov(bx, bp);add(bx, 2);mov([bx], ax));no_op);label(_G2783)); (label(_G2861); (((mov(bx, bp);add(bx, -2);mov(ax, [bx]));sub(ax, 0));instr(jumple, _G2875)); (((mov(bx, bp);add(bx, -2);mov(ax, [bx]));mov(bx, 10);cx xor cx;label(_G3895);dx xor dx;instr(div, bx);instr(push, dx);instr(inc, cx);or(ax, ax);instr(jumpne, _G3895);label(_G3936);instr(pop, dx);add(dl, 48);mov(ah, 2);instr(int, 33);instr(loop, _G3936)); (mov(dl, 13);mov(ah, 2);instr(int, 33);mov(dl, 10);mov(ah, 2);instr(int, 33)); (((mov(bx, bp);add(bx, -2);mov(ax, [bx]));sub(ax, 1));mov(bx, bp);add(bx, -2);mov([bx], ax));no_op);instr(jmp, _G2861);label(_G2875));no_op);instr(int, 32);add(sp, 4);instr(pop, bp);instr(ret, 0));no_op 5.7 Il file ‘assemble’ L’assemblatore. 58 L’assemblatore ha lo scopo di risolvere tutte le occorrenze in cui vi sia riferimento ad una variabile o ad una zona di memoria. Come input tutti i nomi delle variabili globali e delle funzioni fanno riferimento alle relative tabelle di simboli. E tutte le istruzioni di salto fanno ancora riferimento ad etichette generiche che devono essere ancora assegnate a locazioni corrette di memoria. L’assemblatore deve quindi sapere come viene mappato il file in memoria, qual’è l’indirizzo di partenza e il peso di ciascuna istruzione del codice. Con pesi di una istruzione intendo il numero di bytes necessari per la codifica ti tale istruzione in codice macchina. Una volta a conoscenza di questi dati, l’assemblatore è in grado di risolvere tutte le occorrenze di variabili, funzioni e etichette. In ingresso l’assemblatore accetta codice generato dall’encoder (encode) o dalla conversione in assembler intel (toIntel) e in uscita produce il codice sia pseudo che intel mappato coi giusti riferimenti delle variabili, funzioni e label a cui aggiunge in coda le istruzione di allocazione per variabili globali interi o stringhe che siano. L’assemblatore ha due porzioni di codice separate. La prima si riferisce all’assemblaggio di pseudo codice preso dall’encoder, la seconda allo pseudocodice convertito tramite il predicato toIntel. L’assemblatore ha quindi due predicati di ingresso: assemble che si riferisce allo pseudocodice e assemble2 che assembla codice intel. La differenza principale tra i due assemblatori è che tutte le istruzione dello pseudocodice hanno pesi di un byte, mentre le istruzioni intel possono avere pesi che variano tra uno e quattro bytes. Analizziamo gli assemblatori separatamente partendo da quello che assembla lo pseudocodice (assemble). assemble(Code,D,_,TidyCode;Db;block(L)):tidy_and_count(Code,1,N,TidyCode),N1 is N, allocate(D,N1,N2),L is N2-N1+1,getCode(D,Db),!. Come si vede tidy_and_count l’assemblaore di pseudocodice richiama tre predicati: che conta il numero di bytes dell’intero programma (N bytes) in base al peso delle singole istruzioni e risolvendo i valori delle etichette e delle funzioni, allocate che assegna alle variabili globali i giusti riferimenti in memoria, ed infine getCode che letteralmente attacca lo spazio per le variabili globali in coda al codice eventualmente assegnandogli il corretto valore di partenza. 59 Andiamo a vedere nel dettaglio questi tre predicati. tidy_and_count((Code1;Code2),M,N,SS;SS1):tidy_and_count(Code1,M,M1,SS),tidy_and_count(Code2,M1,N,SS1). tidy_and_count si richiama ricorsivamente in modo da processare una istruzione alla volta. M è l’indirizzo di partenza che essendo un linguaggio astratto che non può essere eseguito da nessuna macchina pongo uguale ad 1 per una buona legibilità del codice, N è il valore dell’ultimo byte di codice +1. tidy_and_count(instr(X,Y),N,N1,instr(X,Y)):- N1 is N+1. tidy_and_count(nop,N,N1,nop):- N1 is N+1. tidy_and_count(init_funz,N,N1,init_funz):- N1 is N+1. tidy_and_count(create_local(X),N,N1,create_local(X)):- N1 is N+1. tidy_and_count(destroy_local(X),N,N1,destroy_local(X)):- N1 is N+1. tidy_and_count(end_funz,N,N1,end_funz):- N1 is N+1. tidy_and_count(halt,N,N1,halt):- N1 is N+1. Lo pseudo codice ha poche istruzioni appartenenti al suo linguaggio e la maggiorparte elencata qui sopra ha peso 1. La pseudo istruzione label ha peso zero, cioè non è considerata nel processo di assemblaggio in quanto il valore del secondo argomento e quello del terzo argomento del predicato hanno lo stesso valore, cioè ha una codifica di zero bytes. Il predicato tidy_and_count applicato a tale pseudo istruzione risolve il valore dell’etichetta: tidy_and_count(label(N),N,N,no_op). L’istruzione no_op ha anch’essa peso zero (secondo e terzo parametro sono uguali) e viene semplicemente ignorata: tidy_and_count(no_op,N,N,no_op). Il predicato allocate visita l’albero binario che rappresenta la tabella delle variabili globali, che ricordiamo è ordinato, ed assegna loro i valori della locazione di memoria a partire dal byte successivo all’ultima istruzione del codice assemblato incrementando il valore da assegnare alla successiva variabile dell’albero di 2 bytes se si tratta di una variabile intera o puntatore, della lunghezza della stringa se la voce della tabella è una stringa. allocate(void,N,N). allocate(dict(_,N1,mstring,V,Before,After),N0,N):strlen(V,A),allocate(Before,N0,N1),N2 is N1+A,allocate(After,N2,N). allocate(dict(_,N1,_,_,Before,After),N0,N):allocate(Before,N0,N1), N2 is N1+1, allocate(After,N2,N). strlen(X,Y):- name(X,F),length(F,Y). 60 Infine il predicato getCode, che è lo stesso per il processo di assemblaggio di pseudo codice e codice intel, rivisita nuovamente l’albero binario ordinato delle variabili globali e per ogni voce aggiunge in code al codice una istruzione che riserva tale locazione di memoria alla variabile, istanziandola nel caso al valore corretto. Il codice per gli interi che sono 2 bytes (1 parola) è dw: define word. Il codice il byte è: db, define byte. Il codice per le stringhe è: ds, define string. getCode(void,instr(db,0)). getCode(dict(_,_,mstring,V,void,void),instr(ds,V)). getCode(dict(_,_,nostr,V,void,void),instr(dw,V)). getCode(dict(_,_,mstring,V,void,After),(instr(ds,V);Db)):getCode(After,Db). getCode(dict(_,_,nostr,V,void,After),(instr(dw,V);Db)):getCode(After,Db). getCode(dict(_,_,mstring,V,Before,void),(Db;instr(ds,V))):getCode(Before,Db). getCode(dict(_,_,nostr,V,Before,void),(Db;instr(dw,V))):getCode(Before,Db). getCode(dict(_,_,mstring,V,Before,After),(Db1;instr(ds,V);Db2)):getCode(Before,Db1),getCode(After,Db2). getCode(dict(_,_,nostr,V,Before,After),(Db1;instr(dw,V);Db2)):getCode(Before,Db1),getCode(After,Db2). Il codice per l’assemblaggio di un sorgente per processori intel percorre gli stessi passi logici di quelli visti fino a qui dal predicato assemble. Tuttavia si utilizzano i predicati: assemble2, tidy_and_count2, allocate2, mentre getCode rimane invariato. assemble2(Code,Gv,_,TidyCode;Db;block(L)):tidy_and_count2(Code,256,N,TidyCode),N1 is N, allocate2(Gv,N1,N2),L is N2-N1+1,getCode(Gv,Db),!. Questi nuovi predicati riconoscono le parole del linguaggio intel e danno il giusto peso alle singole istruzioni. tidy_and_count2((Code1;Code2),M,N,SS;SS1):tidy_and_count2(Code1,M,M1,SS),tidy_and_count2(Code2,M1,N,SS1). tidy_and_count2(instr(push,X),N,N1,instr(push,X)):- N1 is N+1. tidy_and_count2(instr(pop,X),N,N1,instr(pop,X)):- N1 is N+1. tidy_and_count2(instr(int,X),N,N1,instr(int,X)):- N1 is N+2. tidy_and_count2(instr(jmp,X),N,N1,instr(jmp,X-N1)):- N1 is N+3. tidy_and_count2(instr(call,X),N,N1,instr(call,X-N1)):- N1 is N+3. tidy_and_count2(instr(loop,X),N,N1,instr(loop,X-N1)):- N1 is N+2. tidy_and_count2(instr(jumpne,X),N,N1,instr(jumpne,X-N1)):- N1 is N+2. tidy_and_count2(instr(jumple,X),N,N1,instr(jumple,X-N1)):- N1 is N+2. tidy_and_count2(instr(jumpge,X),N,N1,instr(jumpge,X-N1)):- N1 is N+2. 61 tidy_and_count2(instr(jumpeq,X),N,N1,instr(jumpeq,X-N1)):- N1 is N+2. tidy_and_count2(instr(jumplt,X),N,N1,instr(jumplt,X-N1)):- N1 is N+2. tidy_and_count2(instr(jumpgt,X),N,N1,instr(jumpgt,X-N1)):- N1 is N+2. tidy_and_count2(instr(ret,A),N,N1,instr(ret,A)):- N1 is N+1. tidy_and_count2(instr(div,X),N,N1,instr(div,X)):- N1 is N+2. tidy_and_count2(instr(mul,X),N,N1,instr(mul,X)):- N1 is N+2. tidy_and_count2(instr(inc,A),N,N1,instr(inc,A)):- N1 is N+1. tidy_and_count2(instr(X,Y),N,N1,instr(X,Y)):- N1 is N+2. tidy_and_count2(label(N),N,N1,nop):- N1 is N+1. tidy_and_count2(no_op,N,N,no_op).%:- N1 is N+1. tidy_and_count2(nop,N,N1,nop):- N1 is N+1. tidy_and_count2(mov([A],ax),N,N1,mov([A],ax)):\+number(A),nonvar(A),N1 is N+2. tidy_and_count2(mov([A],ax),N,N1,mov([A],ax)):- var(A),N1 is N+3. tidy_and_count2(mov(ax,A),N,N1,mov(ax,A)):- var(A),N1 is N+3. tidy_and_count2(mov(bx,A),N,N1,mov(bx,A)):- var(A),N1 is N+3. tidy_and_count2(mov(ah,X),N,N1,mov(ah,X)):- N1 is N+2. tidy_and_count2(mov(dl,A),N,N1,mov(dl,A)):- N1 is N+2. tidy_and_count2(mov(A,[B]),N,N1,mov(A,[B])):\+number(A),var(B),nonvar(A),N1 is N+3. tidy_and_count2(mov(A,B),N,N1,mov(A,B)):\+number(A),\+number(B),nonvar(A),nonvar(B),N1 is N+2. tidy_and_count2(mov(X,Y),N,N1,mov(X,Y)):- N1 is N+3. tidy_and_count2(sub(ax,[A]),N,N1,sub(ax,[A])):\+number(A),nonvar(A),N1 is N+2. tidy_and_count2(sub(ax,bx),N,N1,sub(ax,bx)):- N1 is N+2. tidy_and_count2(sub(sp,A),N,N1,sub(sp,A)):- N1 is N+4. tidy_and_count2(sub(bx,A),N,N1,sub(bx,A)):- N1 is N+4. tidy_and_count2(sub(A,B),N,N1,sub(A,B)):- N1 is N+3. tidy_and_count2(add(dl,A),N,N1,add(dl,A)):- N1 is N+3. tidy_and_count2(add(sp,A),N,N1,add(sp,A)):- N1 is N+4. tidy_and_count2(add(bx,A),N,N1,add(bx,A)):- N1 is N+4. tidy_and_count2(add(ax,[A]),N,N1,add(ax,[A])):\+number(A),nonvar(A),N1 is N+2. tidy_and_count2(add(ax,[bx]),N,N1,add(ax,[bx])):- N1 is N+2. tidy_and_count2(add(ax,bx),N,N1,add(ax,bx)):- N1 is N+2. tidy_and_count2(add(A,B),N,N1,add(A,B)):- N1 is N+3. tidy_and_count2(addr(A,B),N,N1,addr(A,B)):- N1 is N+2. tidy_and_count2(xor(A,B),N,N1,xor(A,B)):- N1 is N+2. tidy_and_count2(or(A,B),N,N1,or(A,B)):- N1 is N+2. Come vediamo ogni istruzione ha un peso diverso a seconda di quanti bytes serviranno per la sua codifica in linguaggio macchina per processori intel 80x86. Ho scelto di codificate le istruzioni di label dando loro peso 1 e traducendole nell’istruzione nop (no operation). Da notare che alcune istruzioni del codice intel adottano un sistema di indirizzamento relativo anzichè assoluto. Ciò avviene nelle istruzioni di salto condizionato, incondizionato, chiamate a funzioni (call)e in loop. Il principio di indirizzamento di queste istruzioni si basa su uno spiazzamento positivo o negativo a partire dalla loro istruzione successiva. Ad esempio sia: 62 100-mov 103-cmp 106-jne 108-inc 109-int ax,5 ax,4 1 ax 21h dove i numeri a sinistra rappresentano gli indirizzi di memoria relativi all’istruzione. All’indirizzo 106 abbiamo un salto condizionato di 1 byte a partire dall’istruzione successiva (108) a quella del salto(106) se il valore del registro ax è 4, e quindi si salterà all’indirizzo 109. Nel processo di assembleggio al linguaggio assembler di intel ho scelto di rappresentare tale indirizzo relativo tramite una differenza di indirizzi assoluti che verrà poi risolta definitivamente nel processo di scrittura del codice macchina su file. Quindi se abbiamo ad esempio una chiamata al predicato tidy_and_count2 con instr(jmp,X) X come istruzione da assemblare e N come indirizzo di tale istruzione e è un valore di memoria assoluto (sia X che N sono già istanziati!), allora viene calcotalo N1=N+3 cioè il peso della istruzione di salto e N1 è il l’indirizzo della prossima istruzione, e in uscita avremo instr(jmp,X-N1) che da uno spiazzamento positivo o negativo a seconda dei valori della variabili in gioco, il cui valore verrà effettivamente calcolato in fase di scrittura del codice macchina intel. Molto importante è stato stabilire l’ordine corretto in cui trascrivere i predicati tidy_and_count2 nella base di conoscenza del Prolog. Infatti qui si lavora ancora con variabili Prolog e il non corretto ordine dei predicati nella base di conoscenza del Prolog darebbe una errata scelta del predicato successivo da scegliere nel processo di ricorsione. allocate2(void,N,N). allocate2(dict(_,N1,mstring,V,Before,After),N0,N):strlen(V,A),allocate2(Before,N0,N1),N2 is N1+A,allocate2(After,N2,N). allocate2(dict(_,N1,_,_,Before,After),N0,N):allocate2(Before,N0,N1), N2 is N1+2, allocate2(After,N2,N). Il predicato allocate2 differisce da quello usato nell’assemblaggio dello pseudocodice solo per il fatto che ad ogni variabile globale intera vengono assegnati 2 locazioni di memoria anziche 1. Riportiamo qui di seguito l’output generato dall’assemblatore rispettivamente nell’ordine per i programmi somma (pseudocodice poi intel) e fattoriale(pseudocodice poi intel) di Figura 23 e 24: (instr(jump, 12); ((no_op;init_funz); (create_local(2); ((instr(load, stack_bp(6));instr(add, stack_bp(4)));instr(store, 63 stack_bp(-2))); (instr(load, stack_bp(2));destroy_local(2);end_funz);no_op); (destroy_local(2);end_funz);no_op); ((no_op;init_funz); (((((instr(loadc, 1);instr(push, 0)); (instr(loadc, 3);instr(push, 0));no_op);instr(call, 2);destroy_local(4));instr(store, 23));no_op);halt;destroy_local(0);end_funz);no_op);instr(dw, 0);block(2) e (instr(push, cs);instr(pop, ds);instr(jmp, 313-261); ((nop;instr(push, bp);mov(bp, sp)); (sub(sp, 2); (((mov(bx, bp);add(bx, 6);mov(ax, [bx]));mov(bx, bp);add(bx, 4);add(ax, [bx]));mov(bx, bp);add(bx, -2);mov([bx], ax)); ((mov(bx, bp);add(bx, -2);mov(ax, [bx]));add(sp, 2);instr(pop, bp);instr(ret, 0));no_op); (add(sp, 2);instr(pop, bp);instr(ret, 0));no_op); ((nop;instr(push, bp);mov(bp, sp)); (((((mov(ax, 1);instr(push, ax)); (mov(ax, 3);instr(push, ax));no_op);instr(call, 261-328);add(sp, 4));mov([343], ax));no_op);instr(int, 32);add(sp, 0);instr(pop, bp);instr(ret, 0));no_op);instr(dw, 0);block(3) e (instr(jump, 33); ((no_op;init_funz); ((instr(loadc, 5);destroy_local(0);end_funz);no_op); (destroy_local(0);end_funz); (no_op;init_funz); (create_local(6); (((instr(load, stack_bp(4));instr(subc, 1));instr(jumpne, 17)); (instr(loadc, 1);destroy_local(6);end_funz);instr(jump, 17);no_op;no_op;no_op); ((instr(load, stack_bp(4));instr(subc, 1));instr(store, stack_bp(-2))); ((((instr(load, stack_bp(2));instr(push, 0));no_op);instr(call, 8);destroy_local(2));instr(store, stack_bp(-4))); ((instr(load, stack_bp(-4));instr(mul, stack_bp(4)));instr(store, stack_bp(6))); (instr(load, stack_bp(6));destroy_local(6);end_funz);no_op); (destroy_local(6);end_funz);no_op); ((no_op;init_funz); (create_local(4); (instr(loadc, 68);instr(write_str, 0));instr(write_crlf, 0); ((((instr(loadc, 3);instr(push, 0));no_op);instr(call, 8);destroy_local(2));instr(store, stack_bp(-4))); (instr(load, stack_bp(-4));instr(write, 0));instr(write_crlf, 0); (((instr(load, stack_bp(4));instr(subc, 100));instr(jumple, 52)); ((instr(loadc, 4);instr(store, stack_bp(-2)));no_op);instr(jump, 55);no_op; (((no_op;instr(call, 2);destroy_local(0));instr(store, stack_bp(2)));no_op);no_op); (no_op; ((instr(load, stack_bp(2));instr(subc, 0));instr(jumple, 65)); ((instr(load, stack_bp(2));instr(write, 0));instr(write_crlf, 0); ((instr(load, stack_bp(-2));instr(subc, 1));instr(store, stack_bp(2)));no_op);instr(jump, 55);no_op);no_op);halt;destroy_local(4);end_funz);no_op);instr(ds , prog1$);block(7) e (instr(push, cs);instr(pop, ds);instr(jmp, 411-261); ((nop;instr(push, bp);mov(bp, sp)); ((mov(ax, 5);add(sp, 0);instr(pop, bp);instr(ret, 0));no_op); (add(sp, 0);instr(pop, bp);instr(ret, 0)); (nop;instr(push, bp);mov(bp, sp)); (sub(sp, 6); ((((mov(bx, bp);add(bx, 4);mov(ax, [bx]));sub(ax, 64 1));instr(jumpne, 313-301)); (mov(ax, 1);add(sp, 6);instr(pop, bp);instr(ret, 0));instr(jmp, 314-313);nop;no_op;nop); (((mov(bx, bp);add(bx, 4);mov(ax, [bx]));sub(ax, 1));mov(bx, bp);add(bx, 2);mov([bx], ax)); (((((mov(bx, bp);add(bx, -2);mov(ax, [bx]));instr(push, ax));no_op);instr(call, 280-346);add(sp, 2));mov(bx, bp);add(bx, -4);mov([bx], ax)); (((mov(bx, bp);add(bx, -4);mov(ax, [bx]));instr(push, ax);mov(bx, bp);add(bx, 4);mov(ax, [bx]);mov(bx, ax);mov(dx, 0);instr(pop, ax);instr(mul, bx));mov(bx, bp);add(bx, -6);mov([bx], ax)); ((mov(bx, bp);add(bx, -6);mov(ax, [bx]));add(sp, 6);instr(pop, bp);instr(ret, 0));no_op); (add(sp, 6);instr(pop, bp);instr(ret, 0));no_op); ((nop;instr(push, bp);mov(bp, sp)); (sub(sp, 4); (mov(ax, 642);mov(dx, ax);mov(ah, 9);instr(int, 33)); (mov(dl, 13);mov(ah, 2);instr(int, 33);mov(dl, 10);mov(ah, 2);instr(int, 33)); ((((mov(ax, 3);instr(push, ax));no_op);instr(call, 280447);add(sp, 2));mov(bx, bp);add(bx, -4);mov([bx], ax)); ((mov(bx, bp);add(bx, -4);mov(ax, [bx]));mov(bx, 10);cx xor cx;nop;dx xor dx;instr(div, bx);instr(push, dx);instr(inc, cx);or(ax, ax);instr(jumpne, 472-483);nop;instr(pop, dx);add(dl, 48);mov(ah, 2);instr(int, 33);instr(loop, 483-494)); (mov(dl, 13);mov(ah, 2);instr(int, 33);mov(dl, 10);mov(ah, 2);instr(int, 33)); ((((mov(bx, bp);add(bx, -4);mov(ax, [bx]));sub(ax, 100));instr(jumple, 533-519)); ((mov(ax, 4);mov(bx, bp);add(bx, 2);mov([bx], ax));no_op);instr(jmp, 549-533);nop; (((no_op;instr(call, 261-537);add(sp, 0));mov(bx, bp);add(bx, 2);mov([bx], ax));no_op);nop); (nop; (((mov(bx, bp);add(bx, 2);mov(ax, [bx]));sub(ax, 0));instr(jumple, 633-564)); (((mov(bx, bp);add(bx, -2);mov(ax, [bx]));mov(bx, 10);cx xor cx;nop;dx xor dx;instr(div, bx);instr(push, dx);instr(inc, cx);or(ax, ax);instr(jumpne, 577-588);nop;instr(pop, dx);add(dl, 48);mov(ah, 2);instr(int, 33);instr(loop, 588-599)); (mov(dl, 13);mov(ah, 2);instr(int, 33);mov(dl, 10);mov(ah, 2);instr(int, 33)); (((mov(bx, bp);add(bx, -2);mov(ax, [bx]));sub(ax, 1));mov(bx, bp);add(bx, -2);mov([bx], ax));no_op);instr(jmp, 550633);nop);no_op);instr(int, 32);add(sp, 4);instr(pop, bp);instr(ret, 0));no_op);instr(ds, prog1$);block(7) . 5.8 Il file ‘toStream’ Dopo la fase di assemblaggio svolta dai predicati assemble o assemble2 a seconda che si voglia rispettivamente uno pseudocodice assembler o un codice assembler per processori intel, si possono scegliere due strade: la prima trasforma il sorgente assembler per intel direttamente in codice macchina scrivendo su file, la seconda strada stampa a video il sorgente assembler pseudo o intel indifferentemente. Quest’ultimo compito è svolto dal predicato toStream che prende in ingresso indipendentemente lo pseudoassembler o l’assembler di intel e lo stampa a video riformattandolo. Il predicato di ingresso è: toStream((Code1;Code2)):- toStream(Code1),toStream(Code2). 65 che si richiama ricorsivamente processando una istruzione alla volta. toStream(instr(X,Y)):- write(X),write(' '),write(Y),nl. Il predicato scritto sopra stampa a video la generica istruzione (pseudo o intel). Tale istruzione è composta dai parametri della funzione instr. Quindi sia instr(push,0) l’istruzione da processare, si stamperà a video ‘push 0’. I seguenti predicati processano istruzioni esclusivamente intel: toStream(X):- X=..[mov,A,B|_],write('mov '),write(A),write(','),write(B),nl. toStream(X):- X=..[sub,A,B|_],write('sub '),write(A),write(','),write(B),nl. toStream(X):- X=..[add,A,B|_],write('add '),write(A),write(','),write(B),nl. toStream(X):- X=..[xor,A,B|_],write('xor '),write(A),write(','),write(B),nl. toStream(X):- X=..[or,A,B|_],write('or '),write(A),write(','),write(B),nl. toStream(X):- X=..[movr,A,B|_],write('mov '),write(A),write(','),write(B),nl. toStream(X):- X=..[addr,A,B|_],write('add '),write(A),write(','),write(B),nl. Qui si fa uso del predicato Prolog univ (=..) che scompone un predicato e i suoi argomenti in una lista. Prendiamo ad esempio il predicato p(a,b). Applicando su di esso il predicato Prolog univ lo scomponiamo nella seguente lista [p,a,b]. Quindi i predicati sopra scritti scompongono l’istruzione in una lista e ne stampa a video gli elementi formattandoli. Infine il seguente predicato stampa il resto delle istruzioni assembler che non richiedono una riformattazione di output: toStream(X):- write(X),nl. 5.9 Il file ‘toCode’ l’ultimo file del compilatore prende in input il codice sorgente per processori di tipo intel 80x86 e ne produce il codice macchina scrivendo su file. Come abbiamo più volte detto il file prodotto è un eseguibile in ambiente MS-DOS o shell di windows di tipo .COM, cioè un tipo di file senza alcun heather che viene mappato semplicemente in memoria. Il predicato di ingresso è. 66 toCode(I,File):- tell(File),tc(I),told. Tale predicato ridirige l’output su file (tell(File)), ci scrive il codice macchina (tc(I)), e chiude il file ripristinando lo standard output (told). Il seguente predicato (tc) si richiama ricorsivamente processando una istruzione alla volta: tc((Code1;Code2)):- tc(Code1),tc(Code2). Come si vede dai seguenti predicati, ogni istruzione del sorgente assembler intel viene tradotto nel relativo codice macchina, e questo viete scritto su file tramite il predicato Prolog put. tc(instr(push,cs)):- put(14). tc(instr(push,ax)):- put(80). tc(instr(pop,ds)):- put(31). tc(instr(push,bp)):- put(85). tc(instr(pop,bp)):- put(93). tc(mov(bx,ax)):- put(137),put(195). tc(mov(ax,bp)):- put(137),put(232). tc(mov(ax,bx)):- put(137),put(216). tc(mov(ax,dx)):- put(137),put(208). tc(mov(bx,bp)):- put(137),put(235). tc(mov(bx,[A])):- put(139),put(30),splitta(A,H,L),put(L),put(H). tc(mov(bx,A)):- put(187),splitta(A,H,L),put(L),put(H). tc(mov(ax,[bx])):- put(139),put(7). tc(mov(ax,[A])):- put(161),splitta(A,H,L),put(L),put(H). tc(mov(ax,A)):- put(184),splitta(A,H,L),put(L),put(H). tc(mov([bx],ax)):- put(137),put(7). tc(mov([A],ax)):- put(163),splitta(A,H,L),put(L),put(H). tc(instr(jmp,A)):- put(233),Y is A,splitta(Y,H,L),put(L),put(H). tc(instr(call,A)):- put(232),Y is A,splitta(Y,H,L),put(L),put(H). tc(instr(ret,0)):- put(195). tc(nop):- put(144). tc(block(_)):- put(144). tc(instr(int,A)):- put(205),put(A). tc(mov(dx,ax)):- put(137),put(194). tc(mov(dx,A)):- put(186),splitta(A,H,L),put(L),put(H). tc(mov(ah,A)):- put(180),put(A). tc(mov(bp,sp)):- put(137),put(229). tc(mov(sp,bp)):- put(137),put(236). tc(instr(mul,bx)):- put(247),put(227). tc(instr(div,bx)):- put(247),put(243). tc(addr(ax,dx)):- put(1),put(208). tc(add(ax,[bx])):- put(3),put(7). tc(add(ax,bx)):- put(1),put(216). tc(add(ax,A)):- put(5),splitta(A,H,L),put(L),put(H). tc(add(sp,A)):- put(129),put(196),splitta(A,H,L),put(L),put(H). tc(add(bx,A)):- put(129),put(195),splitta(A,H,L),put(L),put(H). tc(sub(ax,[bx])):- put(41),put(7). tc(sub(ax,bx)):- put(41),put(8). tc(sub(ax,[A])):- put(43),put(6),splitta(A,H,L),put(L),put(H). tc(sub(ax,A)):- put(45),splitta(A,H,L),put(L),put(H). tc(add(ax,[A])):- put(3),put(6),splitta(A,H,L),put(L),put(H). tc(sub(sp,A)):- put(129),put(236),splitta(A,H,L),put(L),put(H). tc(sub(ax,A)):- put(129),put(235),splitta(A,H,L),put(L),put(H). tc(instr(dw,A)):- splitta(A,H,L),put(L),put(H). tc(instr(ds,A)):- write(A). tc(instr(db,A)):- put(A). tc(instr(jumpne,A)):- put(117),Y is A,neg_byte(Y,B),put(B). 67 tc(instr(jumple,A)):- put(126),Y is A,neg_byte(Y,B),put(B). tc(instr(jumpge,A)):- put(125),Y is A,neg_byte(Y,B),put(B). tc(instr(jumpeq,A)):- put(116),Y is A,neg_byte(Y,B),put(B). tc(instr(jumplt,A)):- put(124),Y is A,neg_byte(Y,B),put(B). tc(instr(jumpgt,A)):- put(127),Y is A,neg_byte(Y,B),put(B). tc(mov(dl,A)):- put(178),neg_byte(A,B),put(B). tc(instr(pop,bx)):- put(91). tc(instr(inc,cx)):- put(65). tc(instr(push,dx)):- put(82). tc(xor(dx,dx)):- put(49),put(210). tc(xor(cx,cx)):- put(49),put(201). tc(or(ax,ax)):- put(9),put(192). tc(instr(pop,dx)):- put(90). tc(add(dl,A)):- put(128),put(194),neg_byte(A,B),put(B). tc(instr(loop,A)):- put(226),Y is A,neg_byte(Y,B),put(B). tc(mov(cx,[bx])):- put(139),put(15). tc(instr(inc,bx)):- put(67). tc(instr(push,cx)):- put(81). tc(instr(pop,cx)):- put(89). tc(instr(pop,ax)):- put(88). tc(no_op). Il predicato Prolog put(Char) scrive sull’output corrente (nel nostro caso è stato ridiretto al file) il valore (Char) ed è stato pensato per scrivere simboli tramite il corrispondente codice ASCII. Quindi il valore di Char è compreso tra zero e 255. Ad esempio put(65) scriverà il valore 65 che ad esempio è il codice ASCII della lettera A. Nella scrittura del codice macchina bisogna fare attenzione all’architettura del calcolatore. In particolare a come vengono trattati i numeri negativi e a come vengono memorizzati gli interi che corrispondono a 2 bytes. Il calcolatore per natura memorizza una sequenza di bit che codificano in base due un numero naturale (intero positivo) . per interpretare come negativo si dedica il bit più significativo al ruolo di bit di segno, così se tale bit è settato ad uno il numero in questione è negativo, altrimenti è positivo. Facciamo un esempio e supponiamo che un calcolatore lavori a tre bit che codificano i numeri da zero (000) a sette (111). Allora i numeri positivi avranno il bit più significativo a zero (0[000], 1[001], 2[010], 3[011]) e i numeri positivi avranno il bit di segno settato ad uno (-3[111], -2[110], -1[101], -4[100]) e l’intervallo riconosciuto dal calcolatore sarà compreso tra -4 e +3. Siccome lavorare con i bit in Prolog è molto malsano, possiamo pensare che il calcolatore ipotetico visto qui sopra lavori in modulo 8. Quindi riconosca tutti i numeri tra 0 e 7 e ad 8 è congruo a zero (8=0 mod 8). Noi sappiamo che -1+1=0. se lavoriamo in modulo 8 avremo che 7+1=8=0. Quindi in modulo otto, 7 è congruo a -1. 68 Questo è il principio che adotta il predicato neg_byte del mio compilatore per codificate i numeri negativi in codice macchina intel. Sia X uni numero negativo compreso tra -128 e -1 allora questo viene codificato tramite l’espressione 256-X. Cioè per rappresentare i bytes negativi lavoriamo in modulo 256. Per rappresentare gli interi negativi lavoreremo in modulo 65536. neg_byte(A,B):- A<0, B is 256+A. neg_byte(A,A). L’ultimo punto da illustrare è come il calcolatore memorizza gli interi (che sono composti da 2 bytes). I processori intel sono detti little endian, cioè dei due bytes che compongono la parola, viene memorizzato prima il byte meno significativo e poi il byte più significativo. Quindi se dobbiamo memorizzare il numero decimale 7973 che corrisponde al numero 1F25 esadecimale, dove 1F è il byte più significativo e 25 è il byte meno significativo, questo verrà memorizzato così: 251F. Questo è il lavoro svolto dal predicato splitta: splitta(N,Hi,Lo):- N<0, N1 is 65536+N, Hi is N1//256, Lo is N1 mod 256. splitta(N,Hi,Lo):- Hi is N//256, Lo is N mod 256. che accetta tre parametri: N è il numero da convertire, Hi è il relativo byte più significativo, e Lo è il relativo byte meno significativo. 69 Capitolo 6 Il codice sorgente 1) Il file ‘assemble’. assemble(Code,D,_,TidyCode;Db;block(L)):tidy_and_count(Code,1,N,TidyCode),N1 is N, allocate(D,N1,N2),L is N2-N1+1,getCode(D,Db),!. tidy_and_count((Code1;Code2),M,N,SS;SS1):tidy_and_count(Code1,M,M1,SS),tidy_and_count(Code2,M1,N,SS1). tidy_and_count(instr(X,Y),N,N1,instr(X,Y)):- N1 is N+1. tidy_and_count(label(N),N,N,no_op).%:- N1 is N+1. tidy_and_count(no_op,N,N,no_op).%:- N1 is N+1. tidy_and_count(nop,N,N1,nop):- N1 is N+1. tidy_and_count(init_funz,N,N1,init_funz):- N1 is N+1. tidy_and_count(create_local(X),N,N1,create_local(X)):- N1 is N+1. tidy_and_count(destroy_local(X),N,N1,destroy_local(X)):- N1 is N+1. tidy_and_count(end_funz,N,N1,end_funz):- N1 is N+1. tidy_and_count(halt,N,N1,halt):- N1 is N+1. allocate(void,N,N). allocate(dict(_,N1,mstring,V,Before,After),N0,N):strlen(V,A),allocate(Before,N0,N1),N2 is N1+A,allocate(After,N2,N). allocate(dict(_,N1,_,_,Before,After),N0,N):allocate(Before,N0,N1), N2 is N1+1, allocate(After,N2,N). strlen(X,Y):- name(X,F),length(F,Y). %%%%% assemble2(Code,Gv,_,TidyCode;Db;block(L)):tidy_and_count2(Code,256,N,TidyCode),N1 is N, allocate2(Gv,N1,N2),L is N2-N1+1,getCode(Gv,Db),!. tidy_and_count2((Code1;Code2),M,N,SS;SS1):tidy_and_count2(Code1,M,M1,SS),tidy_and_count2(Code2,M1,N,SS1). tidy_and_count2(instr(push,X),N,N1,instr(push,X)):- N1 is N+1. tidy_and_count2(instr(pop,X),N,N1,instr(pop,X)):- N1 is N+1. tidy_and_count2(instr(int,X),N,N1,instr(int,X)):- N1 is N+2. tidy_and_count2(instr(jmp,X),N,N1,instr(jmp,X-N1)):- N1 is N+3. tidy_and_count2(instr(call,X),N,N1,instr(call,X-N1)):- N1 is N+3. tidy_and_count2(instr(loop,X),N,N1,instr(loop,X-N1)):- N1 is N+2. tidy_and_count2(instr(jumpne,X),N,N1,instr(jumpne,X-N1)):- N1 is N+2. tidy_and_count2(instr(jumple,X),N,N1,instr(jumple,X-N1)):- N1 is N+2. tidy_and_count2(instr(jumpge,X),N,N1,instr(jumpge,X-N1)):- N1 is N+2. tidy_and_count2(instr(jumpeq,X),N,N1,instr(jumpeq,X-N1)):- N1 is N+2. tidy_and_count2(instr(jumplt,X),N,N1,instr(jumplt,X-N1)):- N1 is N+2. tidy_and_count2(instr(jumpgt,X),N,N1,instr(jumpgt,X-N1)):- N1 is N+2. tidy_and_count2(instr(ret,A),N,N1,instr(ret,A)):- N1 is N+1. 70 tidy_and_count2(instr(div,X),N,N1,instr(div,X)):- N1 is N+2. tidy_and_count2(instr(mul,X),N,N1,instr(mul,X)):- N1 is N+2. tidy_and_count2(instr(inc,A),N,N1,instr(inc,A)):- N1 is N+1. tidy_and_count2(instr(X,Y),N,N1,instr(X,Y)):- N1 is N+2. tidy_and_count2(label(N),N,N1,nop):- N1 is N+1. tidy_and_count2(no_op,N,N,no_op).%:- N1 is N+1. tidy_and_count2(nop,N,N1,nop):- N1 is N+1. tidy_and_count2(mov([A],ax),N,N1,mov([A],ax)):\+number(A),nonvar(A),N1 is N+2. tidy_and_count2(mov([A],ax),N,N1,mov([A],ax)):- var(A),N1 is N+3. %tidy_and_count2(mov(ax,A),N,N1,mov(ax,A)):- length(,N1 is N+3. tidy_and_count2(mov(ax,A),N,N1,mov(ax,A)):- var(A),N1 is N+3. tidy_and_count2(mov(bx,A),N,N1,mov(bx,A)):- var(A),N1 is N+3. tidy_and_count2(mov(ah,X),N,N1,mov(ah,X)):- N1 is N+2. tidy_and_count2(mov(dl,A),N,N1,mov(dl,A)):- N1 is N+2. tidy_and_count2(mov(A,[B]),N,N1,mov(A,[B])):\+number(A),var(B),nonvar(A),N1 is N+3. tidy_and_count2(mov(A,B),N,N1,mov(A,B)):\+number(A),\+number(B),nonvar(A),nonvar(B),N1 is N+2. %tidy_and_count2(movr(dx,ax),N,N1,movr(dx,ax)):- N1 is N+2. %tidy_and_count2(mov(bp,sp),N,N1,mov(bp,sp)):- N1 is N+2. %tidy_and_count2(mov(sp,bp),N,N1,mov(sp,bp)):- N1 is N+2. %tidy_and_count2(mov(ax,bp),N,N1,mov(ax,bp)):- N1 is N+2. %tidy_and_count2(mov(bx,bp),N,N1,mov(bx,sp)):- N1 is N+2. %tidy_and_count2(mov(ax,bx),N,N1,mov(ax,bx)):- N1 is N+2. %tidy_and_count2(mov(bx,ax),N,N1,mov(bx,ax)):- N1 is N+2. tidy_and_count2(mov(X,Y),N,N1,mov(X,Y)):- N1 is N+3. tidy_and_count2(sub(ax,[A]),N,N1,sub(ax,[A])):\+number(A),nonvar(A),N1 is N+2. tidy_and_count2(sub(ax,bx),N,N1,sub(ax,bx)):- N1 is N+2. tidy_and_count2(sub(sp,A),N,N1,sub(sp,A)):- N1 is N+4. tidy_and_count2(sub(bx,A),N,N1,sub(bx,A)):- N1 is N+4. tidy_and_count2(sub(A,B),N,N1,sub(A,B)):- N1 is N+3. tidy_and_count2(add(dl,A),N,N1,add(dl,A)):- N1 is N+3. tidy_and_count2(add(sp,A),N,N1,add(sp,A)):- N1 is N+4. tidy_and_count2(add(bx,A),N,N1,add(bx,A)):- N1 is N+4. tidy_and_count2(add(ax,[A]),N,N1,add(ax,[A])):\+number(A),nonvar(A),N1 is N+2. tidy_and_count2(add(ax,[bx]),N,N1,add(ax,[bx])):- N1 is N+2. tidy_and_count2(add(ax,bx),N,N1,add(ax,bx)):- N1 is N+2. tidy_and_count2(add(A,B),N,N1,add(A,B)):- N1 is N+3. tidy_and_count2(addr(A,B),N,N1,addr(A,B)):- N1 is N+2. tidy_and_count2(xor(A,B),N,N1,xor(A,B)):- N1 is N+2. tidy_and_count2(or(A,B),N,N1,or(A,B)):- N1 is N+2. allocate2(void,N,N). allocate2(dict(_,N1,mstring,V,Before,After),N0,N):strlen(V,A),allocate2(Before,N0,N1),N2 is N1+A,allocate2(After,N2,N). allocate2(dict(_,N1,_,_,Before,After),N0,N):allocate2(Before,N0,N1), N2 is N1+2, allocate2(After,N2,N). getCode(void,instr(db,0)). getCode(dict(_,_,mstring,V,void,void),instr(ds,V)). getCode(dict(_,_,nostr,V,void,void),instr(dw,V)). getCode(dict(_,_,mstring,V,void,After),(instr(ds,V);Db)):getCode(After,Db). getCode(dict(_,_,nostr,V,void,After),(instr(dw,V);Db)):getCode(After,Db). getCode(dict(_,_,mstring,V,Before,void),(Db;instr(ds,V))):getCode(Before,Db). 71 getCode(dict(_,_,nostr,V,Before,void),(Db;instr(dw,V))):getCode(Before,Db). getCode(dict(_,_,mstring,V,Before,After),(Db1;instr(ds,V);Db2)):getCode(Before,Db1),getCode(After,Db2). getCode(dict(_,_,nostr,V,Before,After),(Db1;instr(dw,V);Db2)):getCode(Before,Db1),getCode(After,Db2). 2) Il file ‘compiler’ load:consult('lexan'),consult('parser2'),consult('encode2'),consult('a ssemble2'),consult('tostream'), consult('tointel'),consult('tocode'),consult('symtab'),assert(lva r(zar,_,_)),retract(lvar(zar,_,_)). compile(File,warren,debug):toklist(File,Tl),write(Tl),nl,parse(Tl,Temp1),write(Temp1),nl,enc ode2(Temp1,Gv,Fv,Code),write(Code),nl,assemble(Code,Gv,Fv,Out),wr ite(Out),nl,!. compile(File,warren,toScreen):toklist(File,A),parse(A,B),encode2(B,Gv,Fv,C),assemble(C,Gv,Fv,E) ,toStream(E). compile(File,intelSrc,toScreen):toklist(File,A),parse(A,B),encode2(B,Gv,Fv,C),toIntel(C,H),assemb le2(H,Gv,Fv,E),toStream(E). compile(File,intelCode,FileOut):toklist(File,A),parse(A,B),encode2(B,Gv,Fv,C),toIntel(C,H),assemb le2(H,Gv,Fv,E),toCode(E,FileOut). compile(File,X,Y):- tell(Y),compile(File,X,toScreen),told. 3) Il file ‘encode2’ lookup2(Nome,_,stack_bp(Addr),nostr,_):- lvar(Nome,_,Addr). lookup2(Nome,Alf,Addr,Type,Val):- lookup(Nome,Alf,Addr,Type,Val). lookup(Key,dict(Key,X,Y,Z,_,_),Value,Type,Val):- !,X = Value,Y=Type,Z=Val. lookup(Key,dict(Key1,_,_,_,Left,_),Value,Type,Val):- Key@<Key1, lookup(Key,Left,Value,Type,Val). lookup(Key,dict(Key1,_,_,_,_,Right),Value,Type,Val):- Key@>Key1, lookup(Key,Right,Value,Type,Val). %lookup(Key,[(Key,Value)|Dict],Value):- !. %lookup(Key,[(Key1,Value1)|Dict],Value):Key\==Key1,lookup(Key,Dict,Value). %mstring(X):- atom(X). encode2(X,Gv,Fv,(instr(jump,A);Y)):lookup(main,Fv,A,nostr,_),encode(X,Gv,Fv,Y). encode((X;Xs),Gv,Fv,(Y;Ys)):- encode(X,Gv,Fv,Y), encode(Xs,Gv,Fv,Ys). encode(void,_,_,no_op). encode(halt,_,_,halt). encode(write_crlf,_,_,instr(write_crlf,0)). encode(funz(Funz,X2),_,Fv,(label(Address);init_funz)):setlocalpar(X2,Funz),lookup(Funz,Fv,Address,nostr,_),assert(lvarn (0)). encode(local(X),_,_,create_local(N1)):- length(X,N),N1 is N*2,setlocalvar(X,_),retract(lvarn(0)),assert(lvarn(N1)). %encode(end_funz,_,_,no_op):- lvarn(0). encode(end_funz,_,_,(destroy_local(X);end_funz)):lvarn(X),retractall(lvarn(_)),retractall(lvar(_,_,_)). encode(assign(E,Ex),Gv,_,(Code;instr(storep,Address))):E=..[p,R|_],lookup2(R,Gv,Address,nostr,0),encode_expression(Ex,Gv ,Code). 72 encode(assign(Name,E),Gv,_,(Code;instr(store,Address))):E=..[p1,R|_],lookup2(Name,Gv,Address,nostr,0),encode_ptr1(R,Gv,Co de). encode(assign(Name,E),Gv,_,(Code;instr(store,Address))):E=..[p2,R|_],lookup2(Name,Gv,Address,nostr,0),encode_ptr2(R,Gv,Co de). encode(assign(Name,E),Gv,_,(Code;instr(store,Address))):lookup2(Name,Gv,Address,nostr,0),encode_expression(E,Gv,Code). encode(if(Test,Then,Else),Gv,Fv,(TestCode;ThenCode;instr(jump,L2) ;label(L1);ElseCode;label(L2))):encode_test(Test,L1,Gv,TestCode),encode(Then,Gv,Fv,ThenCode),enco de(Else,Gv,Fv,ElseCode). encode(while(Test,Do),Gv,Fv,(label(L1);TestCode;DoCode;instr(jump ,L1);label(L2))):- encode_test(Test,L2,Gv,TestCode), encode(Do,Gv,Fv,DoCode). encode(read(X),Gv,_,instr(read,Address)):lookup2(X,Gv,Address,nostr,_). encode(write(mstring(E)),Gv,_,(instr(loadc,Address);instr(write_s tr,0))):name(E,W),append(W,[36],T),name(E2,T),lookup2(E2,Gv,Address,mstri ng,E2). encode(write(E),Gv,_,(Code;instr(write,0))):encode_expression(E,Gv,Code). encode(write_str(mstring(E)),Gv,_,(instr(loadc,Address);instr(wri te_str,0))):name(E,W),append(W,[36],T),name(E2,T),lookup2(E2,Gv,Address,mstri ng,E2). encode(callx(X1,X2),Gv,Fv,(Codepush;instr(call,A);destroy_local(N ))):- lookup(X1,Fv,A,_,_),length(X2,M),N is M*2,codepush(X2,Gv,Codepush). encode(return(X),Gv,_,(Code;destroy_local(N1);end_funz)):encode_expression(X,Gv,Code),lvarn(N1). encode(assignret(X),Gv,_,(instr(store,Address))):lookup2(X,Gv,Address,nostr,0). encode_ptr1(mstring(X),Gv,instr(loadc,Addr)):lookup2(X,Gv,Addr,mstring,X). encode_ptr1(name(X),Gv,instr(loadc,Addr)):lookup2(X,Gv,Addr,nostr,0). encode_ptr2(mstring(X),Gv,instr(loadp,Addr)):lookup2(X,Gv,Addr,mstring,X). encode_ptr2(name(X),Gv,instr(loadp,Addr)):lookup2(X,Gv,Addr,nostr,0). encode_expression(mstring(X),Gv,instr(loadc,Address)):lookup2(X,Gv,Address,mstring,X). encode_expression(number(C),_,instr(loadc,C)). encode_expression(name(X),Gv,instr(load,Address)):lookup2(X,Gv,Address,nostr,0). encode_expression(expr(Op,E1,E2),Gv,(Load;Instruction)):single_instruction(Op,E2,Gv,Instruction),encode_expression(E1,Gv, Load). encode_expression(expr(Op,E1,E2),Gv,Code):\+single_instruction(Op,E2,Gv,_),single_operation(Op,E1,Gv,E2Code ,Code),encode_expression(E2,Gv,E2Code). single_instruction(Op,number(C),_,instr(OpCode,C)):literal_operation(Op,OpCode). single_instruction(Op,name(X),Gv,instr(OpCode,A)):memory_operation(Op,OpCode),lookup2(X,Gv,A,_,_). single_operation(Op,E,Gv,Code,(Code;Instruction)):commutative(Op),single_instruction(Op,E,Gv,Instruction). 73 single_operation(Op,E,Gv,Code,(Code;instr(store,Address);Load;ins tr(OpCode,Address))):\+commutative(Op),lookup('$temp',Gv,Address,_,_),encode_expressio n(E,Gv,Load),op_code(E,Op,OpCode). op_code(number(_),Op,OpCode):- literal_operation(Op,OpCode). op_code(name(_),Op,OpCode):- memory_operation(Op,OpCode). literal_operation('+',addc). literal_operation('-',subc). literal_operation('*',mulc). literal_operation('/',divc). memory_operation('+',add). memory_operation('-',sub). memory_operation('*',mul). memory_operation('/',div). commutative('+'). commutative('*'). encode_test(compare(Op,E1,E2),Label,Gv,(Code;instr(OpCode,Label)) ):- comparison_opcode(Op,OpCode),encode_expression(expr('',E1,E2),Gv,Code). comparison_opcode('==',jumpne). comparison_opcode('>',jumple). comparison_opcode('<',jumpge). comparison_opcode('!=',jumpeq). comparison_opcode('>=',jumplt). comparison_opcode('<=',jumpgt). 4) Il file ‘lexan’ toklist(File,Tl):- see(File),get0(C),doSent(C,Tl),seen. doSent(_,[]):- at_end_of_stream,!. doSent(Char,[W|W2]):skip_blank(Char,X),readWord(X,W,C2),doSent(C2,W2). blank_char(32). blank_char(8). blank_char(9). blank_char(10). blank_char(13). skip_blank(_,32):- at_end_of_stream,!. skip_blank(A,B):- blank_char(A),get0(V),skip_blank(V,B). skip_blank(A,B):- blank_char(A),get0(B). skip_blank(A,A). double_char(33,W,R):peek_byte(61),get0(_),get0(R),name(W,[33,61]). % != double_char(43,W,R):peek_byte(43),get0(_),get0(R),name(W,[43,43]). % ++ double_char(45,W,R):peek_byte(45),get0(_),get0(R),name(W,[45,45]). % -double_char(45,W,R):peek_byte(62),get0(_),get0(R),name(W,[45,62]). % -> double_char(60,W,R):peek_byte(61),get0(_),get0(R),name(W,[60,61]). % <= double_char(61,W,R):peek_byte(61),get0(_),get0(R),name(W,[61,61]). % == double_char(62,W,R):peek_byte(61),get0(_),get0(R),name(W,[62,61]). % >= double_char(42,W,R):peek_byte(42),get0(_),get0(R),name(W,[42,42]). % ** 74 single_char(33,W,R):- get0(R),name(W,[33]). % single_char(34,W,R):- get0(R),name(W,[34]). % single_char(35,W,R):- get0(R),name(W,[35]). % single_char(36,W,R):- get0(R),name(W,[36]). % single_char(37,W,R):- get0(R),name(W,[37]). % single_char(38,W,R):- get0(R),name(W,[38]). % single_char(39,W,R):- get0(R),name(W,[39]). % single_char(40,W,R):- get0(R),name(W,[40]). % single_char(41,W,R):- get0(R),name(W,[41]). % single_char(42,W,R):- get0(R),name(W,[42]). % single_char(43,W,R):- get0(R),name(W,[43]). % single_char(44,W,R):- get0(R),name(W,[44]). % single_char(45,W,R):- get0(R),name(W,[45]). % single_char(46,W,R):- get0(R),name(W,[46]). % single_char(47,W,R):- get0(R),name(W,[47]). % single_char(91,W,R):- get0(R),name(W,[91]). % single_char(92,W,R):- get0(R),name(W,[92]). % single_char(93,W,R):- get0(R),name(W,[93]). % single_char(94,W,R):- get0(R),name(W,[94]). % single_char(58,W,R):- get0(R),name(W,[58]). % single_char(59,W,R):- get0(R),name(W,[59]). % single_char(60,W,R):- get0(R),name(W,[60]). % single_char(61,W,R):- get0(R),name(W,[61]). % single_char(62,W,R):- get0(R),name(W,[62]). % single_char(63,W,R):- get0(R),name(W,[63]). % single_char(123,W,R):- get0(R),name(W,[123]). single_char(125,W,R):- get0(R),name(W,[125]). ! " # $ % & ' ( ) * + , . / [ \ ] ^ : ; < = > ? % { % } isAlNum(C):- isNum(C). isAlNum(C):- isChar(C). isNum(X):- X>47,X<58. % 0-9 isChar(X):- X>64,X<91. % A-Z isChar(X):- X>96,X<123. % a-z readWord(C,W,C2):- double_char(C,W,C2). readWord(C,W,C2):- single_char(C,W,C2). readWord(C,W,C2):isNum(C),get0(C3),getNum(C3,W2,C2),name(W,[C|W2]). readWord(C,W,C2):isChar(C),get0(C3),getWord(C3,W2,C2),name(W,[C|W2]). readWord(_,void,C2):- get0(C2). getNum(A,[],A):- \+isNum(A). getNum(A,[A|Num],F):- isNum(A),get0(B),getNum(B,Num,F). getWord(A,[],A):- \+isAlNum(A). getWord(A,[A|Word],F):- isAlNum(A),get0(B),getWord(B,Word,F). readRest(_):- at_end_of_stream,!. readRest(C,[W1|W2]):readWord(C,W1,C2),skip_blank(C2,C3),readRest(C3,W2). 5) Il file ‘parser2’ parse(Tokens,Structure):- iaz_program(Structure,Tokens,[]),!. parse(_,_):- write('not a valid program'),nl,halt. iaz_program((S;Ss;Sss)) --> subp(S),mainp(Ss),subp(Sss). iaz_program((S;Ss;Sss)) --> subp(S),mainp(Ss),subp(Sss),[void]. mainp((funz(main,[]);S;halt;end_funz)) --> [main],['('],[')'],statement(S). mainp((funz(main,[]);S;halt;end_funz)) --> [main],['('],[')'],statement(S),[void]. subp(void)-->[]. 75 subp((funz(X1,X2);S;end_funz;Ss))--> [sub],identifier(X1),['('],listargs(X2),statement(S),subp(Ss). statement((S;Ss))--> ['{'],statement(S),rest_statements(Ss). statement(assign(X,V))--> identifier(X),['='],expression(V),[';']. statement(assign(X,V))--> ['*'],identifier(X),['='],['"'],string(V),['"']. statement(assign(p(X),V))--> ['*'],identifier(X),['='],expression(V),[';']. statement(assign(X,p1(name(V))))--> identifier(X),['='],['&'],identifier(V),[';']. statement(assign(X,p2(name(V))))--> identifier(X),['='],['*'],identifier(V),[';']. statement(callx(X1,X2);assignret(R))--> identifier(R),['='],['$'],identifier(X1),['('],listargs2(X2),[';' ]. statement(if(T,S1,S2))--> [if],['('],test(T),[')'],statement(S1),[else],statement(S2). statement(if(T,S,void))--> [if],['('],test(T),[')'],statement(S). statement(while(T,S))--> [while],['('],test(T),[')'],statement(S). statement(read(X))--> [read],['('],identifier(X),[')'],[';']. statement(write_crlf)-->[write],['('],[crlf],[')'],[';']. statement(write(X))--> [write],['('],expression(X),[')'],[';']. statement(write_str(X))--> [write],['('],['"'],string(X),['"'],[')'],[';']. statement(write_str(X))->[write_str],['('],['"'],string(X),['"'],[')'],[';']. statement(local(X))--> [local],['('],listargs(X),[';']. statement(callx(X1,X2))--> ['$'],identifier(X1),['('],listargs2(X2),[';']. statement(return(X))--> [return],['('],expression(X),[')'],[';']. statement(void)-->[';']. statement(void)-->[void]. rest_statements((S;Ss))--> statement(S),rest_statements(Ss). rest_statements(void)-->['}']. rest_statements(void)-->[void]. expression(X)--> iaz_constant(X). expression(expr(Op,X,Y))--> iaz_constant(X), arithmetic_op(Op), expression(Y). arithmetic_op('+')-->['+']. arithmetic_op('-')-->['-']. arithmetic_op('*')-->['*']. arithmetic_op('/')-->['/']. string(mstring(X))--> identifier(X). iaz_constant(name(X))--> identifier(X). iaz_constant(number(X))--> iaz_integer(X). identifier2(X)--> identifier(X). identifier2(X)--> iaz_integer(X). identifier(X)-->[X],{atom(X)}. iaz_integer(X)-->[X],{integer(X)}. test(compare(Op,X,Y))->expression(X),comparison_op(Op),expression(Y). comparison_op('==')-->['==']. comparison_op('!=')-->['!=']. comparison_op('>')-->['>']. comparison_op('<')-->['<']. comparison_op('>=')-->['>=']. comparison_op('<=')-->['<=']. listargs([])--> [')']. listargs([A|X])--> [','],identifier(A),listargs(X). listargs([A|X])--> identifier(A),listargs(X). listargs2([])--> [')']. 76 listargs2([A|X])--> [','],identifier2(A),listargs2(X). listargs2([A|X])--> identifier2(A),listargs2(X). 6) Il file ‘symtab’ setlocalpar(X,Funz):- length(X,N), N1 is (N+1)*2,slp(X,Funz,N1,_). slp([],_,N,N). slp([X|L],Funz,N,N2):- slp2(X,Funz,N,N1),slp(L,Funz,N1,N2). slp2(X,Funz,N,N1):- assert(lvar(X,Funz,N)), N1 is N-2. getlocalpar(_). setlocalvar(X,Funz):- length(X,N), N1 is -N*2,slv(X,Funz,N1,_). slv([],_,N,N). slv([X|L],Funz,N,N2):- slv2(X,Funz,N,N1),slv(L,Funz,N1,N2). slv2(X,Funz,N,N1):- assert(lvar(X,Funz,N)), N1 is N+2. getlocalvar(_). tdstrip((Code1;Code2),(X,Y)):- tds(Code1,X),tds(Code2,Y). tds(no_op,''). tds(C,C). codepush([],_,no_op). codepush([X|L],Gv,(X1;X2)):codepush2(X,Gv,X1),codepush(L,Gv,X2). codepush2(X,_,(instr(loadc,X);instr(push,0))):- number(X). codepush2(X,Gv,(instr(load,Addr);instr(push,0))):lookup2(X,Gv,Addr,_,_). 7) Il file ‘tocode’ neg_byte(A,B):- A<0, B is 256+A. neg_byte(A,A). splitta(N,Hi,Lo):- N<0, N1 is 65536+N, Hi is N1//256, Lo is N1 mod 256. splitta(N,Hi,Lo):- Hi is N//256, Lo is N mod 256. toCode(I,File):- tell(File),tc(I),told. tc((Code1;Code2)):- tc(Code1),tc(Code2). tc(instr(push,cs)):- put(14). tc(instr(push,ax)):- put(80). tc(instr(pop,ds)):- put(31). tc(instr(push,bp)):- put(85). tc(instr(pop,bp)):- put(93). tc(mov(bx,ax)):- put(137),put(195). tc(mov(ax,bp)):- put(137),put(232). tc(mov(ax,bx)):- put(137),put(216). tc(mov(ax,dx)):- put(137),put(208). tc(mov(bx,bp)):- put(137),put(235). tc(mov(bx,[A])):- put(139),put(30),splitta(A,H,L),put(L),put(H). tc(mov(bx,A)):- put(187),splitta(A,H,L),put(L),put(H). tc(mov(ax,[bx])):- put(139),put(7). tc(mov(ax,[A])):- put(161),splitta(A,H,L),put(L),put(H). tc(mov(ax,A)):- put(184),splitta(A,H,L),put(L),put(H). tc(mov([bx],ax)):- put(137),put(7). tc(mov([A],ax)):- put(163),splitta(A,H,L),put(L),put(H). tc(instr(jmp,A)):- put(233),Y is A,splitta(Y,H,L),put(L),put(H). tc(instr(call,A)):- put(232),Y is A,splitta(Y,H,L),put(L),put(H). tc(instr(ret,0)):- put(195). tc(nop):- put(144). tc(block(_)):- put(144). tc(instr(int,A)):- put(205),put(A). tc(mov(dx,ax)):- put(137),put(194). 77 tc(mov(dx,A)):- put(186),splitta(A,H,L),put(L),put(H). tc(mov(ah,A)):- put(180),put(A). tc(mov(bp,sp)):- put(137),put(229). tc(mov(sp,bp)):- put(137),put(236). tc(instr(mul,bx)):- put(247),put(227). tc(instr(div,bx)):- put(247),put(243). tc(addr(ax,dx)):- put(1),put(208). tc(add(ax,[bx])):- put(3),put(7). tc(add(ax,bx)):- put(1),put(216). tc(add(ax,A)):- put(5),splitta(A,H,L),put(L),put(H). tc(add(sp,A)):- put(129),put(196),splitta(A,H,L),put(L),put(H). tc(add(bx,A)):- put(129),put(195),splitta(A,H,L),put(L),put(H). tc(sub(ax,[bx])):- put(41),put(7). tc(sub(ax,bx)):- put(41),put(8). tc(sub(ax,[A])):- put(43),put(6),splitta(A,H,L),put(L),put(H). tc(sub(ax,A)):- put(45),splitta(A,H,L),put(L),put(H). tc(add(ax,[A])):- put(3),put(6),splitta(A,H,L),put(L),put(H). tc(sub(sp,A)):- put(129),put(236),splitta(A,H,L),put(L),put(H). tc(sub(ax,A)):- put(129),put(235),splitta(A,H,L),put(L),put(H). tc(instr(dw,A)):- splitta(A,H,L),put(L),put(H). tc(instr(ds,A)):- write(A). tc(instr(db,A)):- put(A). tc(instr(jumpne,A)):- put(117),Y is A,neg_byte(Y,B),put(B). tc(instr(jumple,A)):- put(126),Y is A,neg_byte(Y,B),put(B). tc(instr(jumpge,A)):- put(125),Y is A,neg_byte(Y,B),put(B). tc(instr(jumpeq,A)):- put(116),Y is A,neg_byte(Y,B),put(B). tc(instr(jumplt,A)):- put(124),Y is A,neg_byte(Y,B),put(B). tc(instr(jumpgt,A)):- put(127),Y is A,neg_byte(Y,B),put(B). tc(mov(dl,A)):- put(178),neg_byte(A,B),put(B). tc(instr(pop,bx)):- put(91). tc(instr(inc,cx)):- put(65). tc(instr(push,dx)):- put(82). tc(xor(dx,dx)):- put(49),put(210). tc(xor(cx,cx)):- put(49),put(201). tc(or(ax,ax)):- put(9),put(192). tc(instr(pop,dx)):- put(90). tc(add(dl,A)):- put(128),put(194),neg_byte(A,B),put(B). tc(instr(loop,A)):- put(226),Y is A,neg_byte(Y,B),put(B). tc(mov(cx,[bx])):- put(139),put(15). tc(instr(inc,bx)):- put(67). tc(instr(push,cx)):- put(81). tc(instr(pop,cx)):- put(89). tc(instr(pop,ax)):- put(88). tc(no_op). 8) Il file ‘tointel’ adj(X,instr(push,cs);instr(pop,ds);X). toIntel(Code,XCode2):- adj(Code,XCode),toIntel2(XCode,XCode2). toIntel2((Code1;Code2),(X1;X2)):toIntel2(Code1,X1),toIntel2(Code2,X2). toIntel2(instr(loadc,stack_bp(A)),(mov(ax,bp);add(ax,A))):number(A). toIntel2(instr(load,stack_bp(A)),(mov(bx,bp);add(bx,A);mov(ax,[bx ]))):- number(A). toIntel2(instr(loadp,stack_bp(A)),(mov(ax,bp);add(ax,A);mov(bx,ax );mov(ax,[bx]);mov(bx,ax);mov(ax,[bx]))):- number(A). toIntel2(instr(store,stack_bp(A)),(mov(bx,bp);add(bx,A);mov([bx], ax))):- number(A). toIntel2(instr(addc,stack_bp(A)),(mov(bx,bp);add(bx,A);add(ax,bx) )):- number(A). 78 toIntel2(instr(add,stack_bp(A)),(mov(bx,bp);add(bx,A);add(ax,[bx] ))):- number(A). toIntel2(instr(subc,stack_bp(A)),(mov(bx,bp);add(bx,A);sub(ax,bx) )):- number(A). toIntel2(instr(sub,stack_bp(A)),(mov(bx,bp);add(bx,A);sub(ax,[bx] ))):- number(A). toIntel2(instr(mulc,stack_bp(A)),(mov(bx,bp);add(bx,A);mov(dx,0); instr(mul,bx))). toIntel2(instr(mul,stack_bp(A)),(instr(push,ax);mov(bx,bp);add(bx ,A);mov(ax,[bx]);mov(bx,ax);mov(dx,0);instr(pop,ax);instr(mul,bx) )):- number(A). toIntel2(instr(divc,stack_bp(A)),(mov(bx,bp);add(bx,A);mov(dx,0); instr(div,bx))):- number(A). toIntel2(instr(div,stack_bp(A)),(instr(push,ax);mov(bx,bp);add(bx ,A);mov(ax,[bx]);mov(bx,ax);mov(dx,0);instr(pop,ax);instr(div,bx) )):- number(A). toIntel2(instr(loadc,A),mov(ax,A)). toIntel2(instr(load,A),mov(ax,[A])). toIntel2(instr(jump,A),instr(jmp,A)). toIntel2(instr(call,A),instr(call,A)). toIntel2(instr(loadp,A),(mov(bx,[A]);mov(ax,[bx]))). toIntel2(instr(store,A),mov([A],ax)). toIntel2(instr(storep,A),(mov(dx,ax);mov(bx,A);mov(ax,[bx]);instr (push,ax);instr(pop,bx);mov(ax,dx);mov([bx],ax))). toIntel2(instr(push,0),instr(push,ax)). toIntel2(instr(addc,A),add(ax,A)). toIntel2(instr(add,A),add(ax,[A])). toIntel2(instr(subc,A),sub(ax,A)). toIntel2(instr(sub,A),sub(ax,[A])). toIntel2(instr(mulc,A),(mov(dx,0);mov(bx,A);instr(mul,bx))). toIntel2(instr(mul,A),(mov(dx,0);mov(bx,[A]);instr(mul,bx))). toIntel2(instr(divc,A),(mov(dx,0);mov(bx,A);instr(div,bx))). toIntel2(instr(div,A),(mov(dx,0);mov(bx,[A]);instr(div,bx))). toIntel2(instr(write,_),(mov(bx,10);xor(cx,cx);label(W1);xor(dx,d x);instr(div,bx);instr(push,dx);instr(inc,cx);or(ax,ax);instr(jum pne,W1);label(W2);instr(pop,dx);add(dl,48);mov(ah,2);instr(int,33 );instr(loop,W2))).%write toIntel2(instr(write_str,_),(mov(dx,ax);mov(ah,9);instr(int,33))) .%write_str toIntel2(instr(read,A),(mov(dx,A);mov(ah,10);instr(int,33))).%rea d toIntel2(instr(write_crlf,_),(mov(dl,13);mov(ah,2);instr(int,33); mov(dl,10);mov(ah,2);instr(int,33))). toIntel2(instr(X,Y),instr(X,Y)). toIntel2(no_op,no_op). toIntel2(block(L),block(L)). toIntel2(label(N),label(N)). toIntel2(nop,nop). toIntel2(destroy_local(N),add(sp,N)). toIntel2(end_funz,(instr(pop,bp);instr(ret,0))). toIntel2(create_local(N),sub(sp,N)). toIntel2(init_funz,(instr(push,bp);mov(bp,sp))). toIntel2(halt,instr(int,32)). 9) Il file ‘tostream’ toStream((Code1;Code2)):- toStream(Code1),toStream(Code2). toStream(instr(X,Y)):- write(X),write(' '),write(Y),nl. toStream(nop):- write(nop),nl. toStream(no_op). toStream(block(L)):- write(block(L)),nl. 79 toStream(X):- X=..[mov,A,B|_],write('mov '),write(A),write(','),write(B),nl. toStream(X):- X=..[sub,A,B|_],write('sub '),write(A),write(','),write(B),nl. toStream(X):- X=..[add,A,B|_],write('add '),write(A),write(','),write(B),nl. toStream(X):- X=..[xor,A,B|_],write('xor '),write(A),write(','),write(B),nl. toStream(X):- X=..[or,A,B|_],write('or '),write(A),write(','),write(B),nl. toStream(X):- X=..[movr,A,B|_],write('mov '),write(A),write(','),write(B),nl. toStream(X):- X=..[addr,A,B|_],write('add '),write(A),write(','),write(B),nl. toStream(halt):- write(halt),nl. toStream(X):- write(X),nl. toStream2((Code1;Code2)):- toStream2(Code1),toStream2(Code2). toStream2(instr(X,Y)):- write(X),write(Y). toStream2(nop):- write(90). toStream2(block(L)):- write(block(L)). 80 Capitolo 7 Conclusioni In questa tesi abbiamo sviluppato un compilatore per un sottoinsieme del linuaggio C che compila files eseguibili in ambiente DOS/Windows. Il compilatore può essere utilizzato per scopi didattici e in ambiente accademico. Il progetto del compilatore può essere completato e migliorato nel futuro aggiungendovi nuove funzionalità quali, ad esempio, il riconoscimento di primitive per la programmazione a oggetti, o la creazione di una interfaccia grafica, o la produzione di files eseguibili per altri ambienti (calcolatori e/o sistemi operativi). 11 nov 2004 81 Riferimenti 1. Alfred V. Aho, Ravi Sethi, Jeffrey D. Ullman. Compilers. Principles, Techniques, and Tools. Addison Wesley, 1988. 2. Leon Sterling, Ehud Shapiro. The Art of Prolog. The MIT Press Second Edition, 1994. 3. Alberto Pettorossi. Theory of Computation Vol II and IV. Aracne 2002. 4. Alberto Pettorossi, Maurizio Proietti. First Order Predicate Calculus and Logic Programming. Aracne 2002. 5. Intel 80386 programmer’s reference manual. Intel Corporation 1986. 82