Quali proprietà di un linguaggio di programmazione rendono impossibile la compilazione?


72

Domanda:

"Alcune proprietà di un linguaggio di programmazione possono richiedere che l'unico modo per ottenere il codice scritto sia eseguito dall'interpretazione. In altre parole, la compilazione in un codice macchina nativo di una CPU tradizionale non è possibile. Quali sono queste proprietà?"

Compilatori: principi e pratica di Parag H. Dave e Himanshu B. Dave (2 maggio 2012)

Il libro non fornisce alcun indizio sulla risposta. Ho cercato di trovare la risposta sui concetti dei linguaggi di programmazione (SEBESTA), ma senza risultati. Anche le ricerche sul web erano di scarsa utilità. Hai qualche idea?


31
È noto che Perl non può nemmeno essere analizzato . Oltre a ciò, l'affermazione sembra essere banalmente sbagliata senza ulteriori ipotesi: se c'è un interprete, posso sempre raggruppare interprete e codice in un eseguibile, voilà.
Raffaello

4
@Raphael: Bella idea, ma ... 1) Stai assumendo che il codice sia disponibile prima di essere eseguito. Ciò non vale per l'uso interattivo. Certo, puoi usare la compilazione just-in-time per codice nativo su istruzioni bash o contenuti dello stack PostScript, ma è un'idea piuttosto folle. 2) La tua idea in realtà non compila il codice: il bundle non è una versione compilata del codice, ma comunque un interprete per il codice.
reinierpost,

7
Ai vecchi tempi avevo programmi di auto editing di gwbasic (gwbasic memorizza i programmi di base in una specie di bytecode). Al momento non riesco a pensare a un modo sano per compilarli in codice macchina nativo mantenendo la loro capacità di modificarsi.
PlasmaHH,

15
@PlasmaHH: il codice di modifica automatica risale al 1948. Il primo compilatore è stato scritto nel 1952. Il concetto di codice di modifica automatica è stato inventato nel codice macchina nativo.
Mooing Duck

10
@reinierpost Raphael sta prendendo una posizione teorica su questo tema. Ha il merito di mostrare i limiti concettuali della domanda. La compilazione è la traduzione dalla lingua S alla lingua T. La lingua T potrebbe essere un'estensione di S a cui è possibile aggiungere il codice di interpretazione in qualche altra lingua. Quindi raggruppare S e il suo interprete è un programma in linguaggio T. Sembra assurdo a un ingegnere, ma dimostra che non è facile formulare la domanda in modo significativo. Come si distingue un processo di compilazione accettabile da un processo inaccettabile (come quello di Raffaello) da un punto di vista ingegneristico?
babou,

Risposte:


61

La distinzione tra codice interpretato e compilato è probabilmente una finzione, come sottolineato dal commento di Raffaello :

the claim seems to be trivially wrong without further assumptions: if there is
an interpreter, I can always bundle interpreter and code in one executable ...

Il fatto è che il codice viene sempre interpretato, dal software, dall'hardware o da una combinazione di entrambi, e il processo di compilazione non può dire quale sarà.

