In arrivo con token per un lexer


14

Sto scrivendo un parser per un linguaggio di markup che ho creato (scrivendo in Python, ma non è molto rilevante per questa domanda - infatti se questa sembra una cattiva idea, mi piacerebbe un suggerimento per un percorso migliore) .

Sto leggendo qui i parser: http://www.ferg.org/parsing/index.html e sto lavorando alla scrittura del lexer che, se ho capito bene, dovrebbe dividere il contenuto in token. Quello che ho difficoltà a capire è quali tipi di token dovrei usare o come crearli. Ad esempio, i tipi di token nell'esempio a cui sono collegato sono:

  • CORDA
  • IDENTIFIER
  • NUMERO
  • WHITESPACE
  • COMMENTO
  • EOF
  • Molti simboli come {e (contano come il proprio tipo di token

Il problema che sto riscontrando è che i tipi di token più generali mi sembrano un po 'arbitrari. Ad esempio, perché STRING ha il proprio tipo di token separato rispetto a IDENTIFICATORE. Una stringa può essere rappresentata come STRING_START + (IDENTIFIER | WHITESPACE) + STRING_START.

Questo potrebbe anche avere a che fare con le difficoltà della mia lingua. Ad esempio, le dichiarazioni delle variabili sono scritte come {var-name var value}e distribuite con {var-name}. Sembra '{'e '}'dovrebbe essere i propri token, ma sono nome_var e VAR_VALUE tipi di token ammissibili, o sarebbero questi entrambi cadono sotto IDENTIFIER? Inoltre, VAR_VALUE può effettivamente contenere spazi bianchi. Lo spazio bianco dopo var-nameviene utilizzato per indicare l'inizio del valore nella dichiarazione. Qualsiasi altro spazio bianco fa parte del valore. Questo spazio bianco diventa il suo token? Lo spazio bianco ha solo quel significato in questo contesto. Inoltre, {potrebbe non essere l'inizio di una dichiarazione variabile .. dipende dal contesto (c'è di nuovo quella parola!). {:avvia una dichiarazione di nome e{ può anche essere usato come parte di un certo valore.

Il mio linguaggio è simile a Python in quanto i blocchi sono creati con rientro. Stavo leggendo come Python utilizza il lexer per creare token INDENT e DEDENT (che servono più o meno come cosa {e }farebbero in molte altre lingue). Python afferma di essere privo di contesto, il che significa per me che almeno il lexer non dovrebbe preoccuparsi di dove si trova nello stream durante la creazione di token. Come fa il lexer di Python a sapere che sta costruendo un token INDENT di una lunghezza specifica senza conoscere i caratteri precedenti (ad esempio che la riga precedente era una nuova riga, quindi iniziare a creare gli spazi per INDENT)? Lo chiedo perché devo sapere anche questo.

La mia ultima domanda è la più stupida: perché è persino necessario un lexer? Mi sembra che il parser possa andare carattere per carattere e capire dove si trova e cosa si aspetta. Il lexer aggiunge il vantaggio della semplicità?


2
Vai avanti e prova a scrivere un parser senza scanner. Se funziona (immagino che il risultato potrebbe essere troppo ambiguo per alcuni algoritmi di analisi), è probabile che non vedrai alcuna grammatica effettiva sotto tutto lo "spazio bianco consentito anche qui" e "aspetta, stavo analizzando un identificatore o un numero? ". Parlo per esperienza.

Perché reinventare una ruota personalizzata? Invece di progettare un linguaggio che richiede un lexer personalizzato, hai preso in considerazione l'uso di un linguaggio esistente già fornito con un lexer incorporato, come LISP o FORTH?
John R. Strohm,

2
@ JohnR.Strohm per scopi accademici. La stessa lingua probabilmente non sarebbe comunque praticamente utile.
Pillole di esplosione il

Risposte:


11

La tua domanda (come suggerisce il paragrafo finale) non riguarda in realtà il lexer, si tratta della progettazione corretta dell'interfaccia tra il lexer e il parser. Come puoi immaginare, ci sono molti libri sulla progettazione di lexer e parser. Mi piace il libro di analisi di Dick Grune , ma potrebbe non essere un buon libro introduttivo. Non mi piace molto il libro basato su C di Appel , perché il codice non è utilmente estendibile nel tuo compilatore (a causa dei problemi di gestione della memoria inerenti alla decisione di fingere C è come ML). La mia introduzione è stata il libro di PJ Brown , ma non è una buona introduzione generale (anche se abbastanza buona per gli interpreti in particolare). Ma torniamo alla tua domanda.

La risposta è: fai il più possibile nel lexer senza bisogno di usare vincoli rivolti in avanti o all'indietro.

Ciò significa che (a seconda ovviamente dei dettagli della lingua) dovresti riconoscere una stringa come un carattere "seguito da una sequenza di caratteri non-" e poi un altro ". Restituiscilo al parser come una singola unità. Esistono diversi motivi per questo, ma quelli importanti sono

  1. Ciò riduce la quantità di stato che il parser deve mantenere, limitandone il consumo di memoria.
  2. Ciò consente all'implementazione del lexer di concentrarsi sul riconoscimento dei mattoni fondamentali e libera il parser per descrivere come vengono utilizzati i singoli elementi sintattici per costruire un programma.

Molto spesso i parser possono eseguire azioni immediate sulla ricezione di un token dal lexer. Ad esempio, non appena viene ricevuto IDENTIFIER, il parser può eseguire una ricerca nella tabella dei simboli per scoprire se il simbolo è già noto. Se il tuo parser analizza anche le costanti di stringa come QUOTE (SPAZI IDENTIFICATORI) * QUOTE eseguirai molte ricerche irrilevanti nella tabella dei simboli, o finirai per sollevare le ricerche della tabella dei simboli più in alto sull'albero degli elementi della sintassi del parser, perché puoi solo fare nel punto in cui ora sei sicuro di non guardare una stringa.

Per ribadire ciò che sto cercando di dire, ma in modo diverso, il lexer dovrebbe occuparsi dell'ortografia delle cose e il parser con la struttura delle cose.

Potresti notare che la mia descrizione di come appare una stringa sembra molto un'espressione regolare. Questa non è una coincidenza. Gli analizzatori lessicali sono spesso implementati in linguaggi piccoli (nel senso dell'eccellente libro di Programmazione Perle di Jon Bentley ) che usano espressioni regolari. Sono solo abituato a pensare in termini di espressioni regolari quando riconosco il testo.

Per quanto riguarda la tua domanda sugli spazi bianchi, riconoscila nel lexer. Se la tua lingua è destinata a essere piuttosto in formato libero, non restituire i token WHITESPACE al parser, perché dovrà solo buttarli via, quindi le regole di produzione del tuo parser saranno spammate con rumore essenzialmente - cose da riconoscere solo per lanciare loro via.

Per quanto riguarda ciò che significa su come dovresti gestire gli spazi bianchi quando è sintatticamente significativo, non sono sicuro di poter esprimere un giudizio per te che funzionerà davvero bene senza sapere di più sulla tua lingua. Il mio rapido giudizio è di evitare casi in cui gli spazi bianchi a volte sono importanti e talvolta no, e utilizzare un tipo di delimitatore (come le virgolette). Tuttavia, se non riesci a progettare la lingua nel modo che preferisci, questa opzione potrebbe non essere disponibile per te.

Esistono altri modi per progettare sistemi di analisi del linguaggio di progettazione. Certamente ci sono sistemi di costruzione di compilatori che ti permettono di specificare un sistema lexer e parser combinato (penso che la versione Java di ANTLR faccia questo) ma non ne ho mai usato uno.

Ultima nota storica. Decenni fa, era importante che il lexer facesse il più possibile prima di passare al parser, perché i due programmi non si sarebbero adattati alla memoria contemporaneamente. Fare di più nel lexer ha lasciato più memoria disponibile per rendere intelligente il parser. Ho usato il compilatore C di Whitesmiths per un certo numero di anni e, se ho capito bene, avrebbe funzionato in soli 64 KB di RAM (era un programma MS-DOS di piccolo modello) e anche così traduceva una variante di C che era molto vicino all'ANSI C.


Una buona nota storica sulla dimensione della memoria è uno dei motivi per dividere il lavoro in lexer e parser in primo luogo.
stevegt,

3

Prenderò la tua ultima domanda, che in realtà non è stupida. I parser possono costruire costrutti complessi su una base carattere per carattere. Se ricordo, la grammatica di Harbison e Steele ("C - Un manuale di riferimento") ha produzioni che usano singoli caratteri come terminali e costruiscono identificatori, stringhe, numeri, ecc. Come non terminali dai singoli caratteri.

Dal punto di vista dei linguaggi formali, tutto ciò che un lexer basato su espressioni regolari può riconoscere e classificare come "stringa letterale", "identificativo", "numero", "parola chiave" e così via, persino un parser LL (1) può riconoscere. Quindi non c'è alcun problema teorico con l'uso di un generatore di parser per riconoscere tutto.

Da un punto di vista algoritmico, un riconoscitore di espressioni regolari può funzionare molto più velocemente di qualsiasi parser. Da un punto di vista cognitivo, è probabilmente più facile per un programmatore interrompere il lavoro tra un lexer di espressioni regolari e un parser scritto da un generatore di parser.

Direi che considerazioni pratiche fanno sì che le persone prendano la decisione di avere lexer e parser separati.


Sì, e lo stesso standard C fa la stessa cosa, come se ricordo bene, entrambe le edizioni di Kernighan e Ritchie.
James Youngman,

3

Sembra che tu stia tentando di scrivere un lexer / parser senza capire veramente le grammatiche. In genere, quando le persone scrivono un lexer e un parser, le scrivono per conformarsi ad una certa grammatica. Il lexer dovrebbe restituire i token nella grammatica mentre il parser usa quei token per abbinare regole / non terminali . Se potessi analizzare facilmente l'input andando solo byte per byte, un lexer e un parser potrebbero essere eccessivi.

I Lexer semplificano le cose.

Panoramica grammaticale : una grammatica è un insieme di regole per l'aspetto di una sintassi o input. Ad esempio, ecco una grammatica giocattolo (simple_command è il simbolo di avvio):

simple_command:
 WORD DIGIT AND_SYMBOL
simple_command:
     addition_expression

addition_expression:
    NUM '+' NUM

Questa grammatica significa che -
Un comando_composto è composto da
A) WORD seguito da DIGIT seguito da AND_SYMBOL (questi sono "token" che definisco)
B) Una "aggiunta_espressione" (questa è una regola o "non terminale")

