Quando ha senso compilare prima la mia lingua in codice C?


35

Quando si progetta un proprio linguaggio di programmazione, quando ha senso scrivere un convertitore che prende il codice sorgente e lo converte in codice C o C ++ in modo che io possa usare un compilatore esistente come gcc per finire con il codice macchina? Ci sono progetti che utilizzano questo approccio?



4
Se guardi oltre C vedrai che anche C # e Java vengono compilati in lingue intermedie. Ti risparmi di dover ripetere molto lavoro che qualcun altro ha già fatto prendendo di mira una lingua intermedia invece di andare direttamente all'assemblea.
Casey,

1
@emodendroket Tuttavia, C # e Java si compilano in un IL che è progettato per essere un IL in generale e per C # / Java in particolare, quindi per molti aspetti il ​​bytecode CIL e JVM sono più sensibili e convenienti come un IL di quanto C possa mai essere. Non si tratta di utilizzare una lingua intermedia, ma di quale lingua intermedia utilizzare.

1
Guarda diverse implementazioni di software libero che generano codice C. E spero che renderai il tuo software gratuito per l'implementazione della tua lingua.
Basile Starynkevitch,

2
Ecco il link aggiornato dal commento di @ RobertHarvey: yosefk.com/blog/c-as-an-intermediate-language.html .
Christian Dean,

Risposte:


52

La traduzione in codice C è un'abitudine molto ben consolidata. La C originale con le classi (e le prime implementazioni C ++, allora chiamate Cfront ) lo fecero con successo. Lo stanno facendo diverse implementazioni di Lisp o Scheme, ad esempio Chicken Scheme , Scheme48 , Bigloo . Alcune persone tradotti Prolog a C . E così pure alcune versioni di Mozart (e ci sono stati tentativi di compilare il bytecode Ocaml in C ). Anche il sistema CAIA di intelligenza artificiale di J.Pitrat viene avviato e genera tutto il suo codice C. Vala si traduce anche in C, per il codice relativo a GTK. Il libro di Queinnec Lisp In Small Pieces avere qualche capitolo sulla traduzione in C.

Uno dei problemi quando si traduce in C sono le chiamate ricorsive della coda . Lo standard C non garantisce che un compilatore C li stia traducendo correttamente (in un "salto con argomenti", cioè senza mangiare stack di chiamate), anche se in alcuni casi, le versioni recenti di GCC (o di Clang / LLVM) effettuano tale ottimizzazione .

Un altro problema è la raccolta dei rifiuti . Diverse implementazioni usano semplicemente il garbage collector conservativo di Boehm (che è C amichevole ...). Se si desidera eseguire il garbage collection del codice (come fanno diverse implementazioni Lisp, ad esempio SBCL), questo potrebbe essere un incubo (si vorrebbe dlclosesu Posix).

