Domanda

Di recente ho letto l'intero Libro dei draghi (solo per divertimento, non ho davvero intenzione di implementare un vero compilatore), e mi è rimasta questa grande domanda penzoloni in testa.

Qual è la differenza tra l'implementazione di un compilatore e un interprete?

Per me un compilatore è composto da:

  • Lexer
  • Parser (che costruisce l'albero della sintassi)
  • Genera codice intermedio (come 3 codice indirizzo)
  • Fai tutte queste cose folli per ottimizzare se vuoi :-)
  • Genera " assembly " o " codice nativo " dal codice di indirizzo 3.

Ora, ovviamente, l'interprete ha anche lo stesso lexer e lo stesso parser del compilatore.
Ma cosa fa dopo?

  • Fa " leggi " l'albero di sintassi ed eseguirlo direttamente? (un po 'come avere un puntatore a istruzioni che punta al nodo corrente nella struttura, e l'esecuzione è un grande attraversamento dell'albero più la gestione della memoria per lo stack di chiamate) (e in tal caso, come fa? Spero che l'esecuzione è migliore di un'enorme istruzione switch che controlla che tipo di nodo è)

  • Genera 3 codici indirizzo e lo interpreta? (in tal caso, come lo fa? Ancora una volta, sto cercando qualcosa di più elegante di un'istruzione switch lunga un miglio)

  • Genera vero codice nativo, lo carica in memoria e lo fa funzionare? (a quel punto suppongo che non sia più un interprete, ma più simile a un compilatore JIT)

Inoltre, a quel punto il concetto di "macchina virtuale" intromettersi? Per cosa usi una macchina virtuale in una lingua? (per essere chiari sul mio livello di ignoranza, per me una macchina virtuale è VMWare, non ho idea di come il concetto di VM si applichi ai linguaggi di programmazione / all'esecuzione dei programmi).

Come puoi vedere, la mia domanda è piuttosto ampia. Per lo più cerco non solo quale metodo viene utilizzato, ma soprattutto per capire prima i grandi concetti e poi capire come funziona in dettaglio. Voglio i dettagli brutti e grezzi. Ovviamente, questa è più una ricerca di riferimenti a cose da leggere piuttosto che aspettarti che tu risponda a tutti questi dettagli qui.

Grazie!
Daniel


EDIT: grazie per le risposte finora. Mi sono reso conto che il mio titolo era fuorviante però. Capisco il "funzionale" differenza tra un compilatore e un interprete.
Quello che sto cercando è la differenza su come implementare un interprete rispetto a un compilatore.
Ora capisco come viene implementato un compilatore, la domanda è come un interprete differisce da quello.

Ad esempio: VB6 è chiaramente sia un compilatore che un interprete. Capisco ora la parte del compilatore. Tuttavia, non riesco a capire come, durante l'esecuzione all'interno dell'IDE, possa farmi interrompere il programma in qualsiasi punto arbitrario, modificare il codice e riprendere l'esecuzione con il nuovo codice. Questo è solo un piccolo esempio, non è la risposta che sto cercando. Quello che sto cercando di capire, come spiego di seguito, è cosa succede dopo che ho un albero di analisi. Un compilatore genererà nuovo codice da esso nel "target" linguaggio. Cosa fa un interprete?

Grazie per il tuo aiuto!

È stato utile?

Soluzione

risposta breve:

  • un compilatore converte il codice sorgente in un formato eseguibile per successive esecuzioni
  • un interprete valuta il codice sorgente per l'esecuzione immediata

c'è molta libertà nel modo in cui entrambi sono implementati. È possibile per un interprete generare codice macchina nativo e quindi eseguirlo, mentre un compilatore per una macchina virtuale può generare codice p anziché codice macchina. Le lingue interpretate in thread come Forth cercano le parole chiave in un dizionario ed eseguono immediatamente la loro funzione di codice nativo associata.

i compilatori possono generalmente ottimizzare meglio perché hanno più tempo per studiare il codice e produrre un file per l'esecuzione successiva; gli interpreti hanno meno tempo per ottimizzare perché tendono ad eseguire il codice "come è" a prima vista

è anche possibile un interprete ottimizzato in background, che apprende modi migliori per eseguire il codice

sommario: la differenza sta davvero nel 'preparare il codice per un'esecuzione successiva' o 'eseguire il codice adesso'

Altri suggerimenti

Un compilatore è un programma che traduce un programma in un linguaggio di programmazione in un programma in un altro linguaggio di programmazione. Questo è tutto - chiaro e semplice.

Un interprete traduce un linguaggio di programmazione nel suo significato semantico.

Un chip x86 è un interprete per il linguaggio macchina x86.

Javac è un compilatore per java sulla macchina virtuale java. java, l'applicazione eseguibile, è un interprete per jvm.

Alcuni interpreti condividono alcuni elementi della compilazione in quanto possono tradurre una lingua in un'altra lingua interna che è più facile da interpretare.

Gli interpreti di solito, ma non sempre, presentano un ciclo read-eval-print.

Un programma è una descrizione del lavoro che vuoi svolgere.

Un compilatore converte una descrizione di alto livello in una descrizione più semplice.

Un interprete legge una descrizione di cosa fare e fa il lavoro .

  • Alcuni interpreti (ad es. shell Unix) leggono la descrizione un piccolo pezzo alla volta e agiscono su ogni pezzo come lo vedono; alcuni (ad esempio Perl, Python) leggono l'intera descrizione, la convertono internamente in una forma più semplice e quindi agiscono su quella.
  • Alcuni interpreti (ad esempio JVM di Java o un chip Pentium 4) comprendono solo un linguaggio di descrizione molto semplice che è troppo noioso per lavorare direttamente con gli umani, quindi gli umani usano i compilatori per convertire le loro descrizioni di alto livello in questo linguaggio.

I compilatori non non fanno mai il lavoro. Gli interpreti sempre svolgono il lavoro.

Entrambi hanno molto in comune (es. parser lessicale) e non c'è disaccordo sulla differenza. Guardo in questo modo:

La definizione classica sarebbe che un compilatore analizza e traduce un flusso di simboli in un flusso di byte che può essere eseguito dalla CPU mentre un interprete fa la stessa cosa ma li traduce in un modulo che deve essere eseguito su un software (ad es. JVM, CLR).

Tuttavia le persone chiamano "javac" un compilatore, quindi la definizione informale di un compilatore è qualcosa che deve essere fatto per il codice sorgente come un passaggio separato mentre gli interpreti non hanno un passaggio "build" (ad esempio PHP, Perl).

Non è così nitido come una volta. Un tempo era costruire un albero di analisi, legarlo ed eseguirlo (spesso vincolante all'ultimo secondo).

BASIC è stato fatto principalmente in questo modo.

Potresti affermare che le cose che eseguono bytecode (java / .net) senza fare una JIT sono interpreti - ma non nel senso tradizionale poiché devi ancora 'compilare' in bytecode.

La differenza della vecchia scuola era: se genera codice CPU è un compilatore. Se lo esegui direttamente nel tuo ambiente di modifica e puoi interagire con esso durante la modifica, è un interprete.

Questo è stato molto meno formale dell'attuale libro del Drago - ma spero che sia informativo.

Se la mia esperienza indica qualcosa;

  1. Gli interpreti non tentano di ridurre / elaborare ulteriormente l'AST, ogni volta che viene fatto riferimento a un blocco di codice, il nodo AST rilevante viene attraversato ed eseguito. I compilatori attraversano un blocco al massimo diverse volte per generare codice eseguibile in un determinato posto ed essere fatto con esso.
  2. La tabella dei simboli degli interpreti conserva i valori e i riferimenti durante l'esecuzione, la tabella dei simboli dei compilatori mantiene le posizioni delle variabili. Non esiste una tabella dei simboli di tale cosa durante l'esecuzione.

In shot la differenza può essere semplice come

case '+':
    symtbl[var3] = symtbl[var1] + symtbl[var2];
    break;

tra

case '+':
    printf("%s = %s + %s;",symtbl[var3],symtbl[var1],symtbl[var2]);
    break;

(Non importa se scegli come target un'altra lingua o istruzioni (virtuali) per la macchina.)

Per quanto riguarda questa parte della tua domanda, che le altre risposte non hanno veramente affrontato:

  

Inoltre, a quel punto il concetto   di "macchina virtuale" intromettersi? Cosa fare   si utilizza una macchina virtuale per in a   lingua?

Le macchine virtuali come JVM o CLR sono un livello di astrazione che consente di riutilizzare l'ottimizzazione del compilatore JIT, la garbage collection e altri dettagli di implementazione per linguaggi completamente diversi che vengono compilati per essere eseguiti sulla VM.

Ti aiutano anche a rendere le specifiche del linguaggio più indipendenti dall'hardware reale. Ad esempio, mentre il codice C è teoricamente portabile, devi sempre preoccuparti di cose come l'endianità, la dimensione del tipo e l'allineamento delle variabili se vuoi effettivamente produrre codice portatile. Considerando che con Java, la JVM è chiaramente specificata in questo senso, quindi il progettista del linguaggio e i suoi utenti non devono preoccuparsi di loro; è compito dell'implementatore JVM implementare il comportamento specificato sull'hardware effettivo.

Una volta disponibile un albero di analisi, ci sono diverse strategie:

1) interpreta direttamente l'AST (Ruby, l'interprete originale di WebKit) 2) trasformazione del codice - > in codici byte o codice macchina

Per ottenere Modifica-e-Continua, il contatore del programma o il puntatore dell'istruzione deve essere ricalcolato e spostato. Ciò richiede la cooperazione dell'IDE, poiché il codice potrebbe essere stato inserito prima o dopo la piccola freccia gialla.

Un modo per farlo è incorporare la posizione del contatore del programma nell'albero di analisi. Ad esempio, potrebbe esserci un'istruzione speciale chiamata " break " ;. Il contatore del programma deve essere posizionato solo dopo la "pausa" istruzioni per continuare a correre.

Inoltre, devi decidere cosa vuoi fare riguardo al frame dello stack corrente (e alle variabili nello stack). Forse spuntando lo stack corrente e copiando le variabili o mantenendo lo stack, ma patch in un GOTO e INVIO al codice corrente.

Dato il tuo elenco di passaggi:

  • Lexer
  • Parser (che costruisce l'albero della sintassi)
  • Genera codice intermedio (come 3 codice indirizzo)
  • Fai tutte queste cose folli per ottimizzare se vuoi :-)
  • Genera " assembly " o " codice nativo " dal codice di indirizzo 3.

Un interprete molto semplice (come i primi BASIC o TCL) eseguirà solo i passaggi uno e due una riga alla volta. E poi getta via la maggior parte dei risultati mentre procedi alla riga successiva da eseguire. Gli altri 3 passaggi non verrebbero mai eseguiti affatto.

Se stai cercando un libro, Struttura e interpretazione dei programmi per computer (" il Il libro dei maghi "è un buon punto di partenza per i concetti di interprete. Hai sempre a che fare con il codice dello Schema, che può essere attraversato, valutato e passato come se fosse un AST.

Inoltre, Peter Norvig ha un breve esempio che spiega l'idea principale usando Python (con molti altri esempi in i commenti), ed ecco un altro piccolo esempio su Wikipedia.

Come hai detto, è un traversata di alberi, e almeno per chiamata per valore è semplice: ogni volta che vedi un operatore, valuta il pugno di operandi, quindi applica l'operatore. Il valore finale restituito è il risultato del programma (o la dichiarazione fornita a un REPL).

Nota che non devi sempre fare esplicitamente l'attraversamento dell'albero: potresti generare il tuo AST in modo tale da accettare un visitatore (penso che SableCC lo faccia), o per linguaggi molto piccoli, come le piccole grammatiche aritmetiche usato per dimostrare generatori di parser, puoi semplicemente valutare il risultato durante l'analisi.

Per supportare dichiarazioni e assegnazioni, è necessario mantenere un ambiente circostante. Proprio come valuteresti " più " aggiungendo gli operandi, valuteresti il ??nome di una funzione, variabile, ecc., cercandolo nell'ambiente. Supportare l'ambito significa trattare l'ambiente come uno stack e spingere e far scoppiare le cose al momento giusto. In generale, quanto è complicato il tuo interprete dipende da quali funzioni linguistiche intendi supportare. Ad esempio, gli interpreti rendono possibile la garbage collection e l'introspezione.

Per VM: plinth e j_random_hacker descrivono l'hardware del computer come una specie di interprete. È vero anche il contrario: gli interpreti sono macchine; le loro istruzioni sono di livello superiore a quelle di un vero ISA. Per gli interpreti in stile VM, i programmi assomigliano in realtà al codice macchina, albiet per una macchina molto semplice. Java bytecode utilizza solo pochi "registri", " uno dei quali contiene un contatore di programmi. Quindi un interprete VM è più simile a un emulatore hardware che agli interpreti negli esempi che ho collegato sopra.

Tuttavia, per motivi di velocità, Oracle JVM predefinito funziona traducendo le esecuzioni di istruzioni bytecode Java in istruzioni x86 (" compilazione just in time ").

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top