Un'addizione_espressione è composta da:
NUM seguito da un '+' seguito da un NUM (NUM è un "token" che definisco, '+' è un segno più letterale).

Pertanto, poiché simple_command è il "simbolo iniziale" (il punto in cui inizio), quando ricevo un token, controllo se si adatta a simple_command. Se il primo token nell'input è una WORD e il token successivo è un DIGIT e il token successivo è un AND_SYMBOL, allora ho abbinato un comando semplice e posso agire. Altrimenti, proverò ad abbinarlo all'altra regola di simple_command che è addizione_espressione. Pertanto, se il primo token era un NUM seguito da un '+' seguito da un NUM, allora ho abbinato un comando semplice e ho preso alcune misure. Se non è nessuna di queste cose, allora ho un errore di sintassi.

Questa è un'introduzione molto, molto basilare alle grammatiche. Per una comprensione più approfondita, consulta questo articolo wiki e cerca nel Web esercitazioni grammaticali senza contesto.

Utilizzando una disposizione lexer / parser, ecco un esempio di come potrebbe apparire il tuo parser:

bool simple_command(){
   if (peek_next_token() == WORD){
       get_next_token();
       if (get_next_token() == DIGIT){
           if (get_next_token() == AND_SYMBOL){
               return true;
           } 
       }
   }
   else if (addition_expression()){
       return true;
   }

   return false;
}