Ciò che percepisci come compilazione è un processo di traduzione da una lingua (per l'origine) a un'altra lingua T (per l'obiettivo). E, l'interprete per la S è in genere diverso dal interprete per T .STST

Il programma compilato viene tradotto da una forma sintattica in un'altra forma sintattica P T , in modo tale che, data la semantica prevista delle lingue S e T , P S e P T abbiano lo stesso comportamento computazionale, fino ad alcune cose che di solito stanno cercando di cambiare, possibilmente per ottimizzare, come complessità o semplice efficienza (tempo, spazio, superficie, consumo di energia). Sto cercando di non parlare di equivalenza funzionale, poiché richiederebbe definizioni precise.PSPTSTPSPT

Alcuni compilatori sono stati effettivamente utilizzati semplicemente per ridurre le dimensioni del codice, non per "migliorare" l'esecuzione. Questo è stato il caso del linguaggio usato nel sistema Plato (sebbene non lo chiamassero compilare).

Si può prendere in considerazione il codice completamente compilato se, dopo il processo di compilazione, non è più necessario l'interprete per . Almeno, questo è l'unico modo in cui posso leggere la tua domanda, come una questione ingegneristica piuttosto che teorica (dal momento che, teoricamente, posso sempre ricostruire l'interprete).S

Una cosa che può sollevare problemi, a parte, è la meta-circolarità . Questo è quando un programma manipolerà le strutture sintattiche nel suo linguaggio , creando frammenti di programma che vengono poi interpretati come se fossero stati parte del programma originale. Dal momento che puoi produrre frammenti di programma arbitrari nel linguaggio S come risultato di calcoli arbitrari manipolando frammenti sintattici insignificanti, immagino che tu possa rendere quasi impossibile (da un punto di vista ingegneristico) compilare il programma nel linguaggio T , in modo che la guida generare frammenti di T . Quindi sarà necessario l'interprete per S , o almeno il compilatore da S aSSTTSS perla compilazione al volodi frammenti generati in S (vedi anchequesto documento).TS

Ma non sono sicuro di come questo possa essere formalizzato correttamente (e non ho tempo per ora). E impossibile è una parola grossa per un problema che non è formalizzato.

Altre osservazioni

Aggiunto dopo 36 ore. Potresti voler saltare questo sequel molto lungo.

I molti commenti a questa domanda mostrano due punti di vista del problema: una visione teorica che lo considera insignificante e una visione ingegneristica che purtroppo non è così facilmente formalizzata.

Esistono molti modi per esaminare l'interpretazione e la compilazione e proverò a disegnarne alcuni. Cercherò di essere il più informale possibile

Il diagramma della pietra tombale

Una delle prime formalizzazione (dall'inizio degli anni '60 alla fine del 1990) sono i diagrammi T o Tombstone . Questi diagrammi presentavano in elementi grafici componibili il linguaggio di implementazione dell'interprete o del compilatore, il linguaggio di origine interpretato o compilato e il linguaggio di destinazione nel caso dei compilatori. Versioni più elaborate possono aggiungere attributi. Queste rappresentazioni grafiche possono essere viste come assiomi, regole di inferenza, utilizzabili per derivare meccanicamente la generazione di processori da una prova della loro esistenza dagli assiomi, alla Curry-Howard (anche se non sono sicuro che sia stato fatto negli anni sessanta :).

Valutazione parziale

Un'altra visione interessante è il paradigma della valutazione parziale . Sto prendendo una semplice visione dei programmi come una sorta di implementazione di funzioni che calcola una risposta dati alcuni dati di input. Poi un interprete per il linguaggio S è un programma che prende un programma p S scritto in S e dati d per quel programma, e calcola il risultato in base alla semantica di S . Valutazione parziale è una tecnica per un programma specializzato di due argomenti un 1 e un 2 , quando un solo argomento, dire un 1ioSSpSSdSun'1un'2un'1, è conosciuto. L'intento è quello di avere una valutazione più veloce quando finalmente ottieni il secondo argomento . È particolarmente utile se un 2 cambia più spesso di un 1 poiché il costo della valutazione parziale con un 1 può essere ammortizzato su tutti i calcoli in cui cambia solo un 2 .un'2un'2un'1un'1un'2

Questa è una situazione frequente nella progettazione dell'algoritmo (spesso l'argomento del primo commento su SE-CS), quando una parte più statica dei dati è pre-elaborata, in modo che il costo della pre-elaborazione possa essere ammortizzato su tutte le applicazioni dell'algoritmo con più parti variabili dei dati di input.

Questa è anche la situazione stessa degli interpreti, poiché il primo argomento è il programma da eseguire, e di solito viene eseguito più volte con dati diversi (o ha sottoparti eseguite più volte con dati diversi). Quindi diventa un'idea naturale specializzare un interprete per una valutazione più rapida di un determinato programma valutandolo parzialmente su questo programma come primo argomento. Questo può essere visto come un modo per compilare il programma, e ci sono stati lavori di ricerca significativi sulla compilazione mediante valutazione parziale di un interprete sul suo primo argomento (programma).

Il teorema di Smn

Il punto interessante dell'approccio di valutazione parziale è che affonda le sue radici nella teoria (sebbene la teoria possa essere un bugiardo), in particolare nel teorema di Smn di Kleene . Sto provando qui a darne una presentazione intuitiva, sperando che non sconvolga i puri teorici.

Data una numerazione di Gödel delle funzioni ricorsive, è possibile visualizzare φ come hardware, in modo che dato il numero di Gödel p (leggi il codice oggetto ) di un programma φ p sia la funzione definita da p (cioè calcolata dal codice oggetto sul tuo hardware ).φφpφpp

Nella sua forma più semplice, il teorema è dichiarato in Wikipedia come segue (fino a un piccolo cambiamento nella notazione):

Data una numerazione di Gödel delle funzioni ricorsive, esiste una funzione ricorsiva primitiva σ di due argomenti con la seguente proprietà: per ogni numero di Gödel q di una funzione calcolabile parziale f con due argomenti, le espressioni φ σ ( q , x ) ( y ) e f ( x , y ) sono definiti per le stesse combinazioni di numeri naturali x ed y , ei loro valori sono uguali per qualsiasi combinazione. In altre parole, la seguente uguaglianza estensionale di funzioni vale per ogniφσqfφσ(q,X)(y)f(X,y)Xy : Xφσ(q,X)λy.φq(X,y).

Ora, prendendo come interprete I S , x come codice sorgente di un programma p S e y come dati d per quel programma, possiamo scrivere: qioSXpSydφσ(ioS,pS)λd.φioS(pS,d).

possono essere visti come l'esecuzione dell'interprete I S sull'hardware, cioè, come un black-box pronto a interpretare i programmi scritti in linguaggio S .φioSioSS

La funzione può essere vista come una funzione specializzata nell'interprete I S per il programma P S , come nella valutazione parziale. Pertanto il numero di Gödel σ ( I S , p S ) può essere visto trovi codice oggetto che è la versione compilata del programma p S .σioSPSσ(ioS,pS)pS

Quindi la funzione può essere visto come una funzione che prende come argomento il codice sorgente di un programma q S scritto nella lingua S , e restituisce la versione del codice oggetto per quel programma. Quindi C S è ciò che viene solitamente chiamato un compilatore.CS=λqS.σ((ioS,qS)qSSCS

Alcune conclusioni

Tuttavia, come ho detto: "la teoria può essere un bugiardo", o in realtà sembra esserlo. Il problema è che non sappiamo nulla della funzione . In realtà ci sono molte di queste funzioni, e la mia ipotesi è che la dimostrazione del teorema possa usare una definizione molto semplice per essa, che potrebbe non essere migliore, dal punto di vista ingegneristico, della soluzione proposta da Raffaello: semplicemente raggruppare il codice sorgente q S con l'interprete che S . Questo può sempre essere fatto, in modo che possiamo dire: la compilazione è sempre possibile.σqSioS

Formalizzare una nozione più restrittiva di cosa sia un compilatore richiederebbe un approccio teorico più sottile. Non so cosa sia stato fatto in quella direzione. Il vero lavoro svolto sulla valutazione parziale è più realistico dal punto di vista ingegneristico. E naturalmente ci sono altre tecniche per scrivere compilatori, tra cui l'estrazione di programmi dalla prova delle loro specifiche, sviluppata nel contesto della teoria dei tipi, basata sull'isomorfismo di Curry-Howard (ma sto uscendo dal mio dominio di competenza) .

Il mio scopo qui è stato quello di mostrare che l'osservazione di Raffaello non è "pazza", ma un sano promemoria che le cose non sono ovvie e nemmeno semplici. Dire che qualcosa è impossibile è un'affermazione forte che richiede definizioni precise e una prova, se non altro per avere una comprensione precisa di come e perché è impossibile . Ma costruire una corretta formalizzazione per esprimere tale prova può essere piuttosto difficile.

Ciò detto, anche se una specifica funzionalità non è compilabile, nel senso inteso dagli ingegneri, le tecniche di compilazione standard possono sempre essere applicate a parti dei programmi che non utilizzano tale funzionalità, come osservato dalla risposta di Gilles.

Seguendo le osservazioni chiave di Gilles che, a seconda della lingua, alcune cose possono essere fatte in fase di compilazione, mentre altre devono essere fatte in fase di esecuzione, richiedendo quindi un codice specifico, possiamo vedere che il concetto di compilazione è in realtà mal definito, e probabilmente non è definibile in alcun modo soddisfacente. La compilazione è solo un processo di ottimizzazione, come ho cercato di mostrare nella sezione di valutazione parziale , quando l'ho confrontato con la preelaborazione dei dati statici in alcuni algoritmi.

Come processo di ottimizzazione complesso, il concetto di compilazione appartiene in realtà a un continuum. A seconda delle caratteristiche della lingua o del programma, alcune informazioni potrebbero essere disponibili staticamente e consentire una migliore ottimizzazione. Altre cose devono essere rimandate al runtime. Quando le cose si mettono davvero male, tutto deve essere fatto in fase di esecuzione almeno per alcune parti del programma e raggruppare il codice sorgente con l'interprete è tutto ciò che puoi fare. Quindi questo raggruppamento è solo la parte bassa di questo continuum di compilazione. Gran parte della ricerca sui compilatori riguarda la ricerca di modi per fare staticamente ciò che si faceva in modo dinamico. La garbage collection in fase di compilazione sembra un buon esempio.

Si noti che dire che il processo di compilazione dovrebbe produrre codice macchina non è di aiuto. Questo è esattamente ciò che il bundling può fare in quanto l'interprete è il codice macchina (beh, la cosa può diventare un po 'più complessa con la compilazione incrociata).


3
" impossibile è una grande parola" Una parola molto grande. =)
Brian S

3
Se si definisce "compilazione" per fare riferimento a una sequenza di passaggi che avvengono interamente prima che un programma in esecuzione riceva il suo primo input e l'interpretazione come il processo di avere il flusso del programma di controllo dei dati attraverso mezzi che non fanno parte del modello astratto della macchina del programma , quindi, per poter compilare una lingua, deve essere possibile che il compilatore identifichi, prima che inizi l'esecuzione, ogni possibile significato che un costrutto linguistico potrebbe avere. Nelle lingue in cui un costrutto linguistico potrebbe avere un numero illimitato di significati, la compilazione non funzionerà.
supercat

@BrianS No, non lo è, ed è impossibile provare il contrario;)
Michael Gazonda,

