Linguaggi di Programmazione con Laboratorio - dalle lezioni di Marco Bellia - PHC

Pagina creata da Fabio Murgia
 
CONTINUA A LEGGERE
Appunti di

Linguaggi di Programmazione con
           Laboratorio
      dalle lezioni di Marco Bellia
             Francesco Baldino

         Secondo semestre a.a. 20/21

                    v0.2.0

                      1
Indice
1 25/02/2021                                                                    4
  1.1 Linguaggi di programmazione e funzioni calcolabili . . . . . . . .        4
  1.2 Macchine Astratte . . . . . . . . . . . . . . . . . . . . . . . . . .     6
  1.3 Funzione Universale . . . . . . . . . . . . . . . . . . . . . . . . .     7

2 01/03/2021                                                                    8
  2.1 Algoritmi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .   8
  2.2 Differenze nei linguaggi di programmazione . . . . . . . . . . . .        8

3 03/03/2021                                                                   11
  3.1 Macchine Astratte . . . . . . . . . . . . . . . . . . . . . . . . . . 11
  3.2 Basso livello e alto livello . . . . . . . . . . . . . . . . . . . . . . 13

                                        2
Disclaimer
   • Non consiglio di studiare da queste dispense prima della fine del cor-
     so. Trovo alcune definizioni confusionare ed è possibilissimo che in fu-
     turo, quando andiamo avanti col programma, andrò indietro a riscriverle
     corrette. Una volta concluso il corso non dovrebbero esserci problemi.
   • Questi appunti DOVREBBERO essere completi, ma non ne sono sicuro al
     100%. Se volete avvisarmi di errori o di parti mancanti, potete avvisarmi
     alla mia mail di dipartimento: baldino@mail.dm.unipi.it
   • Questi appunti seguono approssimativamente l’ordine delle lezioni del cor-
     so. Alcuni argomenti sono stati anticipati o posticipati di poco per rendere
     più organizzata la presentazione degli argomenti.

   • Continuo ripetutamente a dimenticarmi che in LATEXdevo essere in math
     mode per poter scrivere i simboli < e >. Se vi capita di vedere dei ¡ o ¿,
     sappiate che ¡ =< e ¿ =>. Per favore segnalatemeli che li correggo.

Prerequisiti
E’ consigliato l’aver seguito sia Fondamenti di Programmazione che Algoritmi
e Strutture Dati.

Contenuto
Non ho ancora seguito il corso, non ne ho idea :D

                                       3
1          25/02/2021
1.1         Linguaggi di programmazione e funzioni calcolabili
Cosa sono i linguaggi di programmazione? Cominciamo dando un po’ di defini-
zioni:
Definizione (Formalismo). Un formalismo è una struttura definita da una
sintassi e da una semantica. Esso consiste nell’insieme di tutte le formule aventi
una forma che soddisfa la sintassi. Il significato assegnato a ciascuna formula è
dato dalla semantica.

    La nozione di formalismo è più generale di quella di linguaggio di programma-
zione. Tra i formalismi, infatti, oltre ai linguaggi di programmazione, abbiamo
strutture quali l’Aritmetica di Peano, la logica dei predicati e la Teoria degli
Insiemi di Cantor.

Andiamo ora a specializzare la definizione di formalismo per ottenere quella
di linguaggio di programmazione
Definizione (Linguaggio di Programmazione). Un linguaggio di programma-
zione (LP) è un formalismo hPL , [| |]L i che definisce tutte e solo le funzioni
calcolabili, F. Formalmente, è un formalismo in cui:

     • la sintassi PL fissa la forma di ogni programma
     • la semantica [| |]L associa ad ogni programma una funzione calcolabile
       (cioè la funzione calcolabile da esso definita)
    Diamo ora le definizioni di funzione parziale e funzione calcolabile per com-
pletare la definizione di linguaggio di programmazione:

