L'analisi separata e i passaggi di lexing sono buone pratiche con i combinatori di parser?


18

Quando ho iniziato a usare i combinatori parser la mia prima reazione è stata un senso di liberazione da quella che sembrava una distinzione artificiale tra analisi e lexing. All'improvviso tutto stava solo analizzando!

Tuttavia, recentemente mi sono imbattuto in questo post su codereview.stackexchange che illustra qualcuno che ripristina questa distinzione. All'inizio ho pensato che fosse molto sciocco da parte loro, ma poi il fatto che in Parsec esistano funzioni per supportare questo comportamento mi porta a interrogarmi.

Quali sono i vantaggi / gli svantaggi dell'analisi su un flusso già lessato nei combinatori parser?


Per favore qualcuno potrebbe aggiungere il tag [parser-combinator]?
Eli Frey,

Risposte:


15

Sotto analisi comprendiamo molto spesso l'analisi dei linguaggi liberi dal contesto. Un linguaggio privo di contesto è più potente di un normale, quindi il parser può (molto spesso) fare immediatamente il lavoro dell'analizzatore lessicale.

Ma questo è a) abbastanza innaturale b) spesso inefficiente.

Per a), se penso a come ifappare un'espressione, penso che SE ESPR POI ESPR ELSE expr e non 'i' 'f', forse alcuni spazi, allora qualsiasi carattere con cui un'espressione può iniziare, ecc. Ottieni idea.

Per b) esistono potenti strumenti che fanno un ottimo lavoro riconoscendo entità lessicali, come identificatori, valori letterali, parentesi di ogni tipo, ecc. Faranno il loro lavoro praticamente in pochissimo tempo e ti daranno una bella interfaccia: un elenco di token. Non preoccuparti più di saltare gli spazi nel parser, il tuo parser sarà molto più astratto quando si tratta di token e non di personaggi.

Dopotutto, se pensi che un parser dovrebbe essere impegnato con cose di basso livello, perché quindi elaborare i caratteri? Si potrebbe scrivere anche a livello di bit! Vedete, un tale parser che funziona a livello di bit sarebbe quasi incomprensibile. È lo stesso con personaggi e gettoni.

Solo i miei 2 centesimi.


3
Solo per motivi di precisione: un parser può sempre fare il lavoro di un analizzatore lessicale.
Giorgio,

Inoltre, per quanto riguarda l'efficienza: non sono sicuro che un parser sarebbe meno efficiente (più lento). Mi aspetterei che la grammatica risultante conterrebbe una sotto-grammatica che descrive un linguaggio regolare, e il codice per quella sotto-grammatica sarebbe veloce come un analizzatore lessicale corrispondente. IMO il vero punto è (a): quanto sia naturale, intuitivo lavorare con un parser più semplice, più astratto.
Giorgio,

@Giorgio - Per quanto riguarda il tuo primo commento: hai ragione. Quello che avevo in mente qui sono casi in cui il lexer pragmaticamente fa qualche lavoro che semplifica la grammatica, in modo che si possa usare LALR (1) invece di LALR (2).
Ingo,

2
Ho rimosso la mia accettazione della tua risposta dopo ulteriori sperimentazioni e riflessioni. Sembra che voi due veniate dal vostro mondo di Antlr e tutti. Considerando la natura di prima classe dei combinatori di parser, spesso finisco semplicemente per definire un parser wrapper per i miei parser token lasciando ogni token come un singolo nome nello strato di analisi dei parser. per esempio il tuo esempio if sarebbe simile if = string "if" >> expr >> string "then" >> expr >> string "else" >> expr.
Eli Frey,

1
Le prestazioni sono ancora una domanda aperta, farò alcuni benchmark.
Eli Frey,

8

Tutti suggeriscono che separare il lex e il parsing sia una "buona pratica" - devo essere in disaccordo - in molti casi eseguire il lex e il parsing in un singolo passaggio dà molta più potenza e le implicazioni sulle prestazioni non sono così negative come sono presentate nel altre risposte (vedi Packrat ).

Questo approccio brilla quando si devono mescolare un numero di lingue diverse in un singolo flusso di input. Ciò non è necessario solo per gli strani linguaggi orientati alla metaprogrammazione come Katahdin e simili , ma anche per applicazioni molto più tradizionali, come la programmazione alfabetica (mescolando lattice e, diciamo, C ++), usando HTML nei commenti, inserendo Javascript in HTML e presto.


Nella mia risposta ho suggerito che si tratta di una "buona pratica in determinati contesti" e non che sia una "pratica migliore in tutti i contesti".
Giorgio

5

Un analizzatore lessicale riconosce un linguaggio regolare e un parser riconosce un linguaggio privo di contesto. Poiché ogni linguaggio regolare è anche privo di contesto (può essere definito da una cosiddetta grammatica lineare destra ), un parser può anche riconoscere un linguaggio regolare e la distinzione tra parser e analizzatore lessicale sembra aggiungere una complessità non necessaria: un singolo contesto grammatica libera (parser) potrebbe fare il lavoro di un parser e di un analizzatore lessicale.

D'altra parte, può essere utile catturare alcuni elementi di un linguaggio privo di contesto attraverso un linguaggio regolare (e quindi un analizzatore lessicale) perché

  1. Spesso questi elementi appaiono così spesso che possono essere trattati in modo standard: riconoscere letterali di numeri e stringhe, parole chiave, identificatori, saltare gli spazi bianchi e così via.
  2. La definizione di un linguaggio regolare di token rende più semplice la grammatica risultante senza contesto, ad esempio si può ragionare in termini di identificatori, non in termini di singoli caratteri, oppure si può ignorare completamente lo spazio bianco se non è rilevante per quel particolare linguaggio.