@supercat Non è ancora una definizione. Qual è il "significato" di un costrutto linguistico?
Rhymoid,

Adoro il concetto di visualizzare un compilatore / interprete come una sorta di esecuzione parziale!
Bergi,

17

In realtà la domanda non è circa la compilazione impossibile . Se una lingua può essere interpretata¹, allora può essere compilata in modo banale, raggruppando l'interprete con il codice sorgente. La domanda è: quali sono le caratteristiche del linguaggio che lo rendono essenzialmente l'unico modo.

Un interprete è un programma che accetta il codice sorgente come input e si comporta come specificato dalla semantica di quel codice sorgente. Se è necessario un interprete, ciò significa che la lingua include un modo per interpretare il codice sorgente. Questa funzione è chiamata eval. Se un interprete è richiesto come parte dell'ambiente di runtime della lingua, significa che la lingua include eval: o evalesiste come una primitiva o può essere codificata in qualche modo. Le lingue conosciute come linguaggi di scripting di solito includono una evalfunzionalità, così come la maggior parte dei dialetti Lisp .

Solo perché una lingua include evalnon significa che la maggior parte di essa non può essere compilata in codice nativo. Ad esempio, ci sono ottimizzatori di compilatori Lisp, che generano un buon codice nativo e che comunque supportano eval; evalIl codice può essere interpretato o compilato al volo.

