Quale dovrebbe essere il tipo di dati dei token che un lexer restituisce al suo parser?


21

Come detto nel titolo, quale tipo di dati dovrebbe restituire / dare al parser un lexer? Nel leggere l' articolo di analisi lessicale di Wikipedia, si afferma che:

Nell'informatica, l'analisi lessicale è il processo di conversione di una sequenza di caratteri (come un programma per computer o una pagina Web) in una sequenza di token ( stringhe con un "significato" identificato).

Tuttavia, in completa contraddizione con la precedente dichiarazione, quando è stata data risposta a un'altra domanda che ho posto su un altro sito ( Revisione del codice se sei curioso), la persona che ha risposto ha dichiarato che:

Il lexer di solito legge la stringa e la converte in un flusso ... di lexemi. I lessemi devono solo essere un flusso di numeri .

e ha dato questo aspetto:

nl_output => 256
output    => 257
<string>  => 258

Più avanti nell'articolo Ha menzionato Flexun lexer già esistente e ha detto che scrivere "regole" con esso sarebbe più semplice che scrivere un lexer a mano. Ha proceduto a darmi questo esempio:

Space              [ \r\n\t]
QuotedString       "[^"]*"
%%
nl_output          {return 256;}
output             {return 257;}
{QuotedString}     {return 258;}
{Space}            {/* Ignore */}
.                  {error("Unmatched character");}
%%

Per approfondire la mia conoscenza e ottenere maggiori informazioni, ho letto l'articolo di Wikipedia su Flex . l'articolo Flex mostrava che era possibile definire un insieme di regole di sintassi, con token, nel modo seguente:

digit         [0-9]
letter        [a-zA-Z]

%%
"+"                  { return PLUS;       }
"-"                  { return MINUS;      }
"*"                  { return TIMES;      }
"/"                  { return SLASH;      }
"("                  { return LPAREN;     }
")"                  { return RPAREN;     }
";"                  { return SEMICOLON;  }
","                  { return COMMA;      }
"."                  { return PERIOD;     }
":="                 { return BECOMES;    }
"="                  { return EQL;        }
"<>"                 { return NEQ;        }
"<"                  { return LSS;        }
">"                  { return GTR;        }
"<="                 { return LEQ;        }
">="                 { return GEQ;        }
"begin"              { return BEGINSYM;   }
"call"               { return CALLSYM;    }
"const"              { return CONSTSYM;   }
"do"                 { return DOSYM;      }
"end"                { return ENDSYM;     }
"if"                 { return IFSYM;      }
"odd"                { return ODDSYM;     }
"procedure"          { return PROCSYM;    }
"then"               { return THENSYM;    }
"var"                { return VARSYM;     }
"while"              { return WHILESYM;   }

Mi sembra che il lexer Flex stia restituendo stringhe di parole chiave \ token. Ma potrebbe restituire costanti uguali a determinati numeri.

Se il lexer avesse restituito dei numeri, come avrebbe letto i letterali delle stringhe? la restituzione di un numero va bene per singole parole chiave, ma come gestiresti una stringa? Il lexer non dovrebbe convertire la stringa in numeri binari e quindi il parser convertirà i numeri in una stringa. Sembra molto più logico (e più semplice) per il lexer restituire stringhe e quindi consentire al parser di convertire qualsiasi valore letterale in numeri reali.

O il lexer potrebbe restituire entrambi? Ho provato a scrivere un semplice lexer in c ++, che ti consente di avere un solo tipo di ritorno per le tue funzioni. Mi porta così a porre la mia domanda.

Per condensare la mia domanda in un paragrafo: quando si scrive un lexer e si assume che possa restituire un solo tipo di dati (stringhe o numeri), quale sarebbe la scelta più logica?


Il lexer restituisce ciò che gli dici di restituire. Se il tuo disegno richiede numeri, restituirà numeri. Ovviamente, rappresentare i letterali di stringa richiederà un po 'di più. Vedi anche È compito di un Lexer analizzare numeri e stringhe? Si noti che i letterali stringa non sono generalmente considerati "elementi del linguaggio".
Robert Harvey,