Quindi separare l'analisi dall'analisi lessicale ha il vantaggio di poter lavorare con una grammatica più semplice senza contesto e incapsulare alcune attività di base (spesso di routine) nell'analizzatore lessicale (divide et impera).

MODIFICARE

Non ho familiarità con i combinatori di parser, quindi non sono sicuro di come le considerazioni precedenti si applichino in quel contesto. La mia impressione è che anche se con i combinatori di parser si ha solo una grammatica senza contesto, distinguere tra due livelli (analisi lessicale / analisi) potrebbe aiutare a rendere questa grammatica più modulare. Come detto, il livello inferiore di analisi lessicale potrebbe contenere parser riutilizzabili di base per identificatori, valori letterali e così via.


2
I Lexem rientrano nelle grammatiche regolari non naturalmente, ma per convenzione, poiché tutti i lexer sono costruiti su motori di espressione regolari. Limita il potere espressivo delle lingue che puoi progettare.
SK-logic,

1
Puoi dare un esempio di una lingua per la quale sarebbe appropriato definire i lessemi che non possono essere descritti come una lingua normale?
Giorgio,

1
ad esempio, in un paio di linguaggi specifici del dominio che ho creato, gli identificatori avrebbero potuto essere espressioni TeX, che hanno semplificato la stampa graziosa del codice, ad esempio un'espressione simile \alpha'_1 (K_0, \vec{T}), dove \ alpha'_1, K_0 e \ vec {T} sono identificatori.
Logica SK

1
Data una grammatica senza contesto, puoi sempre prendere una N non terminale e trattare le parole che può derivare come unità che hanno in se stesse un significato utile (ad esempio, un'espressione, un termine, un numero, un'istruzione). Questo può essere fatto indipendentemente da come si analizza quell'unità (parser, parser + lexer, ecc.). IMO la scelta di un parser + lexer è più tecnica (come implementare l'analisi) che semantica (qual è il significato dei blocchi di codice sorgente che analizzi). Forse sto trascurando qualcosa ma i due aspetti mi sembrano ortogonali.
Giorgio,

3
Quindi, sono d'accordo con te: se definisci alcuni elementari elementari di base ( lexemi ) e vuoi usare un analizzatore lessicale per riconoscerli, questo non è sempre possibile. Mi chiedo solo se questo è l'obiettivo di un lexer. Per quanto ho capito, l'obiettivo di un analizzatore lessicale è più tecnico: rimuovere dal parser alcuni dettagli di implementazione di basso livello e noiosi.
Giorgio

3

Semplicemente, il lessico e l'analisi dovrebbero essere separati perché sono diverse complessità. Il Lexing è un DFA (automa finito deterministico) e un parser è un PDA (automa push-down). Ciò significa che l'analisi intrinsecamente consuma più risorse del lexing e ci sono tecniche di ottimizzazione specifiche disponibili solo per i DFA. Inoltre, scrivere una macchina a stati finiti è molto meno complesso ed è più facile da automatizzare.

Stai sprecando usando un algoritmo di analisi per Lex.


Se usi un parser per fare analisi lessicale, il PDA non userebbe mai lo stack, funzionerebbe sostanzialmente come un DFA: consuma solo input e passa da uno stato all'altro. Non sono sicuro al 100%, ma penso che le tecniche di ottimizzazione (riduzione del numero di stati) che possono essere applicate a un DFA possano essere applicate anche a un PDA. Ma sì: è più facile scrivere l'analizzatore lessicale come tale senza usare uno strumento più potente, e quindi scrivere un parser più semplice su di esso.
Giorgio,

Inoltre, rende il tutto più flessibile e manutenibile. Ad esempio, supponiamo di avere un parser per il linguaggio Haskell senza la regola di layout (ovvero, con punti e virgola e parentesi graffe). Se abbiamo un lexer separato, ora potremmo aggiungere le regole di layout semplicemente facendo un altro passaggio sui token, aggiungendo parentesi graffe e punti e virgola, se necessario. O, per un esempio più semplice: supponiamo che abbiamo iniziato con una lingua che supporta i caratteri ASCII solo negli identificatori e ora vogliamo supportare le lettere unicode negli identificatori.
Ingo,

1
@Ingo, e perché dovresti farlo in un lexer separato? Basta scomporre quei terminali.
SK-logic

1
@ SK-logic: non sono sicuro di aver capito la tua domanda. Perché un lexer separato possa essere una buona scelta ho provato a dimostrare nel mio post.
Ingo

Giorgio, no. Lo stack è un componente cruciale di un normale parser in stile LALR. Fare il lessing con un parser è un orribile spreco di memoria (sia memoria statica che allocata dinamicamente) e sarà molto più lenta. Il modello Lexer / Parser è efficiente
usalo

1

Uno dei principali vantaggi dell'analisi separata / lex è la rappresentazione intermedia: il flusso di token. Questo può essere elaborato in vari modi che altrimenti non sarebbero possibili con un lex / parse combinato.

Detto questo, ho scoperto che un buon decorso ricorsivo può essere meno complicato e più facile da lavorare rispetto all'apprendimento di un generatore di parser, e dover capire come esprimere la debolezza della grammatica all'interno delle regole del generatore di parser.


Potresti spiegare di più sulle grammatiche che sono più facilmente espresse su un flusso prefabbed e poi eseguite al momento dell'analisi? Ho solo esperienza nell'implementazione di linguaggi giocattolo e pochi formati di dati, quindi forse mi sono perso qualcosa. Hai notato delle caratteristiche prestazionali tra il tuo parser RD / lex combo laminati a mano e i generatori alimentati da BNF (presumo)?
Eli Frey,
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.