evalè l'ultima funzionalità di necessità di un interprete, ma ci sono altre caratteristiche che richiedono qualcosa di meno di un interprete. Considera alcune fasi tipiche di un compilatore:

  1. parsing
  2. Tipo di controllo
  3. Generazione del codice
  4. Collegamento

evalsignifica che tutte queste fasi devono essere eseguite in fase di esecuzione. Esistono altre funzionalità che rendono difficile la compilazione nativa. Prendendolo dal basso, alcune lingue incoraggiano il collegamento tardivo fornendo modi in cui funzioni (metodi, procedure, ecc.) E variabili (oggetti, riferimenti, ecc.) Possono dipendere da modifiche al codice non locali. Ciò rende difficile (ma non impossibile) generare codice nativo efficiente: è più facile mantenere i riferimenti agli oggetti come chiamate in una macchina virtuale e lasciare che il motore VM gestisca i bind al volo.

In generale, la riflessione tende a rendere le lingue difficili da compilare in codice nativo. Una primitiva eval è un caso estremo di riflessione; molte lingue non vanno così lontano, ma hanno comunque una semantica definita in termini di macchina virtuale, che consente ad esempio al codice di recuperare una classe per nome, ispezionarne l'eredità, elencarne i metodi, chiamare un metodo, ecc. Java con JVM e C # con .NET sono due esempi famosi. Il modo più semplice per implementare questi linguaggi è compilarli in bytecode , ma ci sono comunque compilatori nativi (molti just-in-time ) che compilano almeno frammenti di programma che non usano funzionalità di riflessione avanzate.