Definizione (Funzione parziale). Dato un insieme D numerabile, una funzione
parziale su D è una funzione g : D → D che può essere non definita su alcuni
punti del dominio D.
    Indichiamo con D → D l’insieme delle funzioni parziali. Se g su D non è
definita per c ∈ D, indichiamo g(c) =⊥. Una funzione parziale definita su tutto
il dominio è detta totale. A volte si usa dire che una funzione è ”strettamente”
parziale per dire che non è totale.
Definizione (Funzione calcolabile). Dato un insieme D numerabile, g parziale
è calcolabile se è continua rispetto alla topologia di Scott(1 ).

    Indichiamo con F = [D → D] l’insieme delle funzioni calcolabili. Nonostan-
te D → D sia più che numerabile, F è numerabile, quindi esiste una bigezione
F∼ = D.

    1 ∀c   ∈ D, ∀ t {ci } ⊂ D, se c ≡ t{ci } allora g(c) = g(t{ci }) = t{g(ci )}

                                                   4
Le funzioni calcolabili sono finitamente e completamente definite mediante
specifici formalismi, che includono: Combinatory Logic, λ-Calculus, Turing Ma-
chines...

    I linguaggi di programmazione sono quindi formalismi con i quali possiamo
scrivere formule, in questo caso chiamate ”programmi”, con cui possiamo:
   • definire tutte e solo le funzioni calcolabili,
   • implementare algoritmi e renderli processi automatici (ovvero eseguibili
     su macchine isolate o interconnesse),

   • esprimere solo (ma non necessariamente tutte) le computer applications,
     ovvero processi automatici.
