È un argomento importante, ma piuttosto che spazzarti via con un pomposo "vai a leggere un libro, ragazzo", invece ti darò volentieri suggerimenti per aiutarti a avvolgerti la testa.
La maggior parte dei compilatori e / o interpreti lavorano in questo modo:
Tokenize : scansiona il testo del codice e lo divide in un elenco di token.
Questo passaggio può essere complicato perché non puoi semplicemente dividere la stringa su spazi, devi riconoscere che if (bar) foo += "a string";
è un elenco di 8 token: WORD, OPEN_PAREN, WORD, CLOSE_PAREN, WORD, ASIGNMENT_ADD, STRING_LITERAL, TERMINATOR. Come puoi vedere, semplicemente dividere il codice sorgente sugli spazi non funzionerà, devi leggere ogni carattere come una sequenza, quindi se incontri un carattere alfanumerico continui a leggere i caratteri fino a quando non colpisci un carattere non alfanico e quella stringa che appena letto è una WORD che verrà ulteriormente classificata in seguito. Puoi decidere tu stesso quanto granulare è il tuo tokenizer: se ingoia "a string"
come un token chiamato STRING_LITERAL per essere ulteriormente analizzato in seguito, o se vede"a string"
come OPEN_QUOTE, UNPARSED_TEXT, CLOSE_QUOTE o qualsiasi altra cosa, questa è solo una delle tante scelte che devi decidere tu stesso mentre lo stai codificando.
Lex : Quindi ora hai un elenco di token. Probabilmente hai taggato alcuni token con una classificazione ambigua come WORD perché durante il primo passaggio non fai troppi sforzi cercando di capire il contesto di ogni stringa di caratteri. Quindi ora leggi di nuovo il tuo elenco di token di origine e riclassifica ciascuno dei token ambigui con un tipo di token più specifico basato sulle parole chiave nella tua lingua. Quindi hai una WORD come "if", e "if" è nel tuo elenco di parole chiave speciali chiamato simbolo IF, quindi cambi il tipo di simbolo di quel token da WORD a IF e qualsiasi WORD che non è nell'elenco delle parole chiave speciali , come WORD foo, è un IDENTIFICATORE.
Parse : ora hai trasformato if (bar) foo += "a string";
un elenco di token lexed che assomiglia a questo: IF OPEN_PAREN IDENTIFER CLOSE_PAREN IDENTIFIER ASIGN_ADD STRING_LITERAL TERMINATOR. Il passaggio consiste nel riconoscere sequenze di token come istruzioni. Questo sta analizzando. Puoi farlo usando una grammatica come:
STATEMENT: = ASIGN_EXPRESSION | IF_STATEMENT
IF_STATEMENT: = IF, PAREN_EXPRESSION, STATEMENT
ASIGN_EXPRESSION: = IDENTIFICATORE, ASIGN_OP, VALUE
PAREN_EXPRESSSION: = OPEN_PAREN, VALUE, CLOSE_PAREN
VALORE: = IDENTIFICATORE | STRING_LITERAL | PAREN_EXPRESSION
ASIGN_OP: = EQUAL | ASIGN_ADD | ASIGN_SUBTRACT | ASIGN_MULT
Le produzioni che usano "|" tra termini significa "corrisponde a uno di questi", se sono presenti virgole tra termini significa "corrisponde a questa sequenza di termini"
Come lo usi? A partire dal primo token, prova ad abbinare la sequenza di token a queste produzioni. Quindi, prima provi a far corrispondere la tua lista di token con STATEMENT, quindi leggi la regola per STATEMENT e dice "uno STATEMENT è un ASIGN_EXPRESSION o un IF_STATEMENT", quindi provi prima ad abbinare ASIGN_EXPRESSION, quindi cerchi la regola grammaticale per ASIGN_EXPRESSION e dice "ASIGN_EXPRESSION è un IDENTIFICATORE seguito da un ASIGN_OP seguito da un VALORE, quindi cerchi la regola grammaticale per IDENTIFIER e vedi che non esiste un ruke grammaticale per IDENTIFIER in modo che significhi che IDENTIFIER sia un" terminale ", il che significa che non richiede ulteriori analizzando per abbinarlo in modo da poter provare ad abbinarlo direttamente con il token, ma il primo token di origine è un IF e IF non è lo stesso di un IDENTIFICATORE, pertanto la corrispondenza non è riuscita. E adesso? Torna alla regola STATEMENT e prova a trovare il termine successivo: IF_STATEMENT. Cerca IF_STATEMENT, inizia con IF, cerca IF, IF è un terminale, confronta il terminale con il tuo primo token, corrispondenze token IF, fantastico continua, il prossimo termine è PAREN_EXPRESSION, cerca PAREN_EXPRESSION, non è un terminale, qual è il primo termine, PAREN_EXPRESSION inizia con OPEN_PAREN, cerca OPEN_PAREN, è un terminale, abbina OPEN_PAREN al tuo prossimo token, corrisponde, ... e così via.
Il modo più semplice per avvicinarti a questo passaggio è avere una funzione chiamata parse () che gli passi il token del codice sorgente che stai cercando di abbinare e il termine grammaticale con cui stai cercando di abbinarlo. Se il termine grammaticale non è un terminale, allora si ricorre: si chiama di nuovo parse () passandogli lo stesso token sorgente e il primo termine di questa regola grammaticale. Questo è il motivo per cui è chiamato un "parser di discendenza ricorsiva" La funzione parse () restituisce (o modifica) la posizione corrente nella lettura dei token di origine, in sostanza restituisce l'ultimo token nella sequenza abbinata e si continua la chiamata successiva a parse () da lì.
Ogni volta che parse () corrisponde a una produzione come ASIGN_EXPRESSION crei una struttura che rappresenta quel pezzo di codice. Questa struttura contiene riferimenti ai token di origine originali. Inizi a costruire un elenco di queste strutture. Chiameremo questa intera struttura l'albero astratto di sintassi (AST)
Compila e / o Esegui : per determinate produzioni nella tua grammatica hai creato funzioni di gestione che, se assegnate una struttura AST, compilerebbero o eseguiranno quel blocco di AST.
Quindi diamo un'occhiata al pezzo del tuo AST che ha il tipo ASIGN_ADD. Quindi come interprete hai una funzione ASIGN_ADD_execute (). Questa funzione viene passata come parte dell'AST che corrisponde all'albero di analisi per foo += "a string"
, quindi questa funzione esamina quella struttura e sa che il primo termine nella struttura deve essere un IDENTIFICATORE, e il secondo termine è il VALORE, quindi ASIGN_ADD_execute () passa il termine VALUE a una funzione VALUE_eval () che restituisce un oggetto che rappresenta il valore valutato in memoria, quindi ASIGN_ADD_execute () esegue una ricerca di "pippo" nella tabella delle variabili e memorizza un riferimento a tutto ciò che è stato restituito da eval_value () funzione.
Questo è un interprete. Un compilatore avrebbe invece le funzioni del gestore che traducono l'AST in codice byte o codice macchina anziché eseguirlo.
I passaggi da 1 a 3 e alcuni 4 possono essere semplificati utilizzando strumenti come Flex e Bison. (alias Lex e Yacc), ma scrivere da soli un interprete è probabilmente l'esercizio più potente che un programmatore possa realizzare. Tutte le altre sfide di programmazione sembrano insignificanti dopo il summit di questo.
Il mio consiglio è di iniziare in piccolo: un linguaggio minuscolo, con una grammatica minuscola, e provare ad analizzare ed eseguire alcune semplici affermazioni, quindi crescere da lì.
Leggi questi e buona fortuna!
http://www.iro.umontreal.ca/~felipe/IFT2030-Automne2002/Complements/tinyc.c
http://en.wikipedia.org/wiki/Recursive_descent_parser
lex
,yacc
ebison
.