Il controllo del tipo determina se un programma è valido. Lingue diverse hanno standard diversi per quante analisi vengono eseguite al momento della compilazione rispetto al tempo di esecuzione: una lingua è conosciuta come "tipizzata staticamente" se esegue molti controlli prima di iniziare l'esecuzione del codice e "tipizzata dinamicamente" se non lo fa. Alcune lingue includono una funzione di cast dinamico o unmarshall-and-typecheck; queste funzionalità richiedono l'incorporamento di un typechecker nell'ambiente di runtime. Ciò è ortogonale ai requisiti di includere un generatore di codice o un interprete nell'ambiente di runtime.

¹ Esercizio: definisce una lingua che non può essere interpretata.


(1) Non sono d'accordo sul raggruppamento di un interprete con il codice sorgente che conta come compilazione, ma il resto del tuo post è eccellente. (2) Totalmente d'accordo su eval. (3) Non vedo perché la riflessione renderebbe le lingue difficili da compilare in codice nativo. Objective-C ha una riflessione e (suppongo) è in genere compilato. (4) Nota vagamente correlata, il metamagico del modello C ++ viene in genere interpretato anziché compilato e quindi eseguito.
Mooing Duck

Mi è appena successo, Lua è compilata. Il evalcompila semplicemente il bytecode e quindi come un passo separato il binario esegue il bytecode. E ha sicuramente riflesso nel binario compilato.
Mooing Duck,

Su una macchina Harvard Architecture, la compilazione dovrebbe produrre codice a cui non è mai necessario accedere come "dati". Direi che le informazioni dal file sorgente che finisce per essere memorizzate come dati piuttosto che come codice non sono realmente "compilate". Non c'è niente di sbagliato in un compilatore che prende una dichiarazione simile int arr[] = {1,2,5};e genera una sezione di dati inizializzata contenente [1,2,5], ma non descriverei il suo comportamento come tradurre [1,2,5] in codice macchina. Se quasi tutti i programmi dovessero essere archiviati come dati, quale parte di essi verrebbe realmente "compilata"?
Supercat,

2
@supercat Questo è ciò che i matematici e gli informatici intendono per banale. Si adatta alla definizione matematica, ma non succede nulla di interessante.
Gilles 'SO- smetti di essere malvagio' il

@Gilles: se il termine "compilare" è riservato per la traduzione in istruzioni macchina (senza conservazione dei "dati" associati) e accetta che in un linguaggio di compilazione, il comportamento della dichiarazione dell'array non è quello di "compilare" l'array, allora ci sono alcune lingue in cui è impossibile compilare una frazione significativa del codice.
Supercat,

13

Penso che gli autori stiano assumendo che la compilazione significhi

  • il programma di origine non deve essere presente in fase di esecuzione e
  • nessun compilatore o interprete deve essere presente in fase di esecuzione.