Vediamo ora alcuni esempi di funzioni calcolabili e funzioni strettamente par-
ziali. Consideriamo il seguente programma in C:
void main() {
    int n, ret=1;
    scanf("%d", &n);
    if(n0) {
        ret *= n;
        n--;
    }
    printf("%d", ret);
}
esso definisce una funzione, a cui viene associata una formula riportata in seguito
in notazione equazionale-algebrica:
                                        (
                                          n! se n ≥ 0
                           fact0 (n) =
                                          0 se n < 0

Notiamo che, nonostante ci sia una corrispondenza diretta tra il programma in
C e la formula equazionale-algebrica, esse sono concettualmente due cose diver-
se. La prima diventa la seconda solo nel momento in cui interpretiamo il suo
significato tramite la semantica del linguaggio C.
La funzione definita è totale, poiché per ogni elemento del dominio, Z, la fun-
zione è definita.
Se consideriamo invece il seguente programma in C:

void main() {
    int n, ret=1;
    scanf("%d", &n);
    while(n != 0) {

                                         5
ret *= n;
         n--;
      }
      printf("%d", ret);
}
osserviamo che la funzione associata è diversa da fact0 . Il programma infatti
è equivalente a fact0 per i positivi, ma è non-terminante e divergente (ovvero
il valore da ritornare non converge a nessun valore finito) sui negativi. Dicia-
mo quindi che la funzione è indefinita o che vale ⊥ (indefinito) sui negativi.
La formula associata, sempre scritta in notazione equazionale-algebrica, è la
seguente:                               (
                                         n! se n ≥ 0
                           fact⊥ (n) =
                                         ⊥ se n < 0
Questo è un caso di funzione ”strettamente” parziale.
Vediamo un ultimo caso di funzione strettamente parziale, la cui terminabilità
non può essere nota a priori. Consideriamo il seguente programma in C che
prende in input una stream illimitata di interi:
void main() {
    int last, curr;
    scanf("%d", &last);
    scanf("%d", &curr);
    while(last != curr) {
        last = curr;
        scanf("%d", &curr);
    }
    printf("%d", curr);
}
Questo programma termina se e solo se la stream contiene due numeri contigui
uguali, condizione che potrebbe non essere nota a priori. Osserviamo che, in
ambito pratico, la non terminazione del programma non costituisce necessaria-
mente un ”errore”: il programma potrebbe essere stato scritto per analizzare
una stream di cui si suppone la non uguaglianza di numeri contigui, e in questo
caso la non terminazione è ”desiderabile” mentre la terminazione del programma
rappresenta la segnalazione di un errore nella stream.

1.2    Macchine Astratte
Abbiamo visto, nell’esempio del programma in C per fact0 , che nonostante
programma e formula siano strettamente associate, sono sostanzialmente cose
diverse. La prima assume senso e ”diventa” la seconda solo nel momento in cui:
    • interpretiamo il programma tramite la semantica,
    • disponiamo di un esecutore in grado di eseguire il programma in accordo
      alla semantica del linguaggio.

                                       6
Introduciamo quindi il concetto di macchina astratta come esecutore di pro-
grammi:
Definizione. Una macchina astratta è un modello di calcolo (hardware e/o
software) costituito da una coppia hL, EL i, dove L è il linguaggio di program-
mazione associato e EL è un esecutore di programmi di L che calcola in accordo
alla semantica di L.
    Ogni linguaggio di programmazione ha una propria macchina astratta il cui
esecutore fornisce una realizzazione (astratta o concreta) del modello di calcolo
utilizzato per calcolare i programmi del linguaggio. Osserviamo però che un
linguaggio di programmazione può avere più di una macchina astratta

1.3    Funzione Universale
Dato F = [D → D] l’insieme delle funzioni calcolabili e dato L = hPL , [| |]L i un
linguaggio di programmazione, chiamiamo funzione di meaning la funzione

                                   [| |]L : PL → F

Teorema 1.1 (Funzione Universale). Vale che:
  1. la funzione di meaning è surgettiva, ovvero ogni funzione calcolabile è
     rappresentabile come programma del linguaggio

  2. esiste una funzione di representation ¯ : PL → D iniettiva, ovvero ogni
     programma può essere rappresentato in modo univoco come elemento del
     dominio. Dato un programma p utiliziamo la notazione p̄ =¯(p).
  3. esiste una funzione universale UL ∈ F tale che:

        • ∀p ∈ PL , UL (p̄) = g ∈ F (tramite la corrispondenza tra D ∼
                                                                     = F)
        • ∀g ∈ F, ∃p ∈ PL tale che:
             – g = [|p|]L
             – UL (p̄)(x) = g(x)    ∀x ∈ D
   Ovvero ogni linguaggio di programmazione L ammette una propria funzione
universale UL calcolabile che applicata ad ogni programma (opportunamente
rappresentato in D) ”esegue” la funzione calcolabile definita dal programma.

                                          7
2     01/03/2021
2.1    Algoritmi
I linguaggi di programmazione trovano maggior utilizzo pratico nell’implementa-
zione di algoritmi, trasformandoli prima in programmi e poi, tramite la funzione
universale, in funzioni calcolabili (o eseguibili su macchine). Andiamo quindi
ad approfondire questo argomento:
Definizione (Algoritmo). Gli algoritmi sono processi effettivi, ovvero una se-
quenza di istruzioni con le seguenti caratteristiche:

    • Finitezza: sono descritti in modo finito
    • Definitezza: sono descritti mediante passi rigorosamente e non ambigua-
      mente definiti
    • Effettività: i passi usano operazioni elementari, riproducibili con un agente
      di calcolo
    • Input/Output: si applicano a dati di dimensione ”finita” ma non necessa-
      riamente ”limitata” (si veda ad esempio un algoritmo che prende in input
      una stream di dati)

    • Usano risorse (energie, tempo e spazio) finite
    Oltre a definire, tramite un linguaggio, funzioni calcolabili, gli algoritmi pos-
sono essere utilizzati per studiare proprietà delle applicazioni o delle funzioni
che li calcolano. In particolare, possiamo studiare proprietà quali il costo dei
procedimenti dell’applicazione o la trattabilità delle funzioni in F.
Nonostante il forte legame pratico con i linguaggi di programmazione, gli algo-
ritmi esistono come strutture a sé stanti senza necessità di far riferimento ad
alcun linguaggio specifico. Dato un algoritmo e un linguaggio, non è detto che
l’algoritmo abbia una rappresentazione tra i programmi del linguaggio (a causa
delle diverse espressività dei linguaggi), e se ce l’ha non è detto che sia unica.
Esistono funzioni calcolabili di cui non si conoscono algoritmi ed esistono algo-
ritmi di cui non si hanno programmi che li rendono processi automatici.

2.2    Differenze nei linguaggi di programmazione
Nonostante i linguaggi di programmazione siano equipotenti (ovvero tutti i lin-
guaggi di programmazione definiscono tutto e solo F), essi non hanno neces-
sariamente la stessa espressività, ovvero differiscono nel tipo di strutture o di
processi che utilizzano. In particolare, è possibile che un certo algoritmo possa
avere un’implementazione banale e pratica in un linguaggio e che abbia solo
implementazioni impraticabili e non trattabili in un altro (poiché la necessità
di emulare i costrutti mancanti in questo linguaggio potrebbe essere di costo

                                         8
eccessivo (2 )).
La diversità nell’espressività giustifica l’esistenza di più di un linguaggio di pro-
grammazione, per poter scegliere di utilizzare linguaggi strutturati apposita-
mente all’implementazione di determinati tipi algoritmi (e quindi al risolvimen-
to di determinati tipi di problemi). La differenza ”strutturale” dei linguaggi di
programmazione può presentarsi in differenze nelle risorse richieste per imple-
mentarli (come vedremo poi quando parleremo di implementazioni di macchine
astratte), differenze di prestazioni in determinate funzioni, o differenza di ma-
nutenzione (facilità di correzione di errori e di modifiche).

   Grossolanamente i linguaggi di programmazione possono essere divisi in due
categorie:
    • Prescriptive (imperative languages): linguaggi che pongono l’enfasi su qual
      è la successione di calcoli da eseguire
    • Descriptive (declarative languages): linguaggi che pongono l’enfasi su
      quale deve essere il risultato dei conti svolti
Riporto brevemente un esempio (non fatto in classe) di una stessa funzione
calcolabile espressa in modo imperativo e in modo dichiarativo (in entrambi
casi con un programma scritto in Python, che quindi mostra che i linguaggi non
sono necessariamente completamente imperativi o completamente dichiarativi):
# Imperative
small_nums = []
for i in range(20):
    if i < 5:
        small_nums.append(i)

# Declarative
small_nums = [x for x in range(20) if x < 5]
Entrambi i programmi(3 ) calcolano la lista ordinata di numeri tra 0 e 20 che
sono anche minori di 5. Il modo in cui lo esprimono, però, è sostanzialmente
diverso.

    Un’ulteriore classificazione dei linguaggi di programmazione può essere fatta
sui paradigmi da essi utilizzati. Vediamo brevemente una lista di paradigmi
senza soffermarci sui loro significati, che comprenderemo in seguito:
    • Procedural: Fortran, Pascal, C
   2 Facendo un esempio pratico, i linguaggi di programmazione quantistici (che si basano sui

qubit invece dei bit) rendono gli algoritmi di fattorizzazione degli algoritmi veloci e immediati.
Gli stessi algoritmi in linguaggi di programmazione ”classici” come il C sono di complessità
esponenziale e quindi non trattabili
   3 Grazie a Mark Rushakoff per il codice, trovato su StackOverflow a questo link:

https://stackoverflow.com/questions/1784664/what-is-the-difference-between-declarative-
and-imperative-paradigm-in-programmin

                                                9
• Functional: Lisp, Scheme, OCaml
• Algebraic: Lucid, OBJ, OPAL
• Logic / Constraint-based: Prolog, LogLisp, DataLog
• Object Oriented: C++, OCaml, Java, C#

• Scripting: Perl, Python, PHP, Javascript
• Concurrent: Lucid, C-Linda, Java
• Dataflow: Lucid, C-Linda, PrologLinda

• Multi-paradigms (i più recenti): F#, Ruby

                                 10
3     03/03/2021
3.1    Macchine Astratte
Abbiamo visto che una struttura fondamentale per l’utilizzo pratico dei lin-
guaggi di programmazione è la macchina astratta, che esegue i programmi del
linguaggio. Un linguaggio che non fa riferimento a nessuna macchina astratta
viene genericamente chiamato modello di calcolo.

Vediamo ora un esempio di schema di macchina astratta:

Questo schema rappresenta l’astrazione di quella che potrebbe essere una mac-
china concreta, quali un PC o una macchina di calcolo. Il blocco fondamentale
che distingue una macchina che fa riferimento ad un linguaggio di program-
mazione da una macchina generica è il blocco ”program”, ovvero quello che
conserva le istruzioni dettate dal programma. Ad esempio, lo schema di una
calcolatrice non presenterebbe questo blocco poiché le calcolatrici (almeno quelle
semplici) non hanno un vero programma da eseguire, ma si limitano ad eseguire
le operazioni dettate dall’user man mano che vengono inserite. Le macchine
astratte relative a linguaggi di programmazione sono quindi solitamente molto
più complesse.
Il blocco principale della macchina astratta, il cui scopo è eseguire i programmi
del linguaggio, è l’interpreter: esso si occupa di interpretare le istruzioni del
programma per poi coordinare e inviare segnali alle varie sottocomponenti per
accedere ai registri di memoria, eseguire le operazioni elementari e salvare i ri-
sultati.
Solitamente, l’esecuzione di un programma avviene per cicli, dettati dall’inter-
preter, di esecuzione di istruzioni semplici. Vediamo ora come sono strutturati,
di solito, i cicli di esecuzioni dell’interpreter

                                        11
Stiamo dando per scontato che il programma abbia una struttura a sequenza
di istruzioni, cosa vera per la maggior parte dei linguaggi di programmazione
moderni. Il ciclo di esecuzione è diviso in diversi passi, che sono:

   • Fetch next instruction: l’interpreter legge da memoria la prossima istru-
     zione del programma da eseguire (che potrebbe essere, prendendo un
     linguaggio semplice fittizio, ”ADD R5 R0”)
   • Decode: l’interprete decodifica l’istruzione, ovvero ne riconosce il signi-
     ficato (prendendo l’istruzione precedente, vorrebbe dire riconoscere che
     dobbiamo sommare due numeri (quindi mandare uno specifico segnale al
     blocco ”operations”), e questi due numeri stanno nei registri di memoria
     5 e 0 (quindi dobbiamo mandare e ricevere specifici segnari al blocco di
     memoria).
   • Fetch operands: l’interprete comunica con il blocco di memoria per andare
     a copiare gli operandi dell’operazione che dobbiamo svolgere dalla memoria
     ad un luogo ad accesso ad alta velocità, da dove poi verranno forniti al
     blocco operazioni).
   • Choose: l’interprete comunica al blocco operazioni quale operazione svol-
     gere, fornendo anche gli operandi. Osserviamo che le operazioni da svol-
     gere non sono necessariamente operazioni aritmetiche: vediamo anche l’o-
     perazione ”execute halt”, che identifica l’interruzione dell’esecuzione del
     programma.
   • Store the result: salva il risultato dell’operazione appena svolta (a meno
     che l’esecuzione non sia terminata)

Questo schema è molto simile al ciclo di esecuzioni delle CPU moderne. Le
istruzioni possono essere di diversi tipi. Abbiamo visto un esempio fittizio di
ADD R5 R0. Questo era un esempio di 2AC (2 address code), ovvero di una
istruzione basata su 2 indirizzi di memoria (un’operazione binaria). 2AC e 3AC

                                      12
sono note classi di di Linguaggi Macchina di cui parleremo.

   Una macchina astratta ML per un linguaggio di programmazione L è quindi
un insieme di strutture dati e di processi che permettono di memorizzare ed
eseguire i programmi del linguaggio L. Abbiamo sostanzialmente tre modi per
implementare una macchina astratta:
   • in hardware, ovvero in modo esclusivamente fisico
   • in software, ovvero in modo esclusivamente emulativo
   • in firmware, una via di mezzo tra le due

Nel caso di una macchina hardware dobbiamo aspettarci che tutte le componen-
ti, quali sistema di allocazione di memoria, CPU e BUS (la struttura di interfac-
cia tra le componenti, che trasportano segnali velocemente), siano realizzabili.
Questa soluzione è quindi particolarmente adatta per linguaggi semplici (vedre-
mo meglio in che senso dopo), che non fanno utilizzo di strutture complicate.
Dei linguaggi complessi potrebbero non essere facilmente realizzabili esclusiva-
mente in hardware, poiché potrebbero prevedere, ad esempio, dei file system
troppo complessi. In questi casi possiamo implementare delle macchine parzial-
mente o completamente software, ovvero delle macchine riescono a descrivere la
funzione universale del linguaggio emulando tramite software le strutture che era
impossibile implementare via hardware. Normalmente una macchina software
è basata su un linguaggio macchina più semplice che è possibile implementare
tramite macchina hardware.
Nel caso di una implementazione a metà tra software e hardware si parla di
firmware. Le macchine firmware si basano su ”microprogrammazione” dei chip,
ovvero utilizzano chip ROM (read only memory) su cui vengono caricati pro-
grammi non troppo complessi atti ad emulare le strutture che non era possibile
implementare completamente in hardware.
Una macchina con alcune o solo componenti (e comunicazioni tra esse) hard-
ware viene anche chiamata macchina concreta, e il suo linguaggio viene detto
linguaggio macchina.

3.2    Basso livello e alto livello
Vediamo ora una distinzione tra linguaggi di programmazione in relazione alla
realizzabilità di una loro macchina astratta: linguaggi a basso livello e linguaggi
ad alto livello.
I linguaggio a basso livello sono linguaggi la cui struttura è solitamente condi-
zionata dalla possibilità di realizzazione della sua macchina astratta, in modo da
renderne semplice la costruzione. Questi linguaggi sono solitamente più veloci
(vista la poca o nulla emulazione necessaria) e possono gestire in modo molto
più diretto le componenti della macchina. In caso di un linguaggio a basso livel-
lo, la somiglianza tra una sua macchina astratta e la struttura a 3 componenti

                                        13
vista in precedenza è elevata.
I linguaggi ad alto livello sono invece linguaggi la cui struttura e condizionata
maggiormente dalle metodologie di programmazione e dal modello di calcolo che
vogliono utilizzare. Non ci si pone quindi il problema della realizzabilità della
macchina astratta associata, poiché essa verrà emulata con un linguaggio più
semplice.
La soluzione ideale per i linguaggi ad alto livello (più pratici per lavorare poiché
più vicini al modello di calcolo che vogliamo utilizzare) è di emulare una mac-
china astratta su una macchina concreta basata su un linguaggio a basso livello.
Chiamando L il linguaggio ad alto livello e L0 il linguaggio più semplice (e ML
e ML0 le relative macchine). Abbiamo allora due opzioni per emulare ML in
ML0 :
   • Interpretativa Pura: costruire un interprete ILL0 che, statement per state-
     ment durante l’esecuzione, interpreta programmi di L in L0
   • Compilativa Pura: costruire un compilatore che trasforma programmi di
     L in programmi equivalenti scritti in L0 , da eseguire poi
Vediamo meglio come sono definiti un interprete e un compilatore, per capirne
le differenze. Come prima, siano L il linguaggio ad alto livello e L0 il linguaggio
più semplice, PL e PL0 i rispettivi insiemi di programmi e UL e UL0 le rispettive
funzioni universali. Supponiamo di disporre di ML0 e di voler emulare ML
Definizione (Interprete). Un interprete di L in L0 è un programma ILL0 ∈ PL0
tale che ∀p ∈ PL , ∀x ∈ D, vale

                            UL (p̄)(x) = UL0 (ILL0 )(hp, xi)
dove h , i : PL × D → D è iniettiva
     In questo caso è l’interprete stesso ad essere il programma che viene esegui-
to, e questo prende in input il programma di L che volevamo eseguire, assieme
all’input x che lui avrebbe preso. Lo scopo della funzione h , i è evitare di dover
cambiare la definizione di funzione parziale: queste prendono in input un solo
elemento di D, mentre UL0 (ILL0 ) dovrebbe prendere in input sia la rappresen-
tazione in memoria di p, sia il suo input x. Per farlo, creiamo la funzione h , i
che, dato un programma e un input, restituisce una configurazione di stati di
memoria (quindi un elemento di D) che contiene sia la rappresentazione p̄ del
programma originale, sia l’input passato.
Definizione (Compilatore). Un compilatore di L in L0 è un programma CL,L0
di un qualche linguaggio LA tale che ∀p ∈ PL , detta ULA (CL,L0 )(hpi) = q(4 )
∈ PL0 , vale
                      ∀x ∈ D, UL0 (q̄)(x) = UL (p̄)(x)
dove h i : PL → D è iniettiva
   4 Sono abbastanza sicuro che qui ci sia un abuso di notazione. La funzione universale

restituisce un elemento di D, quindi al più q̄, non il programma q stesso. Comunque non
cambia niente ai fini della definizione perché poi usiamo solo q̄.

                                          14
In questo caso, il compilatore è un programma che ”traduce” un programma
di L in uno di L0 . Al momento dell’esecuzione, quindi, il compilatore non serve
più a niente e la macchina esegue il programma di L0 tradotto da quello di L.
In soldoni, il compilatore serve ”una volta sola” mentre l’interprete serve ”ogni
volta che si esegue”.
Chiaramente l’approccio tramite compilatore prevede di disporre già di una
macchina astratta per il linguaggio LA utilizzato per compilare, chiamata host
machine.

   Nell’atto pratico, i compilatori utilizzati nei computer moderni non fanno
appoggio su una host machine: i compilatori sono direttamente dei programmi
                                                                        L0
di L0 eseguibili su ML0 . In questo caso indichiamo il compilatore con CL,L0
                                                                             .

    In generale, però, se già scrivere un interprete ILL0 è un’opera ardua, scrivere
                  L0
un compilatore CL,L   0
                         in linguaggio macchina è davvero molto complicato. Per
risolvere questo problema, supponendo di disporre già di una macchina in grado
di eseguire ML (5 ), possiamo dare in input al compilatore il compilatore stes-
so, in modo da ricevere in output un programma in L0 che si comporta come
compilatore da L a L0
                                  L        L          L0
                                 CL,L 0
                                        (hCL,L0
                                                i) ⇒ CL,L0

   5 Che bisogno abbiamo di un compilatore da L a L se già disponiamo di una macchina
                                                           0
ML ? Non l’ho capito benissimo. A lezione l’abbiamo giustificato con ”tanto abbiamo già delle
macchine che eseguono il C”. Credo che l’idea sia: se hai un vecchio hardware che riesce ad
emulare L e un nuovo hardware (magari più performante o migliore in qualche altro senso) sul
quale vorresti poter emulare L, puoi utilizzare ML , ovvero l’emulazione del vecchio hardware
                     L0
di L, per produrre CL,L 0
                          , dove L0 è il linguaggio del nuovo hardware (potenzialmente diverso
da quello vecchio).
Chiaramente questa soluzione non può funzionare per il primo compilatore in assoluto per un
certo linguaggio: quello va necessariamente scritto in linguaggio macchina.

                                              15
Puoi anche leggere