Ancora un altro problema riguarda le continuazioni di prima classe e call / cc . Ma sono possibili trucchi intelligenti (guarda all'interno di Chicken Scheme). L'accesso allo stack di chiamate potrebbe richiedere molti trucchi (ma vedi backtrace GNU , ecc ....). La persistenza ortogonale di continuazioni (cioè di pile o fili) sarebbe difficile in C.

La gestione delle eccezioni è spesso una questione per emettere chiamate intelligenti a longjmp ecc ...

Potresti voler generare (nel tuo codice C emesso) #linedirettive appropriate . Questo è noioso e richiede molto lavoro (ti consigliamo di produrre, ad esempio, un gdbcodice più facilmente debuggabile).

Il mio linguaggio MELT specifico per il dominio lispy (per personalizzare o estendere GCC ) è tradotto in C (attualmente in C ++ scadente). Ha il suo generational copying garbage collector. (Potresti essere interessato da Qish o Ravenbrook MPS ). In realtà, il GC generazionale è più facile nel codice C generato dalla macchina che nel codice C scritto a mano (perché personalizzerai il tuo generatore di codice C per la tua barriera di scrittura e macchine GC).

Non conosco alcuna implementazione del linguaggio che si traduca in un codice C ++ originale, vale a dire usando una tecnica di "garbage collection" in fase di compilazione per emettere codice C ++ usando molti modelli STL e rispettando il linguaggio RAII . (per favore dì se ne conosci uno).

Ciò che è divertente oggi è che (sugli attuali desktop Linux) i compilatori C potrebbero essere abbastanza veloci da implementare un ciclo interattivo di lettura-eval-print di livello superiore tradotto in C: emetterai codice C (poche centinaia di righe) per ogni utente interazione, ne farai forkuna compilazione in un oggetto condiviso, che poi faresti dlopen. (MELT lo sta facendo tutto pronto, e di solito è abbastanza veloce). Tutto ciò potrebbe richiedere alcuni decimi di secondo ed essere accettabile dagli utenti finali.

Quando possibile, consiglierei di tradurre in C, non in C ++, in particolare perché la compilazione C ++ è lenta.

Se stai implementando la tua lingua, potresti anche considerare (invece di emettere il codice C) alcune librerie JIT come libjit , GNU lightning , asmjit o persino LLVM o GCCJIT . Se vuoi tradurre in C, a volte potresti usare tinycc : compila molto rapidamente il codice C generato (anche in memoria) per rallentare il codice macchina. Ma in generale vuoi sfruttare le ottimizzazioni fatte da un vero compilatore C come GCC

Se traduci in C la tua lingua, assicurati di creare prima l'intero AST del codice C generato in memoria (ciò semplifica anche la generazione prima di tutte le dichiarazioni, quindi di tutte le definizioni e del codice funzione). Saresti in grado di fare alcune ottimizzazioni / normalizzazioni in questo modo. Inoltre, potresti essere interessato a diverse estensioni GCC (ad esempio goto calcolate). Probabilmente vorrai evitare di generare un tempo di compilazione di funzioni di grandi dimensioni è proporzionale al quadrato della dimensione del codice funzione). Quindi limitare la dimensione delle funzioni C generate a qualche migliaio di righe ciascuna. enormi funzioni C - ad es. Di centinaia di migliaia di righe di C generato (è meglio dividerle in pezzi più piccoli) poiché l'ottimizzazione dei compilatori C è molto insoddisfacente di funzioni C molto grandi (in pratica, e sperimentalmente,gcc -O

Si noti che entrambi i compilatori C & C ++ di Clang (attraverso LLVM ) e GCC (attraverso libgccjit ) offrono un modo per emettere alcune rappresentazioni interne adatte a questi compilatori, ma farlo potrebbe (o meno) essere più difficile dell'emissione di codice C (o C ++), ed è specifico per ciascun compilatore.

Se stai progettando una lingua da tradurre in C, probabilmente vorrai avere diversi trucchi (o costrutti) per generare una miscela di C con la tua lingua. Il mio documento DSL2011 MELT: un linguaggio specifico del dominio tradotto incorporato nel compilatore GCC dovrebbe darti suggerimenti utili.


Ti riferisci a "Chicken Scheme?"
Robert Harvey,

1
Sì. Ho fornito l'URL.
Basile Starynkevitch,

È relativamente pratico creare una macchina virtuale, come Java o qualcosa del genere, compilare il bytecode in C, quindi usare gcc per la compilazione JIT? O dovrebbero semplicemente passare direttamente dal bytecode all'assemblaggio?
Panzercrisis,

1
@Panzercrisis La maggior parte dei compilatori JIT richiedono i loro backend di codice macchina per supportare cose come la sostituzione di una funzione e l'applicazione di patch al codice esistente con una porta jump / trap. A parte questo, gcc in particolare è ... architettonicamente meno adatto alla compilazione JIT e ad altri casi d'uso. Dai un'occhiata a libgccjit: gcc.gnu.org/ml/gcc-patches/2013-10/msg00228.html e gcc.gnu.org/wiki/JIT

1
Ottimo materiale di orientamento. Grazie!

8

Ha senso quando il tempo per generare il codice macchina completo supera l'inconveniente di avere una fase intermedia di compilazione del "IL" in codice macchina utilizzando un compilatore C.

In genere i linguaggi specifici del dominio vengono scritti in questo modo, un sistema di livello molto elevato viene utilizzato per definire o descrivere un processo che viene quindi compilato in un eseguibile o in una dll. Il tempo impiegato per produrre un buon working / assembly è molto più grande della generazione di C, e C è abbastanza vicino al codice assembly per le prestazioni, quindi ha senso generare C e riutilizzare le abilità degli autori del compilatore C. Nota che non si tratta solo di compilare, ma anche di ottimizzare: i ragazzi che scrivono gcc o llvm hanno trascorso molto tempo a creare un codice macchina ottimizzato, sarebbe stupido provare a reinventare tutto il loro duro lavoro.

Potrebbe essere più accettabile riutilizzare il back-end del compilatore di LLVM, che IIRC è indipendente dalla lingua, quindi si generano istruzioni LLVM invece del codice C.


Sembra che le biblioteche siano una ragione abbastanza convincente da considerare anche.
Casey,

Quando dici "il tuo" IL ", a cosa ti riferisci? Un albero di sintassi astratto?
Robert Harvey,

@RobertHarvey no, intendo il codice C. Nel caso dei PO, questa è una lingua intermedia a metà strada tra la sua lingua di alto livello e il codice macchina. L'ho messo tra virgolette per provare a trasmettere questa idea che non è IL come usato da molte persone (ad esempio .NET IL di Microsoft)
gbjbaanb

2

Scrivere un compilatore per produrre codice macchina potrebbe non essere molto più difficile che scrivere uno che produce C (in alcuni casi potrebbe essere più facile), ma un compilatore che produce codice macchina sarà in grado di produrre programmi eseguibili solo sulla particolare piattaforma per la quale è stato scritto; un compilatore che produce codice C, al contrario, può essere in grado di produrre programmi per qualsiasi piattaforma che utilizza un dialetto di C che il codice generato è progettato per supportare. Si noti che in molti casi potrebbe essere possibile scrivere un codice C che è completamente portatile e che si comporterà come desiderato senza utilizzare comportamenti non garantiti dallo standard C, ma il codice che si basa su comportamenti garantiti dalla piattaforma potrebbe essere in grado di funzionare molto più velocemente su piattaforme che offrono tali garanzie rispetto al codice che no.

Ad esempio, supponiamo che una lingua supporti una funzione per produrre UInt32da quattro byte consecutivi di un allineamento arbitrario UInt8[], interpretato in modo big-endian. Su alcuni compilatori, si potrebbe scrivere il codice come:

uint32_t dat = *(__packed uint32_t*)p;
return (dat >> 24) | (dat >> 8) | ((uint32_t)dat << 8) | ((uint32_t)dat << 24));

e fare in modo che il compilatore generi un'operazione di caricamento di parole seguita da un'istruzione di byte inversi in parola. Alcuni compilatori, tuttavia, non supportano il modificatore __packed e in sua assenza genererebbero codice che non funzionerebbe.

In alternativa, si potrebbe scrivere il codice come:

return dat[3] | ((uint16_t)dat[2] << 8) | ((uint32_t)dat[1] << 16) | ((uint32_t)dat[0] << 24);

un tale codice dovrebbe funzionare su qualsiasi piattaforma, anche quelle in cui CHAR_BITSnon è 8 (supponendo che ciascun ottetto di dati di origine sia finito in un elemento array distinto), ma tale codice potrebbe probabilmente non funzionare quasi alla stessa velocità del non portatile versione su piattaforme a supporto della prima.

Si noti che la portabilità spesso richiede che il codice sia estremamente liberale con dattiloscritti e costrutti simili. Ad esempio, il codice che desidera moltiplicare due numeri interi senza segno a 32 bit e produrre i 32 bit inferiori del risultato deve essere scritto come portabilità come:

uint32_t result = 1u*x*y;

Senza di ciò 1u, un compilatore su un sistema in cui INT_BITS variava da 33 a 64 poteva legittimamente fare qualsiasi cosa desiderasse se il prodotto di xey fosse maggiore di 2.147.483.647 e alcuni compilatori sono inclini a sfruttare tali opportunità.


1

Hai alcune eccellenti risposte sopra ma dato che, in un commento, hai risposto alla domanda "Perché vuoi creare un tuo linguaggio di programmazione in primo luogo?" Con "Sarebbe principalmente a scopo di apprendimento", " risponderò da un'altra angolazione.

Ha senso scrivere un convertitore che prende il codice sorgente e lo converte in codice C o C ++, in modo da poter usare un compilatore esistente come gcc per finire con il codice macchina, se sei più interessato a conoscere lessico, sintassi e analisi semantica di quanto tu stia imparando sulla generazione e l'ottimizzazione del codice!

Scrivere il tuo generatore di codice macchina è un lavoro piuttosto significativo che puoi evitare compilando il codice C, se non è quello che ti interessa principalmente!

Se, tuttavia, ti piacciono i programmi di assemblaggio e sei affascinato dalle sfide dell'ottimizzazione del codice al livello più basso, allora scrivi un generatore di codice per l'esperienza di apprendimento!


-7

Dipende dal sistema operativo in uso se si utilizza Windows, esiste un Microsoft IL (Intermediate Language) che converte il codice in linguaggio intermedio in modo che non ci vuole tempo per essere compilato nel codice macchina. Oppure, se stai usando Linux, esiste un compilatore separato per questo

Tornando alla tua domanda è quando quando progetti la tua lingua dovresti avere un compilatore o un interprete separato per questo perché la macchina non conosce la lingua di alto livello. Il codice deve essere compilato in codice macchina per renderlo utile per la macchina


2
Your code should be compiled into machine code to make it useful for machine- Se il compilatore ha prodotto il codice c come output, è possibile inserire il codice c nel compilatore CA per produrre il codice macchina, giusto?
Robert Harvey,

sì. perché machine non ha il linguaggio c
Tayyab Gulsher Vohra,

2
Destra. Quindi la domanda era "Quando ha senso emettere ce usare ac compilatore, piuttosto che emettere direttamente il linguaggio macchina o il codice byte?"
Robert Harvey,

in realtà chiede di progettare il suo linguaggio di programmazione in cui chiede che "lo converta in codice C o C ++". Quindi lo sto spiegando se stai progettando il tuo linguaggio di programmazione perché dovresti usare il compilatore c o c ++. se sei abbastanza intelligente dovresti progettare il tuo
Tayyab Gulsher Vohra,

8
Non credo che tu capisca la domanda. Vedi yosefk.com/blog/c-as-an-intermediate-language.html
Robert Harvey
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.