bool addition_expression(){
    if (get_next_token() == NUM){
        if (get_next_token() == '+'){
             if (get_next_token() == NUM){
                  return true;
             }
        }
    }
    return false;
}

Ok, quindi quel codice è un po 'brutto e non consiglierei mai le istruzioni if ​​nidificate triple. Ma il punto è, immagina di provare a fare quella cosa sopra carattere per carattere invece di usare le tue funzioni modulari "get_next_token" e "peek_next_token" . Seriamente, provaci. Non ti piacerà il risultato. Ora tieni presente che quella grammatica sopra è circa 30 volte meno complessa di quasi qualsiasi grammatica utile. Vedi i vantaggi dell'utilizzo di un lexer?

Onestamente, lexer e parser non sono gli argomenti più basilari al mondo. Consiglierei prima di leggere e comprendere le grammatiche, quindi leggere un po 'di lexer / parser, quindi immergermi.


Hai qualche consiglio da imparare sulle grammatiche?
Pillole di esplosione il

Ho appena modificato la mia risposta per includere un'introduzione di base alla grammatica e alcuni suggerimenti per ulteriori approfondimenti. Le grammatiche sono un argomento molto importante nell'informatica, quindi vale la pena impararle.
Casey Patton,

1

La mia ultima domanda è la più stupida: perché è persino necessario un lexer? Mi sembra che il parser possa andare carattere per carattere e capire dove si trova e cosa si aspetta.

Questo non è stupido, è solo la verità.

Ma la praticabilità dipende in qualche modo dai tuoi strumenti e obiettivi. Ad esempio, se usi yacc senza un lexer e vuoi consentire lettere unicode negli identificatori, dovrai scrivere una regola grande e brutta che esplicitamente enumera tutti i caratteri validi. Mentre, in un lexer, potresti forse chiedere a una libreria di routine se un personaggio è un membro della categoria di lettere.

Usare o non usare un lexer è una questione di astrazione tra la tua lingua e il livello del personaggio. Si noti che il livello del carattere, al giorno d'oggi, è un'altra astrazione sopra il livello del byte, che è un'astrazione sopra il livello del bit.

Quindi, finalmente, potresti persino analizzare il bit level.


0
STRING_START + (IDENTIFIER | WHITESPACE) + STRING_START.

No, non può. Che dire "("? Secondo te, questa non è una stringa valida. E scappa?

In generale, il modo migliore per trattare gli spazi bianchi è ignorarlo, oltre a delimitare i token. Molte persone preferiscono spazi bianchi molto diversi e l'applicazione delle regole sugli spazi bianchi è nella migliore delle ipotesi controversa.

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.