Ecco alcune caratteristiche di esempio che lo renderebbero problematico se non "impossibile" per un tale schema:

  1. Se puoi interrogare il valore di una variabile in fase di esecuzione, facendo riferimento alla variabile con il suo nome (che è una stringa), allora avrai bisogno che i nomi delle variabili siano presenti in fase di esecuzione.

  2. Se è possibile chiamare una funzione / procedura in fase di esecuzione, facendo riferimento ad essa con il suo nome (che è una stringa), saranno necessari i nomi di funzione / procedura in fase di esecuzione.

  3. Se puoi costruire un pezzo di programma in fase di esecuzione (come una stringa), ad esempio eseguendo un altro programma o leggendolo da una connessione di rete ecc., Allora avrai bisogno di un interprete o di un compilatore in fase di esecuzione per eseguire questo pezzo di programma.

Lisp ha tutte e tre le caratteristiche. Quindi, i sistemi Lisp hanno sempre un interprete caricato in fase di esecuzione. Lingue Java e C # hanno nomi di funzioni disponibili in fase di esecuzione e tabelle per cercare cosa significano. Probabilmente anche lingue come Basic e Python hanno nomi di variabili in fase di esecuzione. (Non ne sono sicuro al 100%).


E se "l'interprete" è compilato nel codice? Ad esempio, utilizzando le tabelle di invio per chiamare metodi virtuali, si tratta di un esempio di interpretazione o compilazione?
Erwin Bolwidt,

2
"nessun compilatore o interprete deve essere presente in fase di esecuzione", eh? Bene, se è vero, quindi in un certo senso, C non può essere "compilato" sulla maggior parte delle piattaforme. Il runtime C non ha molto da fare: avvio, impostazione di stack e così via e arresto per l' atexitelaborazione. Ma deve ancora essere lì.
Pseudonimo,

1
"I sistemi Lisp hanno sempre un interprete caricato in fase di esecuzione." - Non necessariamente. Molti sistemi Lisp hanno un compilatore in fase di esecuzione. Alcuni non hanno nemmeno un interprete.
Jörg W Mittag,

2
Bel tentativo, ma en.wikipedia.org/wiki/Lisp_machine#Technical_overview . Compilano Lisp e sono progettati per eseguire il risultato in modo efficiente.
Peter - Ripristina Monica il

@Pseudonimo: il runtime C è una libreria, non un compilatore né un interprete.
Mooing Duck

8

è possibile che le risposte attuali stiano "ripensando" l'affermazione / le risposte. forse ciò a cui gli autori si riferiscono è il seguente fenomeno. molte lingue hanno un comando "eval" come; ad esempio vedere javascript eval e il suo comportamento è comunemente studiato come una parte speciale della teoria CS (ad esempio dire Lisp). la funzione di questo comando è di valutare la stringa nel contesto della definizione della lingua. quindi in effetti ha una somiglianza con un "compilatore incorporato". il compilatore non può conoscere il contenuto della stringa fino al runtime. pertanto la compilazione del risultato di valutazione in codice macchina non è possibile al momento della compilazione.

altre risposte sottolineano che la distinzione tra linguaggi interpretati e compilati può offuscare in modo significativo in molti casi, specialmente con linguaggi più moderni come dire Java con un compilatore "just in time" alias "Hotspot" (i motori javascript, ad esempio V8, utilizzano sempre più questa stessa tecnica). La funzionalità "simil-simile" è sicuramente una di queste.


2
V8 è un buon esempio. È un compilatore puro, non c'è mai nessuna interpretazione in corso. Tuttavia supporta ancora la semantica completa di ECMAScript, incluso senza restrizioni eval.
Jörg W Mittag,

1
Lua fa la stessa cosa.
Mooing Duck,

3

LISP è un terribile esempio, poiché è stato concepito come una sorta di linguaggio "macchina" di livello superiore come base per un linguaggio "reale". Detto linguaggio "reale" non si è mai materializzato. Le macchine LISP sono state costruite sull'idea di fare (gran parte) di LISP nell'hardware. Poiché un interprete LISP è solo un programma, in linea di principio è possibile implementarlo in un circuito. Non pratico, forse; ma tutt'altro che impossibile.