@RobertHarvey Quindi convertiresti la stringa letterale in numeri binari ?.
Christian Dean,

A quanto ho capito, lo scopo del lexer è quello di prendere gli elementi del linguaggio (come parole chiave, operatori e così via) e trasformarli in token. Pertanto, le stringhe tra virgolette non interessano al lexer, poiché non sono elementi di linguaggio. Anche se non ho mai scritto un lexer da solo, immagino che la stringa tra virgolette sia semplicemente passata invariata (comprese le virgolette).
Robert Harvey,

Quindi, quello che dici è che il lexer non legge né si preoccupa dei letterali di stringa. E quindi il parser deve cercare questi letterali stringa? Questo è molto confuso.
Christian Dean,

Potresti passare qualche minuto a leggere questo: en.wikipedia.org/wiki/Lexical_analysis
Robert Harvey,

Risposte:


10

Generalmente, se stai elaborando una lingua attraverso il lessing e l'analisi, hai una definizione dei tuoi token lessicali, ad esempio:

NUMBER ::= [0-9]+
ID     ::= [a-Z]+, except for keywords
IF     ::= 'if'
LPAREN ::= '('
RPAREN ::= ')'
COMMA  ::= ','
LBRACE ::= '{'
RBRACE ::= '}'
SEMICOLON ::= ';'
...

e hai una grammatica per il parser:

STATEMENT ::= IF LPAREN EXPR RPAREN STATEMENT
            | LBRACE STATEMENT BRACE
            | EXPR SEMICOLON
EXPR      ::= ID
            | NUMBER
            | ID LPAREN EXPRS RPAREN
...

Il tuo lexer prende il flusso di input e produce un flusso di token. Il flusso di token viene utilizzato dal parser per produrre un albero di analisi. In alcuni casi, è sufficiente conoscere il tipo di token (ad esempio, LPAREN, RBRACE, FOR), ma in alcuni casi è necessario il valore effettivo associato al token. Ad esempio, quando incontri un token ID, ti serviranno i caratteri effettivi che compongono l'ID in un secondo momento quando stai cercando di capire a quale identificatore stai cercando di fare riferimento.

Quindi, in genere hai qualcosa di più o di meno come questo:

enum TokenType {
  NUMBER, ID, IF, LPAREN, RPAREN, ...;
}

class Token {
  TokenType type;
  String value;
}

