Le assegnazioni usano il solo segno uguale =.
Le espressioni possono essere una qualsiasi combinazione valida dei seguenti
elementi:
+, -, *, /, (, ), |, ~, =, <, >, &, !
Tiny 1.6 BNF
<program> ::= <var-decl> <proc-decl>
<var-decl> ::= [<type> <var-list>]*
<type> ::= INT | SHORT | CHAR
<var-list> ::= <ident> [',' <ident>]*
<ident> ::= <letter> [<letter> | <digit>]*
<proc-decl> ::= [<proc>]*
<proc> ::= VOID <ident> '(' <param-list> ')' <var-decl>
<begin-block>
<param-list> ::= <type> <ident> [',' <type> <ident>]* | null
<begin-block> ::= '{' <block> '}'
<block> ::= [<statement>]*
<statement> ::= <simple-stat> | <struct-stat> | <begin-block>
<simple-stat> ::= <assignement> | <proc-call> | null
<assignement> ::= ['*'] <ident> '=' <b-expression>
<proc-call> ::= <ident> '(' <exp-list> ')'
<exp-list> ::= <b-expression> [',' <b-expression>]* | null
<struct-stat> ::= <if-stat> | <while-stat>
<if-stat> ::= IF '(' <condition> ')' <statement>
[ELSE <statement>]
<while-stat> ::= WHILE '(' <condition> ')' <statement>
<condition> ::= <b-expression>
<b-expression> ::= <b-term> [<orop> <b-term>]*
<b-term> ::= <not-factor> ['&' <not-factor>]*
<not-factor> ::= ['!'] <relation>
<relation> ::= <expression> [<relop> <expression>]
<expression> ::= <term> [<addop> <term>]*
<term> ::= <signed factor> [<mulop> <factor>]*
<signed factor>::= [<addop>] <factor>
<factor> ::= [<punop>] <variable> | <integer>
| '(' <b-expression> ')'
<orop> ::= '|' | '~'
<relop> ::= '==' | '<' | '<=' | '<>' | '>' | '>='
<addop> ::= '+' | '-'
<mulop> ::= '*' | '/'
<punop> ::= '*' | '&'
<variable> ::= <ident>
<integer> ::= [<digit>]*
7.2.2 La Struttura Dati
La struttura dati è basata su tre tabelle di stringhe: una è usata per memorizzare le parole riservate, un'altra per gli identificatori di variabili globali e di funzioni, la terza tabella memorizza i parametri formali e le variabili locali. A ciascuna di queste tabelle è associata un'altra tabella di caratteri; questi caratteri sono utilizzati o come identificatori della parola riservata (per la prima tabella) o come indicatore di tipo (per le altre due tabelle). Ecco le definizioni:
typedef char symbol[SYMBOLSIZE];
symbol kwlist[NKW]; /* Lista Parole Chiave */
char kwcode[NKW+1]; /* Abbreviazioni Parole Chiave */
symbol st[MAXENTRY]; /* Tabella dei Simboli */
char stype[MAXENTRY+1]; /* Tipi dei Simboli */
symbol params[MaxParam]; /* Tabella dei Parametri */
char paramtyp[MaxParam]; /* Tipi dei Parametri */
Gli elementi di queste tabelle sono memorizzati secondo l'ordine di arrivo e la ricerca è svolta in modo sequenziale. Gli sviluppi di questo compilatore dovranno esaminare la possibilità di cambiare queste strutture in tabelle ordinate con ricerca binaria o in tabelle Hash o in alberi o quanto di più sofisticato sia adatto per questo programma.
7.2.3 Input, output e error
Le funzioni di ingresso sono molto semplici, leggono un carattere alla volta
dal flusso d'ingresso che può essere sia un file che la console. Per l'uscita invece vengono scritte in una volta sola
intere stringhe che spesso coincidono con un'intera riga. Sono disponibili
tre tipi di messaggi di errore: Expected, Undefined e Duplicate. L'I/O è
stato predisposto per essere eseguito su due piattaforme dotate di sistemi
operativi completamente diversi: Theos e DOS. Quest'ultima versione è
stata approntata solo per fini di sviluppo.
7.2.4 Scanner, analizzatore lessicale
L'analizzatore lessicale raggruppa i singoli caratteri in "token", questa
parte del codice distingue i caratteri numerici, alfabetici e speciali e li
raggruppa se necessario, in particolare vengono riconosciuti gli
identificatori come un carattere alfabetico seguito da caratteri
alfanumerici.
7.2.5 Parser, analizzatore sintattico e semantico
Programma principale
Il programma principale deve includere, nell'ordine, la dichiarazione di variabili
e la definizione di procedure. Una delle procedure deve essere chiamata "main". Tutti gli identificatori devono essere dichiarati prima di essere usati.
Funzioni utilizzate: main, topdecls
Dichiarazioni di variabili
Le dichiarazioni di variabili globali sono costituite da una serie lunga a
piacere di identificatori di tipo seguiti, ciascuno, da una lista di
variabili. I tipi possono essere scelti solo tra uno dei tre tipi
predefiniti, cioè int, short e char. I primi due sono trattati con segno
mentre l'ultimo senza segno. La lista di variabili è costituita da un
identificatore che può essere seguito da una serie di identificatori
preceduti da una virgola ciascuno.
Funzioni utilizzate: topdecls, talloc, addentry, allocvar
Definizioni di procedure
Le definizioni di procedure sono una serie (anche vuota) di procedure.
Ciascuna procedura deve incominciare con la parola chiave "void" a cui
deve seguire un identificatore ed una lista di parametri formali; poi ci può essere la dichiarazione di variabili locali e, infine, ci deve essere
un blocco di istruzioni.
La lista di parametri formali è composta da una parentesi tonda aperta e
da un numero arbitrario di identificatori di tipo, ciascuno seguito da un
identificatore di variabile; alla fine ci deve essere una parentesi tonda
chiusa. Anche se le variabili di tipo diverso occupano quantità diverse
di memoria, tuttavia i parametri sullo stack occupano tutti la dimensione
massima, cioè 4 byte. Ciò è dovuto al fatto che quando si chiama
una procedura, il compilatore tiny non ha nessuna informazione sul numero e
sul tipo di parametri; infatti nella symbol table viene memorizzato solo
l'identificatore della procedura.
La lista di variabili locali segue la stessa sintassi delle dichiarazioni di
variabili globali ma viene allocato spazio sullo stack; perciò si
provvede a sommare lo spazio utilizzato da ciascuna variabile per saper quanto spazio occupare con
l'istruzione assembly "LINK". Gli identificatori delle variabili locali
vengono posti su una tabella dei simboli locali assieme agli identificatori
dei parametri formali, pertanto devono essere distinti da questi ultimi, ma
possono essere identici ad identificatori di variabili globali.
Funzioni utilizzate: formallist, locdecls, procprolog, beginblock,
procepilog, clearparams, formalparam, renumparams, locdecl, addparam
Blocco di istruzioni
Un blocco di istruzioni incomincia con il simbolo '{'
prosegue con un numero arbitrario di istruzioni e si conclude con il simbolo ".
Ciascuna istruzione può essere un'istruzione semplice, un'istruzione composta o un blocco di istruzioni.
Funzioni utilizzate: beginblock, block, statement
Istruzioni semplici
Le istruzioni semplici sono le assegnazioni e le chiamate di procedure. In
entrambe i casi l'istruzione inizia con un identificatore. Si verifica, per
prima cosa, che questo identificatore sia contenuto nella tabella locale dei
simboli, se non lo si trova lo si va a cercare nella tabella globale dei
simboli.
Dalla tabella dei simboli si ricava se l'identificatore è riferito ad una
variabile; in questo caso si cerca il segno uguale '=' e poi si valuta un'espressione, infine si chiama la routine per memorizzare il risultato
dell'istruzione nella variabile.
Se il primo identificatore si riferisce ad una procedura, viene letta la
lista di parametri che verranno messi sullo stack, viene chiamata la
subroutine desiderata e, infine, viene riportato lo stack nelle condizioni
iniziali. La lista parametri consiste in una serie, anche vuota, di
espressioni separate da virgola e racchiuse fra parentesi tonde. I risultati
di tutte le espressioni, di qualunque tipo siano, vengono estesi a 32 bit
prima di essere spinti sullo stack, perché non è noto con quali
parametri è stata definita la procedura. Comunque anche le variabili
vengono passate per valore.
Funzioni utilizzate: assignorproc, assignment, callproc, typeof, paramlist,
param
Istruzioni composte
Le istruzioni composte iniziano con una parola riservata e si dividono in
due tipi: le istruzioni che controllano il flusso (if-else e
while) e le chiamate di procedure predefinite.
L'istruzione if riconosce dopo l'identificatore "IF", una condizione che non
è altro che un'espressione racchiusa fra parentesi tonde; a questo punto crea una nuova etichetta e
pone l'istruzione di salto nel caso l'espressione dia risultato nullo, a
questo punto viene riconosciuta un'istruzione (che può essere composta da un blocco di istruzioni racchiuse fra parentesi graffe); se vi è una
parola riservata "ELSE" che non è stata riferita ad un if più
vicino, allora si crea una seconda etichetta, si pone un salto incondizionato
alla seconda etichetta, si inserisce la prima etichetta e si legge un blocco
di istruzioni. Infine quando termina l'istruzione, si
inserisce l'ultima etichetta generata.
L'istruzione while riconosce dopo l'identificatore "WHILE" una condizione
che non è altro che un'espressione racchiusa da parentesi tonde, a questo punto crea due nuove
etichette, inserisce la prima etichetta e pone l'istruzione di salto alla
seconda etichetta nel caso l'espressione ritorni risultato nullo. In seguito
viene riconosciuto un'istruzione che può essere composta da un blocco di istruzioni
racchiuse tra parentesi graffe. In fine si pone un salto
incondizionato alla prima etichetta seguito dalla seconda etichetta.
Le procedure predefinite hanno sintassi analoga a quella delle procedure ma
godono di alcune particolarità nel riconoscimento dei parametri (in
alcuni casi ben determinati sono passati per indirizzo, in altri casi
possono essere passate stringhe costanti). Di norma dopo aver riconosciuto i
parametri e averli posti nei registri opportuni pongono una "TRAP" seguita
da un opportuno codice. Come esempio porto il codice della funzione
docreate che serve ad attivare la chiamata di sistema che crea un nuovo thread:
/*--------------------------------------------*/
/* Process a Thread_Create Statement */
void docreate (e)
TIMP *e;
{
next(e); /* leggi prossimo token */
matchstring(e,"("); /* verifica se token = parentesi */
if (typeof(e, e->value)=='p') /* se parametro e' una procedura */
createit(e, e->value); /* emetti chiamata exec */
else
expected(e, "Procedure Descriptor");
next(e); /* leggi prossimo token */
matchstring(e,")"); /* verifica se e' un parentesi */
}
Funzioni utilizzate: doif, dowhile, doread, readvar, dowrite, writetoken,
doexec, docreate, domutexcreate, domutexdestroy, dolock, dounlock, doexit,
dokill.
Espressioni
la valutazione di un'espressione avviene mediante sette gradi di priorità
con ricorsione. La forma generale per ciascun livello è:
<espressione> ::= <operando1> <operatore> <operando2>
Questa sintassi vale per un'operazione binaria, nel caso di un'operazione
unaria vale lo stesso schema con le dovute semplificazioni. Questa
operazione viene tradotta in una forma generalizzata dal seguente
pseudocodice:
tipo1 = valuta(operando1)
while (operatore) {
push(tipo1)
tipo2 = valuta(operando2)
pop(tipo1)
tipo1 = converti(tipo1,tipo2)
operazione(tipo1)
}
restituisci tipo1
a titolo di esempio si consideri l'operazione di prodotto logico (AND) la cui
BNF è:
<b-term> ::= <not-factor1> ['&' <not-factor>]*
Il codice corrispondente è:
/*-------------------------------------------*/
/* Parse and Translate a Boolean Term */
char boolterm (e)
TIMP *e;
{
char typ;
typ = notfactor(e);
while (e->token == '&') {
char t;
push(e, typ);
next(e);
t = notfactor(e);
pop(e, typ);
typ = sametype(e, typ, t);
popand(e, typ);
}
return typ;
}
Conversione di tipo
In ogni operazione i tipi degli operandi sono convertiti nel tipo del
risultato. In genere il tipo del risultato è uguale al tipo più
grande degli operandi. L'unica eccezione si ha nelle operazioni di
moltiplicazione per cui il risultato è a 16 bit solo se entrambi gli
operandi sono a 8 bit, altrimenti il risultato è a 32 bit. Il primo
operando si trova nel registro D7 (vi viene messo dalla pop che lo preleva
dallo stack) mentre il secondo è in D0 (tutti i risultati parziali
vengono messi in D0). Vengono testati entrambe gli operandi per verificare
se sia necessaria l'estensione. Se è necessaria, allora per ciascun
operando si controlla se è di tipo byte, nel qual caso si estende a word
ponendo a zero gli otto bit alti (si presume che le variabili di tipo char siano senza segno),
poi si testa se l'altro operando è di tipo long, nel qual caso si estende
tenendo conto del segno.
Funzioni utilizzate: sametype, promote
7.2.6 CodeGen, generatore di codice oggetto assembler
Il generatore di codice è composto da una cinquantina di funzioni,
ciascuna delle quali emette il codice assembler corrispondente ad una
funzione elementare. In particolare la push sposta un operando dal registro
D0 alla cima dello stack mentre la pop opera tra lo stack e il registro D7.
Le funzioni di confronto estendono sempre il risultato da 8 a 16 bit perché l'istruzione assembler Scc genera un risultato ad 8 bit ma con segno (0
se la condizione è falsa, -1 se vera), pertanto se si dovesse estendere
il risultato secondo le convenzioni utilizzate altrove in tiny (cioè
considerando i numeri ad 8 bit senza segno) si otterrebbe 255 invece di -1.
Come esempio riporto la setequal.
/*-----------------------------------------------*/
/* Set D0 If Compare was = */
void setequal (e)
TIMP *e;
{
emitln(e,"SEQ D0");
emitln(e,"EXT.W D0");
}
7.3 Assemblatore
L'assemblatore incluso in questo sistema è stato scritto da Paul McKee nel 1986. L'autore ha dichiarato che il programma dovrebbe essere portabile (con modifiche ragionevoli) in qualsiasi ambiente che usi il linguaggio C con interi a 32 bit. Tuttavia ci sono state grosse difficoltà nell'adattare al sistema operativo Theos questo assemblatore. Al momento l'assemblatore talvolta può fornire risultati scorretti o inaccurati, fortunatamente ci si può rendere conto facilmente se l'esecuzione è andata a buon fine oppure no. Un importante sviluppo futuro consisterà nel reperire un altro assemblatore che si adatti meglio al sistema.
Questo programma è un assemblatore a due passi per i microprocessori 68000 e 68010. Riconosce l'intero insieme delle istruzioni e un ragionevole gruppo di direttive. Il file prodotto in uscita normalmente è un file binario eseguibile con lo stesso nome del file d'ingresso ma con estensione modificata in ".h68". È possibile ottenere anche un file di verifica (listing file) mediante l'opzione "-ln". Questo file, che ha estensione ".lis", riporta sia il codice in linguaggo macchina sia quello in assembly visualizzato riga per riga.
Direttiva | Descrizione |
ORG | fissa la locazione iniziale |
EQU | definisce simboli costanti |
SET | definisce costanti ridefinibili |
REG | definisce lista di registri |
DC | definisce costante |
DCB | definisce blocco costante |
DS | definisce blocco non inizializzato |
END | fine del file sorgente |
|
Tabella 7.2: Lista direttive dell'assemblatore
Il file d'ingresso è un file di testo contenente istruzioni, direttive dell'assemblatore e commenti. Ciascuna riga può essere lunga al massimo 256 caratteri. Il file d'ingresso può avere qualsiasi estensione ma, per motivi d'ordine, tutti i file contenenti codice assembly hanno estensione ".a". L'assemblatore non distingue fra le lettere maiuscole e minuscole.
Le direttive dell'assemblatore sono riportate nella tabella 7.2
7.4 Esecuzione di processi
Per mandare in esecuzione un file eseguibile basta digitarne il nome completo (non è riconosciuto nessun tipo di estensione). Eventualmente può essere utile terminare il comando con carattere ' che attiverà il processo in background.
In sintesi i passi per creare una nuova applicazione sono i seguenti:
- scrivere un file sorgente utilizzando l'editor di testo. Per attivarlo la riga di comando sarà del tipo:
programma.c
Si scrive il testo del programma nel linguaggio simile al C riconosciuto dal compilatore. Si termina la scrittura salvando il file con il comando
Esc-z
- compilare il file sorgente usando il compilatore tiny. La riga di comando dovrà includere anche il nome del file destinatario:
programma.c programma.a
- assemblare il file assembly scrivendo:
programma.a
- mandare in esecuzione il file eseguibile generato dall'assemblatore digitandone il nome:
programma.h68
Back to Lucio's Home page.
|