Inoltre, ci sono molti interpreti programmati in silicio, normalmente chiamati "CPU". Ed è spesso utile interpretare (non ancora esistenti, non a portata di mano, ...) codici macchina. Ad esempio, Linux x86_64 è stato scritto e testato per la prima volta su emulatori. C'erano distribuzioni complete a portata di mano quando i chip sono arrivati ​​sul mercato, anche solo per i primi utenti / tester. Java viene spesso compilato nel codice JVM, che è un interprete che non sarebbe troppo difficile da scrivere in silicio.

La maggior parte delle lingue "interpretate" sono compilate in una forma interna, che viene ottimizzata e quindi interpretata. Questo è ad esempio ciò che fanno Perl e Python. Ci sono anche compilatori per linguaggi interpretati come quelli da interpretare, come la shell Unix. D'altra parte è possibile interpretare linguaggi tradizionalmente compilati. Un esempio un po 'estremo che ho visto è stato un editor che utilizzava interpretato C come linguaggio di estensione. È in grado di eseguire programmi normali, ma semplici, senza problemi.

D'altro canto, le moderne CPU prendono persino l'input "linguaggio macchina" e lo traducono in istruzioni di livello inferiore, che vengono poi riordinate e ottimizzate (cioè "compilate") prima di essere consegnate per l'esecuzione.

Tutta questa distinzione tra "compilatore" e "interprete" è davvero discutibile, da qualche parte nello stack c'è un interprete finale che prende "codice" ed esegue "direttamente". L'input del programmatore subisce trasformazioni lungo la linea, che di quelle che si chiama "compilazione" sta semplicemente disegnando una linea arbitraria nella sabbia.


1

La realtà è che c'è una grande differenza tra l'interpretazione di alcuni programmi di base e l'esecuzione dell'assemblatore. E ci sono aree intermedie con P-code / byte-code con o senza compilatori (just-in-time). Quindi proverò a riassumere alcuni punti nel contesto di questa realtà.

  • Se il modo in cui viene analizzato il codice sorgente dipende dalle condizioni di runtime, la scrittura di un compilatore potrebbe diventare impossibile o così difficile da non disturbare nessuno.

  • Il codice che si modifica da solo è impossibile da compilare nel caso generale.

  • Un programma che utilizza una funzione simile ad Eval di solito non può essere compilato completamente in anticipo (se consideri la stringa fornita ad esso come parte del programma), anche se se eseguirai ripetutamente il codice di valutazione potrebbe essere ancora utile per far richiamare il compilatore dalla tua funzione simile a eval Alcune lingue forniscono un'API per il compilatore per renderlo semplice.

  • La possibilità di fare riferimento alle cose per nome non preclude la compilazione, ma sono necessarie tabelle (come detto). Chiamare le funzioni per nome (come IDispatch) richiede un sacco di idraulica, al punto in cui penso che la maggior parte delle persone sarebbe d'accordo sul fatto che stiamo effettivamente parlando di un interprete di chiamata di funzione.

  • La digitazione debole (qualunque sia la tua definizione) rende la compilazione più difficile e forse il risultato meno efficiente, ma spesso non impossibile, a meno che valori diversi innescino analisi diverse. C'è una scala mobile qui: se il compilatore non può dedurre il tipo effettivo, dovrà emettere rami, chiamate di funzione e simili che altrimenti non ci sarebbero, incorporando efficacemente bit di interprete nell'eseguibile.


1

presumo che la caratteristica principale di un linguaggio di programmazione che rende impossibile un compilatore per il linguaggio (in senso stretto, vedi anche self-hosting ) è la funzione di auto-modifica . Significa che la lingua consente di modificare il codice sorgente durante il runtime (non è possibile eseguire un generatore che genera, fisso e statico, il codice oggetto). Un classico esempio è Lisp (vedi anche omoiconicità ). Funzionalità simili sono fornite usando un costrutto linguistico come eval , incluso in molte lingue (ad esempio javaScript). Eval in realtà chiama l'interprete (come una funzione) in fase di esecuzione .

