I parser normali come vengono generalmente insegnati hanno uno stadio lexer prima che il parser tocchi l'input. Il lexer (anche "scanner" o "tokenizer") taglia l'input in piccoli token annotati con un tipo. Ciò consente al parser principale di utilizzare i token come elementi terminali anziché dover trattare ogni carattere come un terminale, il che porta a notevoli guadagni di efficienza. In particolare, il lexer può anche rimuovere tutti i commenti e gli spazi bianchi. Tuttavia, una fase di tokenizer separata significa che le parole chiave non possono essere utilizzate anche come identificatori (a meno che la lingua non supporti lo scostamento che è in qualche modo sfuggito al favore o prefisso tutti gli identificatori con un sigillo simile $foo).
Perché? Supponiamo di avere un tokenizer semplice che comprenda i seguenti token:
FOR = 'for'
LPAREN = '('
RPAREN = ')'
IN = 'in'
IDENT = /\w+/
COLON = ':'
SEMICOLON = ';'
Il tokenizer corrisponderà sempre al token più lungo e preferirà le parole chiave rispetto agli identificatori. Quindi interestingsarà lasciato in sospeso come IDENT:interesting, ma insarà lasciato in sospeso come IN, mai come IDENT:interesting. Uno snippet di codice simile
for(var in expression)
verrà tradotto nel flusso di token
FOR LPAREN IDENT:var IN IDENT:expression RPAREN
Finora funziona. Ma ogni variabile inverrebbe lassata come parola chiave INanziché come variabile, il che spezzerebbe il codice. Il lexer non mantiene alcuno stato tra i token e non può sapere che di insolito dovrebbe essere una variabile tranne quando siamo in un ciclo for. Inoltre, il seguente codice dovrebbe essere legale:
for(in in expression)
Il primo insarebbe un identificatore, il secondo sarebbe una parola chiave.
Esistono due reazioni a questo problema:
Le parole chiave contestuali sono confuse, riutilizziamo invece le parole chiave.
Java ha molte parole riservate, alcune delle quali sono inutili tranne che per fornire messaggi di errore più utili ai programmatori che passano a Java dal C ++. L'aggiunta di nuove parole chiave interrompe il codice. L'aggiunta di parole chiave contestuali è fonte di confusione per un lettore del codice a meno che non abbiano una buona evidenziazione della sintassi e renda difficile l'implementazione degli strumenti perché dovranno utilizzare tecniche di analisi più avanzate (vedi sotto).
Quando vogliamo estendere la lingua, l'unico approccio sano è usare simboli che in precedenza non erano legali nella lingua. In particolare, questi non possono essere identificatori. Con la sintassi del ciclo foreach, Java ha riutilizzato la :parola chiave esistente con un nuovo significato. Con lambdas, Java ha aggiunto una ->parola chiave che in precedenza non poteva essere presente in alcun programma legale ( -->sarebbe comunque stata lasciata libera da ciò '--' '>'che è legale e ->potrebbe essere stata precedentemente lasciata libera come '-', '>', ma quella sequenza sarebbe stata respinta dal parser).
Le parole chiave contestuali semplificano le lingue, implementiamole
I Lexer sono indiscutibilmente utili. Ma invece di eseguire un lexer prima del parser, possiamo eseguirli in tandem con il parser. I parser bottom-up conoscono sempre l'insieme di tipi di token che sarebbero accettabili in una determinata posizione. Il parser può quindi richiedere al lexer di abbinare uno di questi tipi nella posizione corrente. In un ciclo for-each, il parser sarebbe nella posizione indicata dalla ·grammatica (semplificata) dopo che la variabile è stata trovata:
for_loop = for_loop_cstyle | for_each_loop
for_loop_cstyle = 'for' '(' declaration · ';' expression ';' expression ')'
for_each_loop = 'for' '(' declaration · 'in' expression ')'
In quella posizione, i token legali sono SEMICOLONo IN, ma non IDENT. Una parola chiave insarebbe del tutto inequivocabile.
In questo esempio particolare, i parser top-down non avrebbero problemi neanche perché possiamo riscrivere la grammatica sopra
for_loop = 'for' '(' declaration · for_loop_rest ')'
for_loop_rest = · ';' expression ';' expression
for_loop_rest = · 'in' expression
e tutti i token necessari per la decisione possono essere visualizzati senza backtracking.
Considera l'usabilità
Java ha sempre teso alla semplicità semantica e sintattica. Ad esempio, la lingua non supporta il sovraccarico dell'operatore perché renderebbe il codice molto più complicato. Quindi, quando decidiamo tra ine :per una sintassi per ogni ciclo, dobbiamo considerare quale è meno confuso e più evidente per gli utenti. Il caso estremo sarebbe probabilmente
for (in in in in())
for (in in : in())
(Nota: Java ha spazi dei nomi separati per nomi di tipo, variabili e metodi. Penso che questo sia stato un errore, soprattutto. Ciò non significa che la progettazione della lingua successiva debba aggiungere altri errori.)
Quale alternativa fornisce separazioni visive più chiare tra la variabile di iterazione e la raccolta iterata? Quale alternativa può essere riconosciuta più rapidamente quando dai un'occhiata al codice? Ho scoperto che i simboli di separazione sono migliori di una serie di parole quando si tratta di questi criteri. Altre lingue hanno valori diversi. Ad esempio Python spiega molti operatori in inglese in modo che possano essere letti in modo naturale e siano facili da capire, ma quelle stesse proprietà possono rendere abbastanza difficile comprendere un pezzo di Python a colpo d'occhio.