Linguaggi di Programmazione con Laboratorio - dalle lezioni di Marco Bellia - PHC
←
→
Trascrizione del contenuto della pagina
Se il tuo browser non visualizza correttamente la pagina, ti preghiamo di leggere il contenuto della pagina quaggiù
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