In altre parole, la lingua può rappresentare il proprio meta-sistema (vedi anche Metaprogrammazione )

Nota che la riflessione del linguaggio , nel senso di interrogare sui meta-dati di un certo codice sorgente, e possibilmente modificare solo i meta-dati, (come il meccanismo di riflessione di Java o PHP) non è problematica per un compilatore, poiché ha già quelli i metadati al momento della compilazione e possono renderli disponibili al programma compilato, se necessario, se necessario.

Un'altra caratteristica che rende la compilazione difficile o meno l'opzione migliore (ma non impossibile) è lo schema di digitazione utilizzato nel linguaggio (ovvero digitazione dinamica vs digitazione statica e digitazione forte vs digitazione libera). Ciò rende difficile per il compilatore avere tutta la semantica in fase di compilazione, in modo efficace una parte del compilatore (in altre parole un interprete) diventa parte del codice generato che gestisce la semantica in fase di esecuzione . In altre parole, questa non è compilazione ma interpretazione .


LISP è un terribile esempio, poiché è stato concepito come uno sprt
vonbrand

@Vonbrand, forse ma mostra sia il concetto di omoiconicità sia la dualità uniforme di codice dati
Nikos M.

-1

Penso che la domanda originale non sia ben formata. Gli autori della domanda potrebbero aver voluto porre una domanda un po 'diversa: quali proprietà di un linguaggio di programmazione facilitano la scrittura di un compilatore per esso?

Ad esempio, è più semplice scrivere un compilatore per un linguaggio privo di contesto rispetto a un linguaggio sensibile al contesto. La grammatica che definisce una lingua può anche avere problemi che la rendono difficile da compilare, come le ambiguità. Tali problemi possono essere risolti ma richiedono uno sforzo extra. Allo stesso modo, le lingue definite da grammatiche senza restrizioni sono più difficili da analizzare rispetto alle lingue sensibili al contesto (vedi Gerarchia di Chomsky ). Per quanto ne so, i linguaggi di programmazione procedurale più utilizzati sono vicini al contesto, ma presentano alcuni elementi sensibili al contesto, il che li rende relativamente facili da compilare.


2
La domanda intende chiaramente opporsi / confrontare compilatori e interpreti. Mentre possono funzionare in modo diverso, e di solito fanno eccezione per il caso limite @Raphael sopra, hanno esattamente gli stessi problemi per quanto riguarda l'analisi della sintassi e l'ambiguità. Quindi la sintassi non può essere il problema. Credo anche che il problema sintattico non sia di solito la principale preoccupazione nella scrittura di compilatori al giorno d'oggi, sebbene lo sia stato in passato. Non sono il downvoter: preferisco commentare.
babou,

-1

La domanda ha una risposta corretta così ovvia che in genere viene trascurata come banale. Ma importa in molti contesti ed è la ragione principale per cui esistono le lingue interpretate:

La compilazione del codice sorgente in codice macchina è impossibile se non si dispone ancora del codice sorgente.

Gli interpreti aggiungono flessibilità e, in particolare, aggiungono la flessibilità dell'esecuzione del codice che non era disponibile al momento della compilazione del progetto sottostante.


2
"Ho perso il codice sorgente" non è una proprietà di un linguaggio di programmazione ma di un programma particolare, quindi non risponde alla domanda. E hai sicuramente bisogno di una citazione per l'affermazione che evitare la perdita del codice sorgente è "la ragione principale per cui esistono le lingue interpretate", o anche una ragione per cui esistono.
David Richerby,

1
@DavidRicherby Immagino che il caso d'uso che tyleri abbia in mente sia l'interpretazione interattiva, ovvero il codice inserito in fase di esecuzione. Concordo, tuttavia, che non rientra nell'ambito della domanda poiché non è una caratteristica del linguaggio.
Raffaello

@DavidRicherby e Raphael, dico che l'autore di questo post implica (ciò che descrivo nella mia risposta) come la caratteristica di auto-modifica che, naturalmente, è un linguaggio costruito in base alla progettazione e non un artefatto di un programma specifico
Nikos M.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.