Pertanto, quando il lexer restituisce un token, sai di che tipo è (di cui hai bisogno per l'analisi) e la sequenza di caratteri da cui è stata generata (di cui avrai bisogno in seguito per interpretare stringhe e valori numerici numerici, identificatori, eccetera.). Potrebbe sembrare che stai restituendo due valori, poiché stai restituendo un tipo aggregato molto semplice, ma hai davvero bisogno di entrambe le parti. Dopotutto, vorresti trattare i seguenti programmi in modo diverso:

if (2 > 0) {
  print("2 > 0");
}
if (0 > 2) {
  print("0 > 2");
}

Questi producono la stessa sequenza di tipi di token : IF, LPAREN, NUMBER, GREATER_THAN, NUMBER, RPAREN, LBRACE, ID, LPAREN, STRING, RPAREN, SEMICOLON, RBRACE. Questo significa che analizzano anche lo stesso. Ma quando stai effettivamente facendo qualcosa con l'albero di analisi, ti preoccuperai che il valore del primo numero sia '2' (o '0') e che il valore del secondo numero sia '0' (o '2 ') e che il valore della stringa è' 2> 0 '(o' 0> 2 ').


Ottengo la maggior parte di ciò che il tuo dire, ma come è che String valueandando a riempirsi? sarà riempito con una stringa o un numero? Inoltre, come definirei il Stringtipo?
Christian Dean,

1
@ Mr.Python Nel caso più semplice, è solo la serie di personaggi che corrisponde alla produzione lessicale. Quindi, se vedi foo (23, "bar") , otterrai i token [ID, "foo"], [LPAREN, "("], [NUMBER, "23"], [COMMA, "," ], [STRING, "" 23 ""], [RPAREN, ")"] . Preservare tali informazioni potrebbe essere importante. Oppure potresti adottare un altro approccio e fare in modo che il valore abbia un tipo di unione che può essere una stringa o un numero, ecc. E scegliere il tipo di valore corretto in base al tipo di tipo di token che hai (ad esempio, quando il tipo di token è NUMBER , usa value.num e, quando è STRING, usa value.str).
Joshua Taylor,

@MrPython "Inoltre, come definirei il tipo di stringa?" Stavo scrivendo da una mentalità Java-ish. Se lavori in C ++ puoi usare il tipo di stringa di C ++, o se lavori in C, puoi usare un carattere *. Il punto è quello associato a un token, hai il valore corrispondente o il testo che puoi interpretare per produrre il valore.
Joshua Taylor,

1
@ ollydbg23 è un'opzione, non irragionevole, ma rende il sistema meno coerente internamente. Ad esempio, se si desidera il valore di stringa dell'ultima città analizzata, ora è necessario verificare esplicitamente un valore null e quindi utilizzare una ricerca da token a stringa inversa per scoprire quale sarebbe stata la stringa. Inoltre, è l'accoppiamento più stretto tra il lexer e il parser; c'è più codice da aggiornare se LPAREN potrebbe mai corrispondere a stringhe diverse o multiple.
Joshua Taylor,

2
@ ollydbg23 Un caso sarebbe un semplice pseudo-minificatore. È abbastanza facile da fare parse(inputStream).forEach(token -> print(token.string); print(' '))(ad esempio, basta stampare i valori di stringa dei token, separati da spazio). È abbastanza veloce. E anche se LPAREN può derivare solo da "(", quella potrebbe essere una stringa costante in memoria, quindi includere un riferimento ad essa nel token potrebbe non essere più costoso che includere il riferimento null. In generale, preferirei scrivere codice che non mi rende un caso speciale nessun codice
Joshua Taylor,

6

Come detto nel titolo, quale tipo di dati dovrebbe restituire / dare al parser un lexer?

"Token", ovviamente. Un lexer produce un flusso di token, quindi dovrebbe restituire un flusso di token .

Ha menzionato Flex, un lexer già esistente, e ha detto che scrivere "regole" con esso sarebbe più semplice che scrivere un lexer a mano.

I lexer generati automaticamente hanno il vantaggio di poterli generare rapidamente, il che è particolarmente utile se pensi che la tua grammatica lessicale cambierà molto. Hanno lo svantaggio che spesso non si ottiene molta flessibilità nelle scelte di implementazione.

Detto questo, a chi importa se è "più semplice"? Scrivere il lexer di solito non è la parte difficile!

Quando si scrive un lexer e si assume che possa restituire un solo tipo di dati (stringhe o numeri), quale sarebbe la scelta più logica?

Né. Un lexer in genere ha un'operazione "successiva" che restituisce un token, quindi dovrebbe restituire un token . Un token non è una stringa o un numero. È un segno.

L'ultimo lexer che ho scritto è stato un lexer a "piena fedeltà", il che significa che ha restituito un token che tracciava la posizione di tutti gli spazi bianchi e i commenti - che chiamiamo "curiosità" - nel programma, nonché il token. Nel mio lexer un token era definito come:

  • Una serie di curiosità principali
  • Un tipo di token
  • Una larghezza token in caratteri
  • Una serie di curiosità finali

La curiosità è stata definita come:

  • Un tipo di curiosità: spazi bianchi, newline, commenti e così via
  • Una larghezza di curiosità nei personaggi

Quindi se avessimo qualcosa del genere

    foo + /* comment */
/* another comment */ bar;

che sarebbe lex quattro gettoni con tipi di token Identifier, Plus, Identifier, Semicolon, e larghezze 3, 1, 3, 1. Il primo identificatore è leader curiosità costituito da Whitespaceuna larghezza di 4 e finali curiosità Whitespacecon larghezza di 1. L'Plus ha curiosità leader e trivia finale composta da uno spazio bianco, un commento e una nuova riga. L'identificatore finale ha una curiosità principale di un commento e uno spazio e così via.

Con questo schema ogni personaggio nel file viene preso in considerazione nell'output del lexer, che è una proprietà utile da avere per cose come la colorazione della sintassi.

Ovviamente, se non hai bisogno delle curiosità, puoi semplicemente fare un token due cose: il tipo e la larghezza.

Potresti notare che il token e le curiosità contengono solo le loro larghezze, non la loro posizione assoluta nel codice sorgente. È deliberato. Tale schema presenta vantaggi:

  • È compatto in formato memoria e filo
  • Consente il re-lessing sulle modifiche; questo è utile se il lexer è in esecuzione all'interno di un IDE. Cioè, se rilevi una modifica in un token, esegui il backup del lexer su un paio di token prima della modifica e ricomincia a eseguire il lex fino a quando non sei sincronizzato con il flusso di token precedente. Quando si digita un carattere, la posizione di ogni token dopo quel carattere cambia, ma di solito solo uno o due token cambiano in larghezza, quindi è possibile riutilizzare tutto quello stato.
  • L'offset esatto dei caratteri di ogni token può essere facilmente derivato ripetendo il flusso di token e tenendo traccia dell'offset corrente. Una volta ottenuti gli offset di caratteri esatti, è facile estrarre il testo quando necessario.

Se non ti interessa nessuno di questi scenari, un token potrebbe essere rappresentato come un tipo e un offset, piuttosto che un tipo e una larghezza.

Ma la chiave da asporto qui è: la programmazione è l'arte di fare astrazioni utili . Stai manipolando i token, quindi fai un'astrazione utile sui token e poi puoi scegliere tu stesso quali dettagli di implementazione sono alla base.


3

Generalmente, si restituisce una piccola struttura che ha un numero che indica il token (o il valore enum per facilità d'uso) e un valore opzionale (stringa o eventualmente valore generico / modello). Un altro approccio sarebbe quello di restituire un tipo derivato per gli elementi che devono contenere dati extra. Entrambi sono leggermente disgustosi, ma abbastanza bene soluzioni a un problema pratico.


Cosa intendi con leggermente sgradevole ? Sono modi inefficienti per ottenere valori stringa?
Christian Dean,

@ Mr.Python - porteranno a molti controlli prima dell'uso nel codice, il che è inefficiente, ma il moreso rende il codice un po 'più complesso / fragile.
Telastyn,

Ho una domanda simile quando si progetta un lexer in C ++, potrei restituire a Token *o semplicemente a Token, o a TokenPtrche è un puntatore condiviso di Tokenclasse. Ma vedo anche qualche lexer che restituisce solo un TokenType e memorizza il valore di stringa o numero in altre variabili globali o statiche. Un'altra domanda è: come possiamo memorizzare le informazioni sulla posizione, devo avere una struttura token con campi TokenType, String e Location? Grazie.
ollydbg23,

@ ollydbg23 - nessuna di queste cose può funzionare. Vorrei usare uno struct. E per le lingue non di apprendimento utilizzerai comunque un generatore di parser.
Telastyn,

@Telastyn grazie per la risposta. Vuoi dire che una struttura Token potrebbe essere qualcosa del genere struct Token {TokenType id; std::string lexeme; int line; int column;}, giusto? Per una funzione pubblica di Lexer, ad esempio PeekToken(), la funzione potrebbe restituire un Token *o TokenPtr. Lo penso per un po ', se la funzione restituisce semplicemente TokenType, come fa il parser a cercare le altre informazioni sul token? Quindi, un puntatore come tipo di dati è preferito per il ritorno da tale funzione. Qualche commento sulla mia idea? Grazie
ollydbg23,
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.