λ Teoria del calcolo

Grammatiche libere dal contesto: derivazioni e ambiguità

Che cosa sono le grammatiche libere dal contesto, come si scrivono le regole, come leggere derivazioni e alberi sintattici e come si elimina l'ambiguità.

Gli automi a stati finiti non sanno contare in modo illimitato, e quindi non riescono a descrivere strutture annidate come parentesi bilanciate, espressioni aritmetiche o blocchi di codice dentro altri blocchi. Per queste strutture serve un formalismo più espressivo: le grammatiche libere dal contesto (context-free grammars, CFG). Sono il fondamento della sintassi di quasi tutti i linguaggi di programmazione e di molti formati di dati.

La definizione

Una grammatica libera dal contesto è una quadrupla G = (V, Σ, R, S) dove:

  • V è un insieme finito di variabili (o non terminali), che rappresentano categorie sintattiche;
  • Σ è l'alfabeto dei terminali, i simboli che compaiono davvero nelle stringhe;
  • R è un insieme finito di regole della forma A → α, dove A è una variabile e α è una stringa qualsiasi di variabili e terminali (anche vuota);
  • S ∈ V è la variabile iniziale.

«Libera dal contesto» significa che a sinistra di ogni regola c'è una sola variabile, senza simboli intorno: la variabile A si può riscrivere con α ovunque compaia, indipendentemente da ciò che le sta accanto.

Derivazioni

Si parte dalla variabile iniziale e si applicano le regole, sostituendo una variabile alla volta, fino a ottenere una stringa di soli terminali. Questa sequenza è una derivazione. Il linguaggio della grammatica, L(G), è l'insieme di tutte le stringhe di terminali derivabili da S.

Un esempio minimo: la grammatica con la sola variabile S e le regole

S → a S b
S → ε

genera esattamente {aⁿbⁿ : n ≥ 0}. La derivazione di aabb è S ⇒ aSb ⇒ aaSbb ⇒ aabb. Il linguaggio che nessun DFA può riconoscere si descrive con due regole.

Parentesi bilanciate

Le stringhe di parentesi bilanciate sono generate da:

S → ( S ) S
S → ε

La prima regola dice: una sequenza bilanciata è vuota, oppure è una coppia di parentesi che racchiude una sequenza bilanciata, seguita da un'altra sequenza bilanciata. È una definizione ricorsiva tradotta direttamente in regole.

Alberi sintattici

Una derivazione si può rappresentare con un albero sintattico (parse tree): la radice è S, ogni nodo interno è una variabile e i suoi figli sono i simboli del lato destro della regola applicata, le foglie lette da sinistra a destra formano la stringa.

L'albero dimentica l'ordine in cui le variabili sono state sostituite e conserva solo la struttura. Per questo si distinguono la derivazione sinistra, in cui si sostituisce sempre la variabile più a sinistra, e la derivazione destra: a ogni albero corrisponde una e una sola derivazione sinistra.

Nei compilatori l'albero è ciò che conta davvero: è da lì che si ricava il significato del programma.

Ambiguità

Una grammatica è ambigua se esiste almeno una stringa con due alberi sintattici diversi. Il caso classico sono le espressioni aritmetiche:

E → E + E | E * E | ( E ) | n

La stringa n + n * n ha due alberi: uno in cui la somma viene prima, (n + n) * n, e uno in cui viene prima il prodotto, n + (n * n). Se l'albero determina il valore calcolato, l'ambiguità produce due risultati diversi.

Eliminare l'ambiguità con livelli di precedenza

La soluzione standard introduce una variabile per ogni livello di precedenza:

E → E + T | T
T → T * F | F
F → ( E ) | n

Ora un'espressione è una somma di termini, un termine è un prodotto di fattori, un fattore è un numero o un'espressione fra parentesi. La stringa n + n * n ha un solo albero, quello in cui il prodotto è raggruppato dentro il termine. La ricorsione a sinistra in E → E + T rende inoltre la somma associativa a sinistra: n + n + n si legge (n + n) + n.

Bisogna sapere un fatto importante: esistono linguaggi intrinsecamente ambigui, per i quali ogni grammatica possibile è ambigua. Inoltre stabilire se una CFG qualunque è ambigua è un problema indecidibile: non esiste un algoritmo generale che risponda sempre.

Forme normali

Per le dimostrazioni e per alcuni algoritmi conviene portare la grammatica in forma standard. La più usata è la forma normale di Chomsky, in cui ogni regola è del tipo A → BC oppure A → a (con un'eccezione per la stringa vuota). Ogni CFG si può convertire in questa forma senza cambiare il linguaggio. Il vantaggio è che ogni albero diventa binario, e questo rende possibili algoritmi come CYK, che decide in tempo cubico se una stringa appartiene al linguaggio.

Dove si incontrano nel lavoro reale

Nel mio lavoro le grammatiche emergono ogni volta che si scrive un parser per un formato di configurazione, un piccolo linguaggio di interrogazione o un validatore di espressioni inserite dagli utenti. Partire da una grammatica esplicita, invece che da una serie di condizioni scritte a mano, rende il codice più corto, più verificabile e molto meno fragile davanti ai casi limite.

In sintesi

  • Una CFG ha regole con una sola variabile a sinistra e descrive strutture annidate che i DFA non gestiscono.
  • Il linguaggio è l'insieme delle stringhe di terminali derivabili dalla variabile iniziale.
  • L'albero sintattico rappresenta la struttura e corrisponde a una sola derivazione sinistra.
  • Una grammatica è ambigua se una stringa ha due alberi; i livelli di precedenza risolvono il caso delle espressioni.
  • Decidere se una CFG è ambigua è indecidibile, e alcuni linguaggi sono ambigui per natura.
MM
Marco Marzaduri

Marco Marzaduri si occupa di cybersecurity, informatica forense e sviluppo web. Laureato in informatica, unisce il rigore dell'analisi tecnica all'attenzione per le persone che usano la tecnologia.

Leggi anche