Molto probabilmente, l'implementazione della grammatica come più parser interdipendenti renderà il tuo codice più complicato. Il flusso di dati diventerà meno ovvio e duplicherà qualche comportamento. Va bene se una classe è grande.
Tuttavia, molte lingue possono essere facilmente suddivise in diversi livelli e gestirne separatamente potrebbe essere ragionevole. Ad esempio:
- potresti estrarre la tokenizzazione dal parser principale. C ha una fase separata di tokenizzazione e preprocessore.
- Potresti eseguire alcune operazioni di post-elaborazione in una fase separata che costruisce l'AST finale. Questo è particolarmente sensibile se il parser controlla anche la semantica, ad es. risoluzione delle definizioni dei simboli o esecuzione di controlli di tipo. Questi dovrebbero essere separati dall'analisi.
- Se la tua lingua ha una strong dictomia di espressione di istruzioni, potresti avere parser separati per ciascuno, con il parser dell'istruzione che richiama il parser di espressioni come necessario. Markdown è un esempio di una lingua con una grammatica basata sulla linea (indentazione) su una grammatica a livello di blocco (paragrafi, titoli, elenchi) su una grammatica in linea (enfasi, collegamenti). Alcuni parser utilizzano un semplice approccio di discesa ricorsiva per la sintassi del livello di istruzione, come i costrutti del flusso di controllo o le definizioni di livello superiore, ma passano a un algoritmo LR per le espressioni per gestire correttamente la precedenza e l'associatività.
Ho trovato piuttosto vantaggioso estrapolare operazioni di analisi di basso livello in una classe separata: gestire il buffer di input, controllare lookaheads, estrarre token, gestire errori, è meglio farlo da una classe personalizzata piuttosto che fare affidamento sul strutture fornite dalla lingua (in particolare, std::istream
non è adatto alla maggior parte dei problemi). Se stai utilizzando un algoritmo di analisi diverso da Discorsione ricorsiva, dovresti anche gestire queste operazioni in una classe separata.