Storia dei linguaggi e struttura di un compilatore

Lezione 1 - I primi linguaggi: basso livello
Corso — Programmazione
Linguaggi di programmazione— Storia della programmazione
Marco Anisetti
e-mail: [email protected]
web: http://homes.di.unimi.it/anisetti/
Università degli Studi di Milano — Dipartimento di informatica
Il primo linguaggio teorico
• Charles Babbage è stato il matematico e filosofo
britannico, che per primo ebbe l'idea di un calcolatore
programmabile (macchina differenziale e macchina
analitica)
• Ada Lovelace, discepola e collaboratrice di Babbage,
definì il primo linguaggio di programmazione (1837).
• Era un linguaggio di tipo assemblativo, ma lavorava su
una macchina teorica
• Introdusse il concetto di ciclo ripetuto e il concetto di
variabile indice.
• La realizzazione della macchina analitica non fu mai
portata a termine.
Primi calcolatori (1)
• Lo Z3 è il primo calcolatore totalmente programmabile e
totalmente automatico (ideato dall'ingegnere tedesco
Konrad Zuse) (1941)
• Nel 1998 è stato dimostrato che lo Z3 è Turing completo (
stesso potere computazionale di una macchina di Turing
universale)
• Zuse fu il primo programmatore della storia ad agire su
una macchina realmente programmabile
• Lo Z3 fu utilizzava il codice binario
• Utilizzava una tecnologia Elettromeccanica
• Atanasoff-Berry Computer (ABC) completamente
elettronico
• Non era Turing completa
• Elaborazione e memorizzazione separati
Primi calcolatori (2)
• Electronic Numerical Integrator and Computer
• Il primo computer elettronico general purpose della storia
• Costruito dal 1943 al 1945
• Programmazione definita da circuiti elettrici
• L'ENIAC era decimale mentre lo Z3 era già binario.
• Programmare l'ENIAC voleva dire riscrivere il programma
mentre lo Z3 poteva caricarlo da nastro perforato.
• Tutti gli attuali computer hanno un disegno progettuale
che ricorda più lo Z3 che l'ENIAC.
• La macchina non aveva un programma registrato: sei
donne erano impegnate a muovere commutatori e
connettere cavi.
Linguaggio di programmazione concreto
• Konrad Zuse, sviluppò il Plankalkül (1943 1945 ).
• Sviluppò il linguaggio mentre se ne stava nascosto sulle
Alpi della Baviera in attesa della fine della Seconda Guerra
Mondiale.
• Usò il suo linguaggio come opponente nel gioco degli
scacchi sul suo computer Z3.
• Il linguaggio era già in grado di gestire sia tabelle che
strutture di dati.
• Il Plankalkül rimase seppellito in qualche archivio in
Germania per molto tempo.
Linguaggio macchina(1)
• Una sequenza di bit. LOAD 8 potrà essere rappresentata
in una macchina reale come 00110100, dove 0011 è la
rappresentazione interna del codice operativo LOAD
• I linguaggi di programmazione coincidevano con l'insieme
delle istruzioni eseguibili dall'hardware.
• Enorme sforzo richiesto per codificare algoritmi semplici.
Linguaggio macchina(2)
• Sono specifici della macchina.
• Ogni CPU ha il proprio linguaggio macchina.
• Occorre conoscere l architettura della macchina per
scrivere programmi.
• I programmi non sono portabili.
• I codici sono illeggibili all' uomo.
• I programmatori si specializzano nel cercare efficienza su
una macchina specifica, anziché concentrarsi sul
problema.
Linguaggio assembly
• Codifica di tipo simbolico, anziché binaria, dei programmi:
è meglio risparmiare il tempo dell'uomo anche a costo di
sprecare tempo macchina (una parte del tempo è dedicata
alla traduzione di programmi, non alla loro esecuzione
diretta)
• In assembly ogni istruzione è identificata da una sigla
piuttosto che da un numero e le variabili sono
rappresentate da nomi piuttosto che da numeri.
• I programmi scritti in assembly necessitano di un apposito
programma assemblatore per tradurre le istruzioni tipiche
del linguaggio in istruzioni macchina.
• Oggi si utilizza l' assembly solo se esistono vincoli
stringenti sui tempi di esecuzione.
Lezione 2 - I primi linguaggi: alto livello
Corso — Programmazione
Linguaggi di programmazione— Storia della programmazione
Marco Anisetti
e-mail: [email protected]
web: http://homes.di.unimi.it/anisetti/
Università degli Studi di Milano — Dipartimento di informatica
Linguaggi ad alto livello
• Tra gli anni 50 e 60 si passo ai linguaggi ad alto livello,
anche se qualche concetto era già stato introdotto prima
• Si tratta di un passaggio stabilito da un maggior uso di tali
linguaggi
• Essi richiedono un compilatore o un interprete che sia in
grado di tradurre le istruzioni del linguaggio di alto livello
in istruzioni macchina di basso livello le uniche eseguibili
dal calcolatore
• Compilatori ed interpreti sono di gran lunga più complessi
di un assemblatore
FORTRAN
• Il FORmula TRANslator è stato sviluppato da un gruppo di
programmatori della IBM guidati da John Backus e
pubblicato per la prima volta nel 1957.
• Era stato progettato per facilitare la traduzione in codice
di formule matematiche.
• Data la sua semplicità di scrittura i programmatori
riuscivano ad essere fino a 500 volte più veloci in
FORTRAN che in altri linguaggi. E stato il primo linguaggio
``problem oriented'' anziché ``machine oriented''.
COBOL
• Sviluppato nel 1959 da un gruppo di professionisti riuniti
alla Conference on Data Systems Languages (CODASYL).
• Da allora ha avuto molte modifiche e miglioramenti. Nel
tentativo di superare le numerose incompatibilità tra le
varie versioni, l'American National Standards Institute
(ANSI) annunciò uno standard nel 1968, che produsse
l'ASN-COBOL.
• Il linguaggio continua ad evolversi ancora oggi, che è
disponibile in una versione object-oriented inclusa nel
COBOL 2002
LISP
• Nel 1958 John McCarthy fu incaricato di creare una lista di
specifiche per creare l'elaborazione simbolica.
• Il List Processing Language fu integrato come estensione
nel FORTRAN stesso.
• LISP segue i paradigma funzionale
• Un programma LISP può modificare se stesso crescendo
• Sia i dati sia i programmi sono delle liste. Per questo
motivo un programma in LISP può trattare altri
programmi al suo interno.
• E utilizzato nello studio della intelligenza artificiale.
Contiene il concetto di Garbage Collector
Linguaggi degli anni 60
• Algol - ALGOrithmic Language
Primo linguaggio con una sintassi formalmente definita
(BNF)
Primo linguaggio basato sui principi della programmazione
strutturata
Evoluzione: Algol 58, 60 e 68, parente del C e del PASCAL
Introdusse concetti fondamentali come il
record di attivazione
• BCPL Basic Combined Programming Language erede
di CPL mai decollato e progenitore di B e quindi anche di C
• Simula-67
Introduce il concetto di classe e oggetto
E' il progenitore del Java e di tutti i linguaggi orientati agli
oggetti
Linguaggi degli anni 70
• Pascal
Sviluppato per l' insegnamento
Ha avuto un grande successo anche nel mondo dell'
industria
Ha visto svilupparsi di versioni visuali (Delphi)
• Prolog
Introduce la programmazione logica
Si basa sul calcolo dei predicati del primo ordine
• Smalltalk
Prime interfacce grafiche
Object Oriented
• C
Concepito inizialmente come una sorta di assembler
strutturato
E diventato il linguaggio più affermato nella
programmazione di sistema
Linguaggi degli anni 80
• Ada
Omaggio alla prima programmatrice
ADA doveva rappresentare il punto di maturazione perfetta
di tutti i principi di costruzione del software e dei relativi
meccanismi linguistici elaborati negli anni precedenti.
• C++
Uno dei più usati linguaggi Object Oriented fino all'avvento
di Java
E' un'estensione del C, sviluppato da Bjarne Stroustrup
presso i Bell Laboratories.
Linguaggio Basic
• Evoluzione del Basic
Sviluppato nel 1964 presso l'Università di Dartmouth
Fu progettato per essere un linguaggio semplice da
imparare, nacque come linguaggio per principianti
Originariamente progettato come linguaggio compilato,
molte delle sue versioni più note sono interpretate
Una delle più famose versioni è il Microsoft BASIC,
sviluppato da Bill Gates, Monte Davidoff e Paul Allen
Il BASIC ha subito notevoli evoluzioni e cambiamenti,
diventando un linguaggio strutturato con potenzialità molto
simili a quelle di altri linguaggi più evoluti (e.g. Visual
Basic)
Nonostante le carenze si diffuse molto grazie ai personal
computer (Comodore64 basic, Atari e Amiga basic ecc.)
Basic versioni
• Non strutturato
10 LET C = A2 * A3 - A2
15 IF C = 0 THEN 55
20 READ D1, D2
• Strutturato
DO
INPUT Inserisci un numero , Num
LOOP WHILE num > 0
• A oggetti
PUBLIC SUB Button1Click()
DIM Num, Name AS String
IF TextBox1.Text <> `` THEN
Name = TextBox1.Text
ENDIF
END
Lezione 3 - Linguaggi attuali
Corso — Programmazione
Linguaggi di programmazione— Storia della programmazione
Marco Anisetti
e-mail: [email protected]
web: http://homes.di.unimi.it/anisetti/
Università degli Studi di Milano — Dipartimento di informatica
Linguaggi degli anni 90
• Il linguaggio Java è derivato da un linguaggio chiamato
OAK, che fu sviluppato nei primi anni '90 alla Sun
Microsystem come linguaggio piattaforma indipendente
predisposto per applicazioni di intrattenimento come
console per video game e VCR per comunicazioni.
• OAK fu impiegato per la TV via cavo, per ordinare i
programmi da vedere.
• Mentre quel tipo di spettacolo on-demand tramontava, il
World Wide Web, invece, riscontrava sempre più interesse.
A quel punto i tecnici sviluppatori di OAK ci si buttarono a
capofitto, trasformando il programma OAK nel nuovo Java.
Java
• J. Gosling e A. Van Hoof si trovavano spesso ad un caffè
presso il quale discutevano del linguaggio stesso. E così il
linguaggio prese il nome da tale abitudine (Java è una
qualità di caffè dell'omonima isola dell'Indonesia).
• Il magic number che identifica un file .class è 0xCAFEBABE
(probabilmente riferendosi alla cameriera che li serviva).
C#
• La Microsoft propose alla Sun un accordo per apportare
modifiche al linguaggio, la Sun rifiuta, allora Microsoft
dichiara guerra alla Sun sviluppando il linguaggio C#
(siamo nel 2000), sviluppato sulla base di JAVA (ovvero
java con le modifiche che Microsoft aveva proposto alla
Sun che rifiutò)
Linguaggi di scripting
• Sono dei linguaggi di programmazione interpretati,
destinati in genere a compiti di automazione del sistema
operativo, lavori batch o usato per programmazione web
• La vera evoluzione si ebbe con l'introduzione del Common
Gateway Interface (CGI)
• Il CGI permise ai linguaggi di scripting di controllare i web
server per generare web dinamico
• Linguaggi famosi per questi scopi sono PHP, JSP e ASP,
Ruby e Python
• Perl iniziò come linguaggio di script ma poi si evolse in un
linguaggio adatto a coprire problematiche più ampie
Conclusioni unità
• In questa unità abbiamo visto rapidamente la storia della
programmazione fino quasi ai giorni nostri
• Abbiamo ripercorso una strada già vista nelle scorse
lezioni attraverso l'approccio evoluzionistico adottato nel
corso
• Da notare come concetti ad oggi affermati siano nati
molto tempo prima ma abbiano richiesto molto tempo per
concretizzarsi in un linguaggio effettivamente utilizzato
dalla massa dei programmatori
• L'evoluzione è un processo lento ma inesorabile e pilotato
dalle necessità
Lezione 1 - Introduzione
Corso — Programmazione
Linguaggi di programmazione— Compilatori ed interpreti
Marco Anisetti
e-mail: [email protected]
web: http://homes.di.unimi.it/anisetti/
Università degli Studi di Milano — Dipartimento di informatica
Traduttori(1)
• La macchina astratta per la quale è scritto un programma
ad alto livello viene implementata sulla macchina reale
attraverso un traduttore
• Esistono 3 tipologie di traduttori: Compilatori, Interpreti,
Traduttori ibridi
• Compilatore: È un programma che traduce un
programma scritto in linguaggio ad alto livello in un
programma equivalente nel linguaggio macchina (della
macchina dove verrà eseguito)
Traduttori(2)
• Interprete: È un programma che simula direttamente la
macchina astratta sulla macchina concreta attraverso i
seguenti passi alcuni dei quali in comune con un
compilatore:
1. Verifica correttezza sintattica
2. Effettua la traduzione
3. Esegue il codice tradotto
Libro consigliato per approfondimenti: Compilers Principles, Techniques, and tools
Alfred V. Aho, Ravi Sethi, Jeffrey D. Ullman
Discende dal famosissimo libro chiamato Dragon book: Principles of Compiler Design
(difficile da reperire)
Compilatori (1)
• Genericamente traducono da un linguaggio ad un altro e
si occupano di controllare eventuali errori nel linguaggio
sorgente rispetto alla sua grammatica
• Quasi sempre traducono i programmi (sorgente) in
codice macchina specifico per una determinata
architettura hardware (oggetto)
• L'oggetto viene poi assemblato con altre funzioni di
libreria, o altri programmi, utilizzate dal programma
stesso tramite un linker formando l' eseguibile
Compilatori (2)
• Un programma per essere eseguito deve essere caricato
in memoria, il loader, si occupa del caricamento
• Abbiamo visto come è definito lo spazio di memoria
allocato al programma nel momento che viene eseguito
(dipende dal S.O.)
• La compilazione non genera obbligatoriamente il
linguaggio macchina della macchina ospite
(cross-compilatori)
• La cross-compilazione permette di creare eseguibili per
una piattaforma non dotata di un compilatore installato
• Esistono moltissimi tipi di compilatori (singolo passo, multi
passo...) nonostante questo i mattoni base son sempre gli
stessi
Compilatori (3)
Interpreti (1)
• Gli interpreti devono essere sempre attivi durante
l'esecuzione del programma principale
• Precisamente l'interprete è l'unico programma in
esecuzione, ed esegue il programma interpretato
• Esistono due tipologie di interpreti:
Machine Interpreters: simulano l'esecuzione di un
programma compilato per una particolare architettura es
Java usa un interprete bytecode e la Java Virtual Machine
(JVM)
Language Interpreters: simulano l'effetto
dell'esecuzione di un programma (senza compilarlo) scritto
per un particolare set di istruzioni. Una forma di
Intermediate Representation (IR), per esempio un AST
viene utilizzato per l'esecuzione es Ruby
Interpreti (2)
Vantaggi:
• Fase di debug (interattivo) facilitata permettendo
l'aggiunta o modifica di codice durante l'esecuzione
• Gli interpreti supportano a pieno l'indipendenza dalla
macchina
Svantaggi:
• Non si può ottimizzare il codice
• Esecuzione molto lenta
• Il tempo di startup ingombrante soprattutto per piccoli
programmi
• Sostanziale overhead di spazio
• Gli interpreti sono sfruttati prevalentemente per il debug e
lo sviluppo del programma, mentre e i compilatori per la
produzione dell'applicazione.
Interpreti (3)
• Le fasi di un interprete possono essere mappate sulle fasi
relative ad un parser che produca un output in formato
intermedio eseguibile dall'interprete
• Parsing: Analisi lessicale, sintattica e trasformazione in
rappresentazione intermedia spesso come albero AST.
• Esecuzione: La rappresentazione intermedia viene
eseguita in accordo con la semantica del linguaggio
• Esistono interpreti per un linguaggio scritti in un altro
linguaggio es Lispy (lisp interpretato con Python)
Traduttori ibridi
• Alcune versioni di Lisp permettono l'esecuzione di
programmi parzialmente interpretati e compilati
• Un approccio ancora più interessante è quello che sfrutta
il bytecode es. Java.
• E' un linguaggio macchina che però non è fruibile
direttamente dal processore, ma deve essere interpretato
dalla Java Virtual Machine (JVM), la quale non è altro che
un ulteriore strato di software che si interpone tra la
macchina reale ed il programma.
• Questo rende quindi il programma indipendente dal
processore (molto utile per il deploy su cellulari purchè
dotati di una JVM compatibile)
Compilazione ed interpretazione in
RAPTOR(1)
• RAPTOR permette di eseguire il flow chart del progetto in
due modalità
• Interpretazione: modalità nella quale le singole istruzioni
vengono interpretate blocco per blocco. Utile per il debug
e per i primi programmi semplici
• Compilazione: il flow chart viene pseudo-compilato e può
essere eseguito all'interno di raptor in maniera più
performante. Le performance derivano soprattutto dal
fatto che non viene più gestita la visualizzazione del flusso
del flow chart, ma viene direttamente eseguito
Compilazione ed interpretazione in
RAPTOR(2)
• RAPTOR permette anche di fare il packaging delle
applicazioni nella modalità standalone. In tale modalità
viene generato un eseguibile del programma associato
alle librerie di raptor basilari per la sua esecuzione.
Conclusioni
• Un programmatore deve sapere bene come funziona il
traduttore del linguaggio che utilizza
• Agli albori era fondamentale per poter scrivere codice
ottimizzato
• Ad oggi meno determinante per l'ottimizzazione dato che i
traduttori possono ottimizzare il codice
• Fondamentale comunque per capire il meccanismo delle
librerie, la portabilità del codice
• Interessante applicazione di tecniche di programmazione
evolute
• Steve Yegge: <<If you don't know how compilers work,
then you don't know how computers work>>
Lezione 2 - Compilatori parte I
Corso — Programmazione
Linguaggi di programmazione— Compilatori ed interpreti
Marco Anisetti
e-mail: [email protected]
web: http://homes.di.unimi.it/anisetti/
Università degli Studi di Milano — Dipartimento di informatica
Compilatori
• Il compilatore è esso stesso un programma (i primi furono
scritti in assembler)
• I compilatori Pascal e C sono detti auto-compilanti perchè
furono scritti negli stessi linguaggi (Il primo compilatore
auto-compilato, fu creato per il linguaggio Lisp da Hart e
Levin 1962)
• Ovviamente il primo compilatore di quel linguaggio deve
essere per forza scritto in un altro linguaggio (problema
del bootstrapping) oppure compilato facendo operare il
compilatore come un interprete
• Storicamente furono scritti con grandi ``ottimizzazioni''
date le limitate capacità dei calcolatori
• I compilatori consentono ai programmatori di ignorare i
dettagli machine-dependent della programmazione
(compilatori visti come delle black-box)
Compilatori: obiettivi
• Correttezza (errori ortografici e logici) e Sicurezza (Java
Bytecode Verifier)
• Protezione della proprietà intellettuale (parziale se non
espressamente offuscati)
• Efficienza
• Supportare l'espressività del linguaggio
• Offrire un ``ambiente di programmazione''
• Consentono indirettamente di sfruttare le opportunità
offerte dai dettagli architetturali di basso livello: Scelta
delle istruzioni, Metodi di indirizzamento, Pipelines, Uso
della cache, Parallelismo a livello di istruzione
I compilatori sono necessari per coprire il gap tra i linguaggi di alto livello e quelli di
basso
Sistema di processazione del linguaggio
Struttura di un compilatore(1)
• Ci sono due fasi fondamentali in un compilatore: Analisi e
Sintesi
• Analisi: viene spezzato il sorgente nei suoi pezzi
costituenti e creata una rappresentazione intermedia del
programma sorgente attraverso analisi Lessicale,
Sintattica, Semantica ed un
Generatore di codice intermedio.
• Sintesi: viene creato, partendo dalla rappresentazione
intermedia, il programma target equivalente attraverso il
generatore di codice e l' ottimizzatore.
Struttura di un compilatore(2)
Struttura di un compilatore(3)
• Due attività vengono utilizzate in quasi tutte le fasi
appena viste e questi sono:
• Symbol table: Una funzione essenziale di un compilatore
è quella di registrare gli identificatori usati e i loro
attributi. Si tratta di una struttura dati contenente un
record per ogni identificatore con i relativi attributi. La
fase lessicale individua le entry della tabella ma non gli
attributi che vengono definiti nelle fasi successive. Gli
attributi vengono usati in fasi come l'analisi semantica e la
generazione del codice intermedio.
• Gli attributi della symble table vengono aggiornati durante
i vari passaggi
Struttura di un compilatore(4)
• Error Handler: In ogni fase possono capitare errori che
devono essere gestiti per evitare che ad esempio si
termini la fase di compilazione troppo frettolosamente. Gli
errori sono soprattutto gestiti dalle fasi sintattiche e
semantiche.
Lessicale: valuta se i token appartengono al linguaggio.
Sintattica valuta se le stringhe di token validi violano le
regole strutturali del linguaggio.
Semantica valuta se le strutture sintatticamente corrette
hanno un significato valido per gli operatori coinvolti
Programmi a supporto (1)
• Preprocessore: Genera l'input per il compilatore
Processa le macro
Inclusioni di file esterni
Preprocessatori razionali che aggiungono a vecchi linguaggi
non ben strutturati, costrutti per il controllo di flusso più
attuali attraverso macro specifiche non appartenenti al
linguaggio
Estensioni al linguaggio che permettono di aggiungere
capacità al linguaggio attraverso macro
Programmi a supporto (2)
• Assemblatore: Alcuni compilatori generano codice
assembler che viene quindi passato ad un assemblatore
per la generazione di codice macchina rilocabile da
passare al linker/loader
Primo passo: trovo tutti gli identificatori che indicano una
locazione di storage e li inserisco in una symble table (non
la stessa del compilatore) indicando anche la locazione
Secondo passo: rifaccio la scansione e trasformo le
operazioni nella sequenza di bit relativa che rappresenta
l'operazione nel linguaggio macchina usando anche le
locazioni della symble table trasformate in indirizzi
Programmi a supporto (3)
• Loader e linker: Agisce sul codice rilocabile associandolo
ad un area di memoria usando un indirizzo di base o
modificando gli indirizzi relativi nel codice macchina.
• Il linker permette di associare più codici macchina in un
unico programma. Per far questo occorre tener traccia dei
riferimenti esterni
• I riferimenti esterni vengono risolti nelle chiamate corrette
Front end Back end
• Le fasi possono anche essere logicamente raggruppate nel
momento in cui si vuole sviluppare un compilatore:
• Front end: include fasi lessicale, sintattica, creazione
della symble table, semantica, generazione del codice
intermedio (anche una parziale ottimizzazione a volte),
error handling che riguarda queste fasi
• Back end: tutta la parte specifica della macchina target e
che non dipende dal linguaggio di programmazione ma dal
linguaggio intermedio. Ottimizzazione specifica,
generazione del codice, symble table e error handling di
queste fasi
• Cross compilazione: stesso front end ma back end
specifici
• Front end multipli per la stessa macchina (non molto
successo date le differenze tra linguaggi)
Classificazione di un compilatore (1)
• Numero di passi: compilatori a singolo passo compilatori a più passi
• Ottimizzazione: di tempo spazio e potenza dissipata
• Linguaggio oggetto prodotto
Pure machine code: generato senza assumere la
presenza di un particolare sistema operativo o libreria di
funzioni
Classificazione di un compilatore (2)
• Augmented machine code: generato per un particolare
set di istruzioni macchina arricchito considerando la
presenza di un sistema operativo o di una collezione di
routine di supporto ad esempio all' I/O e per l' allocazione
di memoria. Differenza tra codice virtuale generato e
hardware
• Virtual machine code: composto esclusivamente da
codice virtuale, permette di generare codice eseguibile
indipendente dall'hardware.
Un compilatore ``Just in Time'' (JIT) può tradurre porzioni di virtual code in codice
nativo per velocizzare l'esecuzione.
Classificazione di un compilatore (3)
• Formato target prodotto:
Assembly Language: viene prodotto un file di testo
contenente il codice sorgente assembler. Alcune decisioni
di generazione vengono lasciate all'assemblatore (symbolic
format). Il fatto che si generi codice assemply semplifica il
dubug e la comprendione di come funziona un copilatore
(approccio didattico)
Relocatable Binary Format: il codice può essere
generato in un formato binario con riferimenti esterni e con
gli indirizzi dei dati non ancora soggetti a vincoli. La
determinazione degli indirizzi avviene rispetto all'indirizzo
di base dell'oggetto o rispetto ad una unità denominata
simbolicamente. Il linker consente di aggiungere le librerie
di supporto e altre routine compilate separatamente
producendo un formato binario assoluto ed eseguibile del
programma.
Classificazione di un compilatore (4)
• Memory-Image: il codice compilato può essere caricato
in memoria ed immediatamente eseguito. La possibilità di
utilizzo di librerie può essere limitata. Il programma deve
essere ricompilato per ogni esecuzione (Absolute Binary)
Compilatori a due passi
• Sfrutta l'aggregazione front end, back end
• Front end: il compilatore traduce il sorgente in un
linguaggio intermedio dipende dal linguaggio sorgente,
ma è indipendente dalla macchina target
• Back end: generazione del codice oggetto e
l'ottimizzazione, indipendente dal linguaggio sorgente, ma
dipende dalla macchina target
Implicazione della compilazione a due passi : rende più semplice la costruzione di un
compilatore per un nuovo processore (retargeting), in aggiunta consente di progettare
compilatori con multipli front-end.
Lezione 3 - Compilatori parte II
Corso — Programmazione
Linguaggi di programmazione— Compilatori ed interpreti
Marco Anisetti
e-mail: [email protected]
web: http://homes.di.unimi.it/anisetti/
Università degli Studi di Milano — Dipartimento di informatica
Analisi Lessicale
• Legge il programma sorgente carattere per carattere e
ritorna i token del programma sorgente. I token sono gli
elementi minimi di un linguaggio (parole chiave, nomi di
variabili, operatori).
• Implementato come automa a stati finiti deterministico
• Programma di scan o scanner
• Identificatori memorizzati nella symble table assieme agli
attributi rilevabili
Esempio: 32+29 +10 diventa <num,32>
<+,><num,29><+,><num,10> con num il token che
rappresenta gli interi
L'attributo non ha ruolo nel parsing ma serve
successivamente
Fase di analisi Lessicale(1)
• Vengono eliminate le informazioni non necessarie presenti
nel codice sorgente (commenti)
• Vengono processate le direttive di compilazione (include,
define, etc)
• Vengono scoperti eventuali errori nel lessico
• Si tiene traccia nell'error handling della riga in cui è stato
rilevato l'errore
a = b ∗ 10
token:
a identificatore
:= assegnamento
b identificatore
* operatore moltiplicazione
10 numero
Fase di analisi Lessicale(2)
• Indentificatori: Utilizzati come nomi per variabili
funzioni ecc
Per una grammatica un identificatore è un token
contatore := contatore + incremento per la grammatica è
una cosa del tipo id := id + id
I lessemi (contatore e incremento) servono per capire a
che istanza è associato il token e vengono inseriti nella
symble table
Un lessema è una sequenza di caratteri che genera un
match con un pattern per le generazione di token
I lessemi vengono scritti nella symble table e un puntatore
ai lessemi viene associato al token id come attributo
• Keyword: identificano i costrutti, sono costruiti secondo
le regole che generano un identificatore
Problema: capire se un lessema è un identificatore o una
keyword
Si risolve con le keyword riservate
Una keyword è generata da un pattern unico per ogni
keyword
• Pattern: Un token tipo relazione è espresso dall'elenco
Fase di analisi Lessicale(3)
• L'analizzatore lessicale quindi legge i caratteri li associa in
lessemi e passa i token formati dai lessemi con i relativi
attributi allo stage successivo del compilatore
• A volte per farlo deve leggere avanti. Esempio se legge <
deve leggere il prossimo carattere per capire se si tratta di
<=
• A livello lessicale solo pochi errori possono essere
identificati, data la visione molto localizzata
dell'analizzatore lessicale
• Esempio fi (a==f(x))... è un if scritto male o un
identificativo di funzione?
Fase di analisi Lessicale(4)
• Grammatica:
stmt → if expr then stmt | if expr then stmt
else stmt | expr → term relop term | term
term → id | num
• I terminali in grassetto sono generati da espressioni
regolari
if → if; then → then; else → else
relop → <|>|<=|>=|!=|=
letter → A|B|· · · |Z|a|· · · |z
digit → 0|1|· · · |9
id → letter(letter|digit)∗
num → digit + (. digit + )?(E(+|-)?digit + )?
• L'operatore ? indica zero o una istanza
• Notazione esponenziale 6.44E-5
• Questo num rappresenza interi senza segno e valori reali
con notazione esponenziale stile Pascal
Fase di analisi Lessicale(5)
• Considerando i lessemi separati da spazi bianchi allora
servirebbero anche le definizioni di questi spazi
delim → blank | tab | newline
ws → delim+
• Se l'analizzatore lessicale trova un ws non torna token
Fase di analisi Lessicale(6)
• Dato che si usano le espressioni regolari lo strumento che
serve per riconoscerle è la ASFD a volte uno strumento
analogo chiamato transition diagram
• Il transition diagram permette di ritornare in modo
esplicito anche l'attributo associato al token che in questa
fase potrebbe essere:
Un puntatore alla symbol table per identificatori (id) e
numeri (num)
Una costante simbolica che dice cosa fa il simbolo letto es:
LT per il simbolo < con token relop
Fase di analisi Lessicale(7)
• Le tecniche usate per la scrittura di un analizzatore
lessicale possono essere usate in molti altri ambiti
• Il problema è quello di specificare e progettare programmi
che eseguono azioni scatenate da pattern (linguaggi
specifici pattern programming come lex)
• Descrizione di pattern tramite espressioni regolari
• Tool per la generazione di un analizzatore lessicale
Fase di analisi Lessicale:tool(1)
• Un tool per la generazione di analizzatori lessicali molto
famoso è il Lex
• Si usa il linguaggio lex per definire le specifiche
dell'analizzatore lessicale
• Tali specifiche vengono compilate dal compilatore lex che
genera un programma c che compilato a sua volta
produce il programma che fa da analizzatore lessicale
Fase di analisi Lessicale:tool(2)
• Il linguaggio Lex prevede:
Parte dichiarativa: con variabili costanti e espressioni
regolari
Regole di traduzione: che consistono in una espressione
regolare ed una azione (in lex scritta in C) associata che
l'analizzatore deve compiere nel momento che il pattern
regolare è riconosciuto
Procedure ausiliarie: procedure richieste dalle azioni
• Nota: l'analizzatore lessicale se pur trattato come
elemento separato dall'analizzatore sintattico coopera
fortemente tanto che a volte cede il controllo al parser
subito dopo aver compiuto l'azione relativa ad un token
riconosciuto
Lezione 4 - Compilatore parte III
Corso — Programmazione
Linguaggi di programmazione— Compilatori ed interpreti
Marco Anisetti
e-mail: [email protected]
web: http://homes.di.unimi.it/anisetti/
Università degli Studi di Milano — Dipartimento di informatica
Struttura di un compilatore
Fase di analisi sintattica (1)
• Si tratta del procedimento di costruzione della derivazione
di una frase rispetto ad una data grammatica. Usa i token
ed esegue il controllo sintattico attraverso una CFG
(Context Free Grammar), spesso nella forma di BNF. Il
risultato di questa fase è un albero sintattico
• Cerca errori sintattici e raggruppa i token in frasi
grammaticali
• Implementata come un parser che riceve i token dallo
scanner (analisi lessicale)
Fase di analisi sintattica (2)
• Le regole di una CFG, come la BNF vista nelle precedenti
lezioni, sono ricorsive.
• Ecco le regole BNF che stanno dietro il parse tree della
slide precedente
< assegnamento > ::= < identificativo > ``←''
< espressione >
< espressione > ::= < identificativo > | < numero > |
< expressione > < operazione > < expressione >
< operazione > ::= ``*''
< identificativo > ::= ``a'' | ``b''
< numero > ::= ``10''
Fase di analisi sintattica (3)
• In relazione a come viene creato il parse tree,ci sono
differenti tecniche, che possono essere divise in due
gruppi:
• Top-Down Parsing: analisi sintattica discendente, viene
anche definita ``predittiva'' dato che la costruzione del
parse tree inizia dalla radice e procede verso le foglie.
Recursive Predictive Parsing, Non-Recursive
Predictive Parsing (LL Parsing: Left to right, Left most).
Si parte dal simbolo iniziale e si procede applicando
produzioni successive per arrivare alla stringa target
Fase di analisi sintattica (4)
• Bottom-Up Parsing: analisi sintattica ascendente, viene
anche definita ``shift-reduce parsing'' dato che la
costruzione del parse tree inizia dalle foglie e procede
verso la radice. Operator-Precedence Parsing (LR
Parsing: Left to right, Right most). Si parte dalla stringa
target e si procede a ritroso per riduzioni verso il simbolo
iniziale. Durante il parsing è fattore chiave riuscire a
determinare quando effettuare una riduzione e quale
produzione applicare affinchè il parsing possa proseguire
Backtracking
• Tecnica che prevede di enumerare tutte le soluzioni in
ricerca di quella che soddisfa dei vincoli
• Navigazione in strutture ad albero tenendo traccia delle
visite effettuate
• Molto complesso e lento, a volte integra euristiche per
velocizzarlo
Recursive descent parsing
• Nel caso di recursive descent parsing può capitare di
dover tornare su una decisione presa (una derivazione
scelta) perchè non porta ad un risultato
• Questa azione è il backtracking, che nel caso dei parser
non è poi così frequente, o meglio si può evitare
• Bisogna star attenti a come è definita la grammatica se
esiste una left-recursion allora potrebbe innestarsi un
ciclo infinito
• Grammatica left-recursive: A → Aα|β esiste anche la
left-recursive indiretta
Predictive parsing
• La predizione serve per cercare di evitare il backtracking
• La predizione si basa su un lookahead del simbolo
seguente che disambigua le scelte possibili
• A volte continuano ad esistere delle ambiguità anche con
il lookahead
• Esempio: stmt → if expr then stmt else stmt | if expr
then stmt
• Se leggo il simbolo lookahead if per in non terminale stmt
comunque ho due alternative
• La soluzione è l'utilizzo della fattorizzazione sinistra
• In un linguaggio di programmazione le keyword aiutano la
predizione
LL parsing
• Derivazione LL Parsing (derivazione canonica sinistra) =
derivazione in cui ad ogni passo si espande il simbolo non
terminale più a sinistra della forma di frase corrente
[ESEMPIO]
Grammatica G con regole di produzione
F = S → AB|cSc,A → a,B → bB|b
Derivazione canonica sinistra della stringa cabbc:
S => cSc => cABc => caBc => cabBc => cabbc
• Al terzo passo di derivazione si espande il simbolo non
terminale A invece del non terminale B
Top-down Parsing: non ricorsivo(1)
• Come si potrebbe fare a sviluppare un algoritmo non
ricorsivo quando il problema sembra inerentemente
ricorsivo?
• L'algoritmo di parsing non ricorsivo mantiene esplicito lo
stack contenente i simboli della grammatica
• Si basa anche su una tabella di parsing
Top-down Parsing: non ricorsivo(2)
• Parsing predittivo: l'alternativa corretta deve essere
rilevabile guardando soltanto al primo simbolo che deriva
• Considero X il primo simbolo nello stack e a il primo
simbolo da parsare
• Questi due simboli determinano quello che il parser farà
1. Se X=a=# allora ho terminato
2. Se X=a!=# allora X viene tolta dallo stack e mi sposto sul
prossimo simbolo
3. Se X è un non terminale allora controllo la tabella nella
entry M[X ,a], la quale contiene le produzioni da usare o un
simbolo di errore
4. Se in M[X ,a] ho la produzione X → UVW allora rimpiazzo X
nello stack con WVU (ovvero U per ultimo)
• Come output indico le produzioni usate
Top-down Parsing: non ricorsivo(3)
• Esempio tratto da Compilers: principles, techniques and
tools
Bottom-Up Parsing
• Shift-reduce parsing: la costruzione del parse tree
inizia dalle foglie e procede verso la radice. Si parte dalla
stringa target e si procede a ritroso per riduzioni verso il
simbolo iniziale
• Ad ogni riduzione una particolare sottostringa che fa
match con la parte destra di una produzione è rimpiazzata
dal simbolo sinistro della produzione
• La sottostringa deve essere scelta bene tra le possibili
• L'algoritmo di parsing più famosi di questa categoria è l'LR
(left to right scanning of the input, rightmost reduction)
• Gestisce una più amplia classe di grammatiche rispetto ai
predictive parser
• Difficile da costruire a mano ma si possono usare i tool di
generazione
Bottom-Up Parsing: riduzione
Per definizione una riduzione è il passo inverso della
procedura di derivazione (attraverso la quale un non terminale
era sostituito dal corpo di una produzione, durante la
generazione di una stringa).
< exp > ::= < exp > ``*'' < G > | < G >
< G > ::= < G > ``+'' < F > | < F >
< F > ::= ``(''< exp > ``)'' | ``a''
Riduzione vs Derivazione
• Si consideri la grammatica BNF
S ::= 'a'< A >< B >'e'
A ::= < A >'b'c' |'b'
B ::= 'd'
• Data la stringa: abbcde
• La riduzione si ottiene:
abbcde (A→b A→ < A >bc B→d S→aABe)
abbcde, aAbcde, aAde, aABe, S
• La derivazione si ottiene:
S ⇒ aABe ⇒ aAde ⇒ aAbcde ⇒ abbcde
Fase di analisi semantica(1)
• Si occupa di controllare il significato delle istruzioni
presenti nel codice in ingresso. Il type checking ad
esempio dipendente solamente dalle regole semantiche
del linguaggio sorgente mentre è indipendente dal
linguaggio target.
• L'analisi semantica integra l'analisi sintattica o traduce
alberi sintattici in strutture utili alle elaborazioni
successive es AST
• Si appoggia sulla tabella dei simboli e i relativi attributi
• Generalmente vengono effettuati controlli statici: Divisioni
per 0, Controlli sul flusso, Controlli di unicità es. verificare
dichiarazioni multiple delle variabili in uno scope
[ESEMPIO]
a := b + 10
Il tipo dell'identificatore a deve corrispondere con il tipo
dell'espressione b + 10
Fase di analisi semantica(2)
• Le informazioni semantiche non possono essere
rappresentate con un semplice parse tree, ma serve
associarvi degli attributi
• Le produzioni vengono integrate, come già visto con
annotazioni di regole ( a volte frammenti di codice)
• Tali regole o frammenti sono eseguiti quando la
produzione viene utilizzata durante l'analisi sintattica
• Le regole servono per il calcolo degli attributi dei nodi
coinvolti
• Syntax-directed Translation: L'esecuzione dei
frammenti di codice, nell'ordine determinato dall'analisi
sintattica
Generatore codice intermedio(1)
• L' AST rappresenta la prima forma di Intermediate
Representation (IR) del programma sorgente.
• In generatore di codice traduce ciò che è semanticamente
corretto catturandone il significato a run-time
[ESEMPIO]
L'AST di un ciclo contiene due sotto-alberi, uno per
controllare l'espressione di controllo, e l'altro per il corpo
del ciclo. Nulla nell'AST dimostra che un ciclo ``itera'',
solo la sua traduzione lo palesa
• La Intermediate Representation esplicita la nozione di
verifica del valore dell'espressione di controllo e
dell'esecuzione del corpo del ciclo
Generatore codice intermedio(2)
• Three-address code(TAC o 3AC): Codice di basso livello
simile al linguaggio macchina che si può generare da un
AST
• Ogni istruzione nella forma 3AC può essere descritta dalla
quadrupla (operatore, operando1, operando2, risultato),
nella forma y = x op z
• Se ho più di una operazione fondamentale op in una
espressione allora la scompongo
• Si parla solo di una sola operazione di calcolo, un
confronto oppure un salto.
Generatore codice intermedio(3)
Generatore codice intermedio(4)
• Alcuni compilatori prevedono una IR di alto livello (source
oriented) e una IR a basso livello (target oriented)
• Chiara separazione fra dipendenze derivanti dal sorgente
e dal target
1.
2.
3.
4.
5.
a := b ∗ c + 2
id1 := id2 ∗ id3 + 2
MULT id2,id3,temp1
ADD temp1,#2,temp2
MOV temp2,id1
Compilatori: fase di sintesi
• Generatore codice: Mappa il codice IR prodotto dal
traduttore nel codice macchina target (di una specifica
architettura)
• Il codice target è un codice oggetto rilocabile contenente
codice macchina
• Si basa sulla definizione di template che mettano in
corrispondenza le istruzioni low-level IR con le istruzioni
target
• Ottimizzatore: cerca di migliorare il codice per limitare il
tempo di esecuzione e la memoria necessaria
• Esempio per l' IR precedente:
1.
2.
3.
4.
a := b ∗ c + 2
id1 := id2 ∗ id3 + 2
MULT id2,id3,temp1
ADD temp1,#2,id1