INTRODUZIONE

La simulazione numerica di sistemi fisici su supercalcolatori interessa oramai molti campi della fisica e, più in generale, della scienza e ingegneria.

Il ricorso a tale metodo di indagine scientifica trae origine da due distinte cause: la prima è la grande evoluzione che la tecnologia ha avuto nello sviluppo di un’elettronica sempre più rapida; grazie ad essa applicazioni che una volta sarebbero state improponibili a causa della loro elevata richiesta di tempo (a volte decine di anni), potevano essere riproposte all’attenzione magari solo dopo appena un lustro.

L’altra causa è la sempre maggiore difficoltà incontrata nella risoluzione con i tradizionali metodi analitici dei nuovi problemi che, via via sempre più complessi, si pongono alla ricerca scientifica. Lo sfruttamento intensivo di tutti gli strumenti che l’analisi mette a disposizione, la creazione di nuovi strumenti matematici introdotti talvolta a costo di una perdita di rigore matematico, e comunque anticipando quello che è il naturale corso del pensiero matematico, spesso non sono sufficienti a garantire grandi passi in avanti nel conseguimento di nuovi risultati scientifici.

In alcuni casi il motivo trainante verso la simulazione numerica, è stato l’inapplicabilità del tradizionale metodo matematico usato in fisica, detto teoria delle perturbazioni. Come infatti nel caso della teoria di campo QCD (Quantum Chromo Dynamics), poiché ci si accorse che la costante di accoppiamento della teoria non è sufficientemente piccola da consentire l’applicazione del consueto metodo della teoria delle perturbazioni.

Sicché lo sviluppo di simulazioni numeriche su calcolatore venne visto in origine come un metodo alternativo ai più tradizionali sistemi in uso nella fisica, laddove essi fallivano. Oggi invece, la mia impressione è che l’interesse verso simulazioni numeriche su calcolatore venga visto non più come extrema ratio di fronte all’impotenza dell’analisi, bensì come ulteriore mezzo di indagine scientifica verso l’acquisizione di nuove conoscenze.

Il progetto APE100 per la realizzazione di supercalcolatori, nasce sotto la spinta delle esigenze sentite giustappunto dalla fisica teorica nello sviluppo della teoria QCD, la teoria di campo candidata a descrivere le interazioni forti fra le particelle costituenti barioni e mesoni, i quarks. E possiamo considerare questo un caso in cui la scienza pura, impaziente, sollecita lo sviluppo tecnologico ai propri fini.

La fisica teorica, essendone l’ispiratrice, caratterizza il progetto dei calcolatori APE100; interviene attivamente nella definizione delle caratteristiche costruttive. Le peculiarità di questi calcolatori, quali il parallelismo, dimostrano senz’altro il loro orientamento verso il calcolo scientifico.

Insieme allo sviluppo dell’hardware, si rende necessario uno sviluppo coerente di software, in modo tale che la fisica teorica possa sfruttare al meglio le caratteristiche messe a sua disposizione dai calcolatori APE100.

Il mio lavoro di tesi si inserisce proprio in quest’ambito: contribuire allo realizzazione di un linguaggio di programmazione, l’APESE, orientato allo sviluppo dei problemi della fisica teorica, in modo che essi abbiano in APE100 una soluzione il più efficiente possibile.

Fondamentale al conseguimento di questo risultato è stato l’utilizzo di un nuovo concetto della teoria dei linguaggi formali, dovuto proprio al gruppo APE: quello di “linguaggio evolvente”. L’APESE deve gran parte delle sue caratteristiche al suo attributo di evolvenza.

L’utilizzo di questi nuovi strumenti della teoria dei linguaggi formali, i linguaggi evolventi e i parsers dinamici, mi sembra fervido di nuovi risultati anche in campi diversi da quello strettamente dello sviluppo di linguaggi di programmazione per la fisica teorica.

Il mio compito in particolare è stata la scrittura delle librerie del linguaggio orientate alla risoluzione dei problemi di simulazione numerica della teoria di gauge QCD su reticolo, oltre comunque a garantire il mio contributo nello sviluppo di altri settori del linguaggio e del compilatore (quali la realizzazione delle strutture subroutines e functions o la scrittura in C delle routines semantiche che consentono la traduzione del linguaggio APESE verso l’ASSEMBLER).

Occorre però precisare che un lavoro imponente quale la teorizzazione di un nuovo linguaggio dalle caratteristiche senz’altro innovative, e la successiva realizzazione del compilatore che interpreti tale linguaggio, non può che avvenire in equipe. Per cui il mio lavoro si è sviluppato coerentemente con quello del mio relatore P.S. Paolucci, cui va tutta la mia gratitudine per il suo costante impegno nel seguirmi da vicino e per tutto ciò che da lui ho imparato; di G.M. Todesco, pazientissimo nel fornirmi aiuto e spiegazioni riguardo il funzionamento del parser dinamico ZzParser, il parser del compilatore.

Ringraziamenti vanno a G. Martinelli, per le indicazioni e i consigli offertimi durante lo sviluppo del mio lavoro, così come a R. Sarno; infine all’intero gruppo APE per l’accoglienza e la disponibilità che mi hanno dimostrato.

CAPITOLO 1 LA TEORIA DI GAUGE QCD SU RETICOLO

1.1 INTRODUZIONE

La teoria di campo QCD è la teoria candidata a descrivere le interazioni forti. Essa è il risultato della ricerca sia teorica che sperimentale condotta gli ultimi decenni nel campo della fisica delle particelle elmentari.

La QCD nasce come sviluppo del modello a partoni e sull’onda del successo della teoria di gauge elettrodebole che ha imposto la simmetria di gauge quale principio unificante nella teoria delle interazioni fondamentali.

La QCD prevede l’esistenza di particelle elementari fermioniche a spin 1/2 e carica elettrica frazionaria, dette quark, finora mai osservate libere ma da identificarsi coi costituenti elementari dei nucleoni, in origine detti partoni, dei quali già da tempo si avevano evidenze sperimentali; il cosiddetto modello a partoni era infatti in grado di spiegare alcuni risultati sperimentali quali lo scaling di Bjorken nei processi di scattering profondamente anelastico.

Successive verifiche indirette di consistenza all’identificazione partoni-quark vennero dalla misura della carica elettrica dei componenti elementari degli adroni (che risultò effettivamente frazionaria) e dalla scoperta dell’ulteriore grado di libertà di cui tali costituenti elementari sarebbero in possesso, e che la QCD chiama colore.

La lagrangiana della QCD è invariante sotto trasformazioni locali relative al gruppo di simmetria SU(3) di colore:

L (x) = ∑_{n_{f} =1}^{6} [i\overline{ψ}_{i} (x) γ_{μ} (δ_{ij}∂^{μ} − ig (T_{a})_{ij} A_{a}^{μ} (x)) ψ_{j} (x) − m\overline{ψ}_{i} (x) ψ_{i} (x)] +

− \frac{1}{4} tr[F^{μν}(x)F_{μν}(x)]  (1.1.1)

dove:

T_{a} = \frac{1}{2}λ_{a}

F^{μν} (x) = F_{a}^{μν} (x) T_{a}

F_{a}^{μν} (x) = ∂^{μ}A_{a}^{ν} (x) − ∂^{ν}A_{a}^{μ} (x) − gf_{abc}A_{b}^{μ} (x) A_{c}^{ν} (x)

le trasformazioni locali di fase SU(3) sono:

\begin{matrix}ψ_{i} (x) → ψ′_{i} (x) = V_{ij} (x) ψ_{j} (x) \\ \overline{ψ}_{i} (x) → \overline{ψ}′_{i} (x) = \overline{ψ}_{j} (x) V_{ji}^{†} (x) \\ A^{μ} (x) → A^{′μ} (x) = V (x) A^{μ} (x) V^{†} (x) − \frac{i}{g} V (x) ∂^{μ}V^{†} (x) \\ F^{μν} (x) → F^{′μν} (x) = V (x) F^{μν}V^{†} (x) \end{matrix}  (1.1.2)

in cui V_{ij}(x) = e^{−iα_{a}(x)(T_{a})_{ij}} è una matrice appartenente al gruppo SU(3).

Per ottenere che la lagrangiana della teoria sia invariante sotto le leggi di trasformazione appena mostrate è stato necessario introdurre entro la (1.1.1) i campi di gauge A_{a}^{μ}(x), a = 1, …, 8, relativi ad 8 particelle bosoniche, dette gluoni, svolgenti il ruolo di mediatori dell’interazione; un ruolo analogo a quello svolto dal fotone nella QED, dove la simmetria è però la U(1).

Le prime stime del valore della costante di accopiamento g della teoria e lo studio del suo andamento al variare della scala delle energie rivelarono due diversi comportamenti della teoria: il confinamento dei quark nei processi a piccolo momento trasferito i quali sono in relazione ad una grande costante di accoppiamento; la libertà asintotica dei quark nei processi a grande momento trasferito, corrispondentemente ad una costante di accoppiamento piccola.

Nel caso in cui la costante d’accoppiamento è sufficientemente piccola, ossia per i processi a grande momento trasferito (Deep Inelastic Scattering, processi di Drell-Yan, etc.), è possibile calcolare le grandezze fisiche di interesse mediante l’uso della teoria delle perturbazioni. In questa regione di energie si sono avute brillanti conferme della QCD; ad esempio con la previsione delle piccole violazioni dello scaling di Bjorken osservate sperimentalmente e in accordo con l’andamento previsto dall’equazione di Altarelli-Parisi la cui derivazione è stata fatta nell’ambito di una teoria QCD perturbativa.

Nella regione delle basse energie, a causa dell’alto valore della costante di accoppiamento lo studio della teoria non può avvenire con metodi perturbativi. Sicché se vogliamo ad esempio sapere qualcosa in piú sugli stati legati dei quark, dovremo usare tecniche non perturbative in grado di fornire risultati attendibili per questa scala di energie.

Una possibile tecnica non perturbativa di studio è la simulazione numerica della QCD sul reticolo. Ma per proseguire in questa direzione è necessario disporre di calcolatori dalle elevate prestazioni.

I primi risultati ai quali si è giunti grazie a tali metodi sono relativi allo spettro di massa adronico; la coincidenza entro gli errori fra il valore ottenuto sul reticolo per le masse degli adroni e il loro valore reale misurato sperimentalmente, è una necessaria conferma della validità di questi metodi, oltre che della teoria QCD stessa.

L’interesse può quindi volgersi ad un affinamento delle tecniche di calcolo verso l’obbiettivo della previsione dei valori di osservabili fisiche ancora non noti sperimentalmente. In modo che la teoria diventi realmente efficace anche a questa scala di energie.

Il progetto APE100 nasce proprio con l’intento di sviluppare e fornire strumenti (hardware e software) utili alla risoluzione di tali problemi.

In questo capitolo saranno descritte le caratteristiche essenziali delle teorie di gauge sul reticolo; in particolare ci si riferirà alla teoria QCD.

Saranno poi esaminate le tecniche con cui la teoria può essere simulata su un calcolatore. Si mostreranno gli algoritmi attraverso i quali sono possibili previsioni per gli osservabili della teoria. In particolare, nell’esame di tali algoritmi, si presterà particolare attenzione ai punti critici che mettono in crisi i sistemi tradizionali di elaborazione.

Si passerà quindi a proporre le soluzioni scelte per la struttura hardware di APE100 in risposta a tali punti critici.

Infine verrà delineata a grandi linee la forma di un linguaggio di programmazione specificatamente studiato per problemi di simulazione della QCD sul reticolo.

La prima richiesta per un tale linguaggio dovrà essere quella di consentire traduzioni degli algoritmi tipici della teoria nell’utilizzo più corretto delle risorse e delle caratteristiche di APE100. Sarà opportuno che l’utilizzatore del linguaggio avverta il meno possibile le caratteristiche e la struttura hardware della macchina che sta usando. Dovrà essere il compilatore del linguaggio a preoccuparsi di tradurre gli algoritmi tipici della teoria nel codice più ottimizzato possibile, sollevando il programmatore da tale compito.

La seconda richiesta per il linguaggio dovrà essere quella di una integrazione nell’ambiente della teoria per cui è stato studiato. Un buon linguaggio di programmazione infatti, è tale per essere il più vicino possibile al modo di ragionare e alla mente umana che si serve di esso; e d’altra parte deve essere lontano dalla impostazione caratteristica della macchina. Per cui troveremo che l’ambiente e le strutture tipiche delle teorie di campo sul reticolo, avranno un corrispettivo nel linguaggio. Cosicché il programmatore troverà del tutto naturale affrontare i problemi che si è posto nei termini offerti dal linguaggio.

1.2 TEORIE DI CAMPO SUL RETICOLO

La formulazione sul reticolo delle teorie di campo, consiste nella discretizzazione dello spazio-tempo; il continuo fisico, reale, quadridimensionale, viene trasformato in un reticolo quadridimensionale, di passo a arbitrario. Un evento spazio-temporale sul reticolo, invece di essere associato ad un punto x_{μ} appartenente al continuo R^{4}, corrisponde ad un sito del reticolo x_{μ} = an_{μ} con n_{μ} ∈ I, μ = 0, 1, 2, 3.

La trasformata di Fourier di una qualsiasi funzione del punto x dello spazio

f̃(k) = ∫ dx e^{−ikx}f(x)  (1.2.1)

sul reticolo assume periodicità per il valore 2π/a, come si può mostrare:

f̃(k) = ∑_{n} ae^{−ikx}f(x_{n}),  x_{n} = an  (1.2.2)

f̃(k) = f̃(k + \frac{2π}{a} )  (1.2.3)

Per cui, considerato che nello spazio dei momenti le funzioni sono invarianti per traslazioni di 2π/a, potremo limitarci a considerare solo impulsi compresi all’interno di un intervallo ampio 2π/a. Infatti il comportamento della funzione nell’intervallo prescelto è simile a quello degli intervalli adiacenti. Ad esempio potremo limitare l’impulso all’interno dell’intervallo, detto prima zona di Brillouin, definito:

k ∈ [−\frac{π}{a} , +\frac{π}{a} )  (1.2.4)

come anche in un’altra qualsiasi zona di Brillouin.

La teoria quindi possiede negli estremi dell’intervallo un cut-off naturale che taglia via tutti i valori dell’impulso al di fuori, ed ha come effetto l’eliminazione delle divergenze ultraviolette; si realizza così in un facile modo la regolarizzazione della teoria.

La trattazione in formalismo funzionale o di Feynman di teorie di campo è più agevole se si assume una regolarizzazione reticolare; infatti, i ben noti problemi delle maldefinizioni matematiche del formalismo in cui spesso ci si imbatte nel continuo, sul reticolo vengono agevolmente superati.

Mostriamo gli aspetti principali della formulazione di Feynman, sottolineando i punti che risultano in una forma migliore grazie ad una trattazione sul reticolo. Nel formalismo funzionale di Feynman il ruolo principale è svolto dal funzionale generatore il quale, genericamente, in uno spazio-tempo Euclideo assume la forma:

Z[J] = c ∫ DΦ e^{−S_{E}(Φ)}e^{∫ d^{4}x J(x)Φ(x)}  (1.2.5)

dove Φ è un campo scalare, fermionico o vettoriale, o anche un insieme di campi dalle diverse proprietà, a seconda della teoria che intendiamo trattare; S_{E} è l’azione nella formulazione euclidea e c è una costante di normalizzazione che vale:

c = \frac{1}{∫ DΦ e^{−S_{E}(Φ)}}  (1.2.6)

Tutta la fisica del sistema risiede però nelle funzioni di Schwinger della teoria le quali sono ottenute dall’integrale funzionale Z per derivazioni funzionali rispetto alla sorgente J.

Ad esempio l’ampiezza di probabilità vuoto-vuoto o prima funzione di Green è data da:

Z[J]|_{J≡0} = c ∫ DΦ e^{−S_{E}(Φ)} = 1  (1.2.7)

e la funzione di Green a due linee o propagatore:

\begin{aligned}⟨0 |T(Φ(x_{1})Φ(x_{2}))| 0⟩ & = \frac{∂^{2}Z[J]}{∂J(x_{1})∂J(x_{2})}|_{J≡0} \\ & = c ∫ DΦ e^{−S_{E}(Φ)} \frac{∂}{∂J(x_{1})} \frac{∂}{∂J(x_{2})}e^{∫ d^{4}x J(x)Φ(x)}|_{J≡0} \end{aligned}  (1.2.8)

Tutti i passaggi mostrati nell’espressione precedente sono stati svolti secondo le regole definite per la derivazione funzionale.

Naturalmente si ha la necessità di precisare meglio i concetti di derivazione funzionale e di integrazione funzionale alla base, come abbiamo ora visto, di una formalizzazione alla Feynman delle teorie di campo. Col simbolo:

∫ DΦ  (1.2.9)

indichiamo una misura in uno spazio infinito dimensionale; le variabili di integrazione su cui eseguiamo l’integrale sono tutte le Φ al variare di x ∈ R. Sul reticolo il simbolo ∫ DΦ è ben definito poiché sta per:

∫ ∏_{n} dΦ_{n},  Φ_{n} = Φ (x_{n}) = Φ(na)  (1.2.10)

che nel caso di reticoli finiti ricade nella definizione ordinaria di integrale.

Il motivo per cui intendiamo trattare la teoria dei campi secondo il formalismo di Feynman sta nel fatto che, per questa nuova veste, possiamo utilizzare i metodi della meccanica statistica. E’ possibile infatti dimostrare l’equivalenza fra il funzionale generatore di una teoria di campo e la funzione di partizione definita in meccanica statistica; fra le funzioni di Green e le funzioni di correlazione.

In quanto segue si darà per scontato che i reticoli scelti avranno una spaziatura uniforme lungo tutte le direzioni (il passo fra un sito e l’altro del reticolo avrà valore a in tutte le direzioni), ma verrà lasciata arbitraria la scelta della grandezza dei reticoli (in genere in simulazioni su calcolatore si utilizzano reticoli più lunghi nella direzione temporale).

Si lavorerà esclusivamente nell’Euclideo, perché si vorrà trattare il tempo allo stesso modo delle altre direzioni spaziali e perché la formulazione euclidea è preferibile nel formalismo funzionale.

Per qualsiasi teoria si sviluppi sul reticolo sarà fondamentale il test del limite al continuo a → 0; in ogni caso in tale limite dovrà essere ripristinata la teoria originaria.

1.3 TEORIE DI PURA GAUGE SUL RETICOLO

Nella formulazione sul reticolo di teorie di campo è desiderabile vengano mantenute le simmetrie che la teoria possiede nel continuo. Tra queste merita particolare riguardo la simmetria di gauge, visto il ruolo che essa gioca nelle odierne teorie delle interazioni fondamentali, quali ad esempio la QCD.

In generale noi vogliamo che l’azione reticolare di una teoria di pura gauge obbedisca alle seguenti prescrizioni:

- località.

- invarianza sotto le trasformazioni di gauge.

- l’azione di Yang-Mills quale limite al continuo. Naturalmente prima dovremo trovare la forma corretta assunta dalle trasformazioni di gauge su un reticolo.

Esistono diverse azioni per cui valgano le proprietà ora specificate. Noi seguiremo la formulazione dovuta a Wilson (Wilson 1974).

Se la teoria che vogliamo descrivere sul reticolo è una teoria di gauge SU(n) (invariante sotto trasformazioni di gauge SU(n) ), i campi di gauge saranno rappresentati da matrici U_{μ}(x_{n}), appartenenti al gruppo continuo SU(n); complessivamente quattro matrici (μ = 0, 1, 2, 3) per ogni sito del reticolo. Nella formulazione sul reticolo della teoria i campi di gauge sono descritti dagli elementi del gruppo SU(n) (nel caso della QCD SU(3)) piuttosto che dai corrispondenti elementi A_{μ}(x_{n}) dell’Algebra di Lie associata, più comuni nelle teorie al continuo. Le U_{μ}(x_{n}) sono legate agli A_{μ}(x_{n}) tramite la relazione:

U^{μ} (x_{n}) = e^{igaA^{μ}(x_{n})} = e^{igaA_{a}^{μ}(x_{n})T_{a}}  (1.3.1)

figura 1.1. Links e siti

ove gli A_{a}^{μ}(x_{n}) sono i campi associati ai bosoni vettori (in QCD i gluoni mediatori dell’interazione forte) e le T_{a} sono i generatori dell’Algebra di Lie associata al gruppo SU(n) (le matrici di Pauli nel caso SU(2), le matrici di Gellman nel caso SU(3)).

Usualmente, secondo un’idea di Wegner, i campi U_{μ}(x_{n}) vengono pensati come associati ai links piuttosto che ai siti del reticolo (sono cioè associati ai trattini di unione di lunghezza a che congiungono i siti del reticolo).

Altresì nelle teorie di campo sul reticolo, è frequente incontrare campi di gauge U_{−μ}(x_{n}), associati al link lungo la direzione μ inversa, definiti secondo la:

U_{−μ} (x_{n+μ}) = U_{μ}^{†} (x_{n})  (1.3.2)

ove x_{n+μ} è il sito immediatamente a destra dell’x_{n} lungo la direzione μ. Le U_{−μ}(x_{n}) vanno associate cioè ai links che connettono il sito x_{n} ai siti x_{n−μ}. Tale associazione di idee deriva dal ruolo che i campi di gauge giocano nelle teorie al continuo; per rendere invariante locale di fase o invariante di gauge una lagrangiana già invariante globale di fase, dobbiamo eseguire la sostituzione:

∂_{μ} → D_{μ} = ∂_{μ} + igA_{μ}(x)  (1.3.2)

Una trasformazione di gauge fa variare, in modo differente da un punto all’altro dello spazio, le fasi dei campi. Il campo di gauge A_{μ}(x) viene introdotto affiancando la derivata parziale proprio a compensare, con la sua variazione, la variazione subita dalla derivata parziale a causa della differente variazione della fase dei campi da un punto dello spazio all’adiacente; cosí che complessivamente la derivata covariante (la D_{μ}) risulti invariante di gauge. Il campo A_{μ}(x), detto anche connessione, compie il trasporto parallelo necessario a rendere confrontabili le fasi relative ai punti x_{μ} e x_{μ} + dx_{μ}; è intuitivo dunque pensarlo associato al link che connette i due punti.

Una volta introdotti i campi di gauge, vediamo qual’è la forma assunta dalle trasformazioni di gauge sul reticolo, posto che nel limite per a → 0 restituiscano l’ordinaria trasformazione di gauge del continuo.

La trasformazione:

U_{μ} (x_{n}) → U′_{μ} (x_{n}) = V (x_{n}) U_{μ} (x_{n}) V^{†} (x_{n+μ})  (1.3.3)

con V (x_{n}):

V (x_{n}) = e^{−iα_{a}(x_{n})T_{a}}  (1.3.4)

sembra riprodurre esattamente una trasformazione di gauge sul reticolo; la dimostrazione l’abbiamo sviluppando i campi di gauge in serie di a e trascurando i termini di ordine a^{2} che non contribuiscono al limite a → 0:

U′_{μ} (x_{n}) = V (x_{n}) U_{μ} (x_{n}) V^{†} (x_{n+μ})
= V (x_{n}) (1 + igaA_{μ} (x_{n}) + O (a^{2}))V^{†} (x_{n+μ})
= V (x_{n}) V^{†} (x_{n+μ}) + igaV (x_{n}) A_{μ} (x_{n}) V^{†} (x_{n+μ}) + O (a^{2})
= V (x_{n}) V^{†} (x_{n}) + aV (x_{n}) ∂_{μ}V^{†} (x_{n}) + igaV (x_{n}) A_{μ} (x_{n}) V^{†} (x_{n}) + O (a^{2})
= 1 + iga (V (x_{n}) A_{μ} (x_{n}) V^{†} (x_{n}) − \frac{i}{g} V (x_{n}) ∂_{μ}V^{†} (x_{n})) + O (a^{2})  (1.3.5)

figura 1.2. Placchetta

da cui essendo:

U′_{μ} (x_{n}) = 1 + igaA′_{μ} (x_{n}) + O (a^{2})  (1.3.6)

eguagliando all’ordine a otteniamo infine:

A′_{μ} (x_{n}) = V (x_{n}) A_{μ} (x_{n}) V^{†} (x_{n}) − \frac{i}{g} V (x_{n}) ∂_{μ}V^{†} (x_{n})  (1.3.7)

L’ultima espressione riproduce l’ordinaria trasformazione di gauge dei campi nel continuo.

Con questa definizione della trasformazione di gauge, è immediato verificare che sono gauge-invarianti le tracce dei prodotti di campi U_{μ}(x_{n}) calcolati lungo percorsi chiusi qualsiasi del reticolo. Il percorso chiuso più semplice è la placchetta, l’insieme dei quattro links congiungenti quattro siti adiacenti, come mostrato dalla figura 1.2 .

Una scelta possibile per un azione gauge-invariante potrebbe essere del tipo (Wilson 1974, Polyakov 1975, Wegner 1971):

S = \frac{1}{2g^{2}} ∑_{n,μ,ν} tr [U_{μ} (x_{n}) U_{ν} (x_{n+μ}) U_{μ}^{†} (x_{n+ν}) U_{ν}^{†} (x_{n})] + h.c.  (1.3.8)

Essa è infatti locale e gauge invariante; perché sia la corretta azione per i campi di gauge, occorre mostrare che nel limite per il continuo restituisca l’azione di Yang-Mills (Yang, Mills 1954)

S_{YM} = −\frac{1}{2} ∫ d^{4}x tr [F^{μν}(x)F_{μν}(x)]  (1.3.9)
F^{μν}(x) = ∂^{μ}A^{ν}(x) − ∂^{ν}A^{μ}(x) + ig [A^{μ}(x), A^{ν}(x)]  (1.3.10)
F_{a}^{μν}(x) = ∂^{μ}A_{a}^{ν}(x) − ∂^{ν}A_{a}^{μ}(x) − g ∑_{b,c} f_{abc}A_{b}^{μ}(x)A_{c}^{ν}(x)  (1.3.11)

Lo svolgimento del limite al continuo per la (1.3.8) si trova in appendice.

Naturalmente, una volta data l’azione (1.3.8) e quindi la lagrangiana:

L (x_{n}) = \frac{1}{2g^{2}} \frac{1}{a^{4}} ∑_{μ,ν} tr [U_{μ} (x_{n}) U_{ν} (x_{n+μ}) U_{μ}^{†} (x_{n+ν}) U_{ν}^{†} (x_{n})] + h.c.  (1.3.12)

è possibile, supponendo un accoppiamento debole (ossia, la costante di accoppiamento g deve essere sufficientemente piccola), effettuare uno studio perturbativo della teoria così come è stato fatto utilizzando altri tipi di regolarizzazione, quali quella dimensionale o quella di Pauli-Villars. Si tratterà, a partire dalla lagrangiana, di stabilire il corretto insieme di regole di Feynman e di studiare le differenze rispetto al continuo. L’ulteriore verifica del corretto sviluppo della teoria sul reticolo sarà anche in questo caso il test sul limite a → 0, che dovrà restituire la teoria delle perturbazioni originaria. Non entreremo qui nel dettaglio della teoria delle perturbazioni sul reticolo, rinviando per gli approfondimenti ai testi in bibliografia. Tuttavia sarà utile prendere in esame il punto in cui la teoria perturbativa sul reticolo sembra discostarsi maggiormente dalla teoria al continuo; esso è legato al fatto che in tutte le espressioni della teoria sul reticolo, come in quelle appena mostrate della azione e della lagrangiana, compare sempre la variabile di campo:

U^{μ} (x_{n}) = e^{igaA_{a}^{μ}(x_{n})T_{a}}  (1.3.13)

mentre nel continuo la teoria è definita in termini della variabile di campo:

A_{a}^{μ}(x_{n})

Prima di stilare le regole di Feynman per la teoria si dovrà quindi sviluppare in serie il campo U^{μ}(x_{n}):

U^{μ} (x_{n}) = 1 + igaA_{a}^{μ} (x_{n}) T_{a} − \frac{1}{2}g^{2}a^{2} (A_{a}^{μ} (x_{n}) T_{a})^{2} + ⋯  (1.3.14)

Nella lagrangiana il prodotto dei campi U^{μ}(x_{n}) appartenenti ad una data placchetta darà luogo ad una serie infinita di prodotti di campi bosonici, i quali interverranno in numero sempre crescente. I grafici di Feynman associati a tale serie infinita di termini saranno vertici con un numero via via maggiore di linee bosoniche entranti; verranno generati così diagrammi anomali (ovvero senza analogo nel continuo). Tali termini però, compariranno moltiplicati per potenze di a sempre maggiori e nel limite a → 0 gli unici vertici che daranno contributo non nullo saranno quelli a tre linee bosoniche (di ordine g), e a quattro (di ordine g^{2}), come vuole la teoria al continuo.

1.4 FORMULAZIONE FUNZIONALE

La teoria esposta nei capitoli precedenti verrà ora riformulata sfruttando il formalismo funzionale. Il funzionale generatore per una teoria di pura gauge sarà:

Z[J^{μ}] = c ∫ DU e^{−S(U)} e^{∫ d^{4}x J^{μ}(x)U_{μ}(x)}  (1.4.1)

Derivando funzionalmente la Z rispetto alla J, otteniamo le funzioni di Green della teoria, nelle quali risiede tutta la fisica del sistema.

Il valore di aspettazione di una osservabile O è dato da:

⟨O⟩ = c ∫ DU O (U) e^{−S(U)}  (1.4.2)

Il significato dell’espressione ora scritta è il seguente: data una configurazione di campi di gauge U_{i}, il valore che l’osservabile assume in quella configurazione O(U_{i}), pesa nel calcolo del valore medio dell’osservabile secondo il fattore e^{−S(U_{i})}; ossia tanto minore è il valore assunto dall’azione in tale configurazione, tanto più probabile sarà il valore assunto dall’osservabile in corrispondenza di quella configurazione. Il valore medio globale si ottiene integrando (∫ DU) su tutte le possibili configurazioni in cui possiamo trovare i campi di gauge U .

Vediamo ora come tecniche usate in meccanica statistica, quali il metodo di Monte Carlo sviluppato per simulazioni su calcolatore, possano essere utilizzate per calcolare i valori di aspettazione delle osservabili.

L’idea del metodo è di sostituire l’integrazione su tutte le infinite, possibili configurazioni nelle quali possiamo trovare il campo di gauge U, con una media su un insieme finito di configurazioni di campi di gauge {U_{i}}:

⟨O⟩ = c ∑_{i} O (U_{i})  (1.4.3)

L’insieme è scelto però in modo che le configurazioni si distribuiscano secondo la legge di Boltzmann:

e^{−S(U)}  (1.4.4)

cosicché nell’insieme scelto siano presenti in numero maggiore le configurazioni che hanno più peso nell’integrale funzionale.

Esistono diversi algoritmi pensati per calcolatore, in grado di generare un insieme di configurazioni U_{i} distribuite secondo la legge di Boltzmann.

Verrà illustrato l’algoritmo di Metropolis.

1.5 ALGORITMO DI METROPOLIS

Il primo ingrediente dell’algoritmo di Metropolis è una configurazione U di partenza che è costituita da un insieme di matrici di SU(n), ognuna associata ad un link del reticolo (quattro per ogni sito); per una partenza “a freddo”, si può scegliere una configurazione costituita da tutte matrici unitarie.

Si passa quindi dalla configurazione U ad un’altra configurazione U′, semplicemente variando la singola variabile di link, ossia una qualsiasi matrice del reticolo.

Gli schemi che specificano il modo con cui avviene la transizione U → U′ sono molteplici; in tutti però, la procedura di variazione della variabile di gauge deve essere in grado di coprire uniformemente il gruppo continuo cui appartengono le variabili stesse.

Lo schema classico consiste nel servirsi di una tabella di matrici SU(n) random, generata all’inizio e rinnovata periodicamente per evitare pericolose correlazioni. Scelto poi un link, se ne cambia la variabile associata attraverso la moltiplicazione con una matrice scelta casualmente dalla tabella (Wilson 1980).

Una volta generata la nuova configurazione U′, viene calcolata la variazione che subisce l’azione nel passare da U a U′:

ΔS = S (U′) − S (U)  (1.5.1)

La nuova configurazione viene accettata e sostituisce la U se ΔS è minore di zero. Se ΔS è maggiore di zero, la configurazione U′ viene accettata con la probabilità condizionale exp(−ΔS); ossia, dato un numero random x, 0 < x < 1, la nuova configurazione sostituisce la vecchia se:

exp (−ΔS) > x

Trovandoci nella configurazione U′, o nella U nel caso la U′ sia stata rifiutata, ci si sposta su un altro link e si ripete la procedura di variazione.

Dopo aver agito su tutti i link del reticolo, e dopo aver spazzato più volte il reticolo stesso, si giunge ad una configurazione di gauge U_{1} all’equilibrio termico, vale a dire un membro della distribuzione di Boltzmann. Essa può essere quindi utilizzata per calcolare il primo termine O(U_{1}) che contribuisce alla:

⟨O⟩ = c ∑_{i} O (U_{i})  (1.4.3)

La configurazione di gauge U_{1} è quindi riutilizzata come configurazione di partenza dell’algoritmo di Metropolis. La nuova configurazione U_{2} che si ottiene riapplicando la procedura, si troverà parimenti all’equilibrio termico e sarà statisticamente indipendente dalla U_{1}. Riapplicando molte volte l’algoritmo, otteniamo un insieme consistente di configurazioni di gauge {U_{i}}, distribuito secondo la legge di Boltzmann. Più grande sarà l’insieme delle U_{i}, migliore sarà la stima di ⟨O⟩. Viene riportata in appendice la dimostrazione del fatto per cui l’algoritmo di Metropolis conduca il sistema all’equilibrio termico, cioè faccia trovare configurazioni distribuite proprio secondo la legge di Boltzmann exp(−S).

1.6 L’ALGORITMO DI METROPOLIS SUL CALCOLATORE

Consideriamo gli aspetti principali di una implementazione su calcolatore dell’algoritmo di Metropolis.

In corrispondenza alle variabili della teoria di gauge U(x_{n}), in un programma di simulazione dovrà dichiararsi un grande vettore a cinque dimensioni, di N_{x}×N_{y}×N_{z}×N_{t}×4 elementi totali, dove N_{x}, N_{y}, N_{z}, N_{t} sono il numero di siti lungo le quattro direzioni del reticolo; dato che le U(x_{n}) sono associate ai links del reticolo, per ogni sito dovremo dichiarare quattro variabili di gauge; e quindi il vettore avrà una ulteriore dimensione relativa alla direzione del link μ, con μ che può assumere quattro possibili valori μ = 0, 1, 2, 3.

Il vettore di dati così dichiarato (i dati saranno semplici numeri complessi nel caso di una teoria di gauge U(1) come ad esempio la QED, ma avranno la struttura più complessa di matrici 3 × 3 nel caso di una teoria di gauge SU(3) come la QCD) rappresenta la singola configurazione che i campi di gauge possono assumere. Osserviamo che nell’algoritmo di Metropolis tutti i siti e i links del reticolo vengono trattati allo stesso modo, senza che alcuno sia individuato da proprietà particolari e peculiari; la lista di calcoli, lo schema della procedura viene ripetuto immutato per ogni n = (n_{x}, n_{y}, n_{z}, n_{t}) e per ogni μ, cosicché nessuna delle variabili di gauge necessita di un trattamento differenziato.

L’algoritmo richiede notevole potenza di calcolo dato che il singolo passo dell’algoritmo va ripetuto su tutto il reticolo e spazzando più volte il reticolo. Il fatto osservato in precedenza suggerisce però di risolvere il problema dei grandi tempi di calcolo, ricorrendo a calcolatori SIMD.

I calcolatori SIMD (Single Instruction Multiple Data) o calcolatori paralleli ben si prestano alla risoluzione del particolare tipo di problemi che abbiamo affrontato. Essi infatti, sono strutturati in modo che una singola CPU, il processore centrale che imposta il flusso del programma in esecuzione, piloti più unità aritmetiche di calcolo. Ogni unità di calcolo esegue parallelamente alle altre le stesse operazioni aritmetiche, agendo però su dati differenti; ognuna preleva i dati sui quali deve agire dalla propria memoria locale.

Il problema dei grandi tempi di calcolo richiesti per l’algoritmo di Metropolis, su una macchina SIMD viene risolto attraverso il frazionamento del reticolo considerato per la simulazione; ciascuna singola parte del reticolo frazionato viene controllata da una unità di calcolo. Il vettore di dati U_{μ}(x_{n}) sarà così dichiarato su più unita locali di memoria; le elaborazioni da effettuarsi sui singoli elementi del vettore andranno ripetute il numero di volte necessario a coprire le dimensioni del reticolo locale, essendo garantito che esse avverranno parallelamente su tutti i reticoli locali nei quali abbiamo frazionato il reticolo globale.

Resta ben inteso però che le macchine SIMD raggiungono il massimo della efficienza nei punti del programma in cui l’eleborazione consiste effettivamente di una stessa lista di operazioni ripetuta in loop molte volte; il massimo della efficienza significa una riduzione del tempo di calcolo pari a N volte quella che si avrebbe se il calcolatore non fosse parallelo, ove N è il numero di unità di calcolo locali delle quali è provvista la macchina SIMD.

Passiamo ora a considerare i punti critici insiti nel tipo di operazioni matematiche implicate dall’algoritmo di Metropolis; studiamo anche qui margini eventuali di ottimizzazione.

Il passo elementare dell’algoritmo di Metropolis consiste nella variazione della matrice U_{μ}(x_{n}) associata al generico sito del reticolo, attraverso l’applicazione ad essa di una matrice random; quindi nel calcolo della azione S(U′) relativa alla nuova configurazione così ottenuta, attraverso la:

S = \frac{1}{2g^{2}} ∑_{n,μ,ν} tr [U_{μ} (x_{n}) U_{ν} (x_{n+μ}) U_{μ}^{†} (x_{n+ν}) U_{ν}^{†} (x_{n})] + h.c.  (1.3.8)

A parte l’ottimizzazione nel calcolo della serie ∑_{n}, che su un calcolatore SIMD si riduce al calcolo parallelo di serie parziali più piccole, quello che mette in crisi i tempi di esecuzione sono le laboriose operazioni di prodotto fra le matrici appartenenti al gruppo SU(n) che rappresentano le nostre variabili di gauge.

Immaginiamo di eseguire il singolo prodotto fra due matrici U e V di SU(n) servendoci di un qualsiasi linguaggio di programmazione (ad esempio il FORTRAN) e di un qualsiasi calcolatore tradizionale.

Facendo riferimento alla rappresentazione fondamentale del gruppo SU(n), le nostre due matrici U e V sarebbero dichiarate in memoria come arrays bidimensionali di dimensione n × n:

                                  COMPLEX U(n, n), V(n, n)

Il prodotto righe per colonne fra le due risulterebbe in tutta una serie di linee di programma (R matrice risultato del prodotto):

R(1, 1) = U(1, 1) ∗ V(1, 1) + U(1, 2) ∗ V(2, 1) + U(1, 3) ∗ V(3, 1)
R(1, 2) = U(1, 1) ∗ V(1, 2) + U(1, 2) ∗ V(2, 2) + U(1, 3) ∗ V(3, 2)
R(1, 3) = U(1, 1) ∗ V(1, 3) + U(1, 2) ∗ V(2, 3) + U(1, 3) ∗ V(3, 3)
R(2, 1) = U(2, 1) ∗ V(1, 1) + U(2, 2) ∗ V(2, 1) + U(2, 3) ∗ V(3, 1)
R(2, 2) = U(2, 1) ∗ V(1, 2) + U(2, 2) ∗ V(2, 2) + U(2, 3) ∗ V(3, 2)
R(2, 3) = U(2, 1) ∗ V(1, 3) + U(2, 2) ∗ V(2, 3) + U(2, 3) ∗ V(3, 3)
R(3, 1) = U(3, 1) ∗ V(1, 1) + U(3, 2) ∗ V(2, 1) + U(3, 3) ∗ V(3, 1)
R(3, 2) = U(3, 1) ∗ V(1, 2) + U(3, 2) ∗ V(2, 2) + U(3, 3) ∗ V(3, 2)
R(3, 3) = U(3, 1) ∗ V(1, 3) + U(3, 2) ∗ V(2, 3) + U(3, 3) ∗ V(3, 3)

(1.6.1)

L’analisi dei cicli macchina relativi a tale lista di operazioni, suggerisce che il calcolatore passi più tempo nel calcolo di indirizzi di memoria e nell’esecuzione di trasferimenti di dati da memoria a registri, piuttosto che nell’esecuzione delle operazioni aritmetiche stesse. Infatti l’operazione di calcolo dell’indirizzo di memoria dell’elemento U(1, 1) e di trasferimento del dato ad esso relativo da memoria a registri è ripetuta per ben tre volte. Così come accade per tutti gli altri elementi della matrice U o della V.

Una soluzione avente lo scopo di ridurre il numero di fetch in memoria per l’esempio di programma che abbiamo portato, sarebbe quella di dotare il calcolatore di processori a grande numero di registri. I processori dovrebbero essere in grado di contenere le due intere matrici U e V, la matrice risultato R, mantenendo ancora altri registri di appoggio per risultati parziali durante l’evalutazione delle espressioni (1.6.1).

In più, il processore potrebbe essere dotato di una operazione rapida di caricamento su registri di più dati da memoria; la rapidità di una tale operazione dovrebbe derivare dallo sfruttare la consecutività nella allocazione di quei dati in memoria. Ossia, supposto che si vogliano trasferire su registri i dati contenuti in n locazioni di memoria consecutive, il processore potrebbe riferirsi a tali locazioni semplicemente calcolando l’indirizzo di memoria relativo a solo la prima di esse, evitando di ricalcolarlo per le altre. E’ conseguente l’ottimizzazione in termini di cicli macchina.

Supponendo di possedere un calcolatore dalle caratteristiche accennate, l’operazione di prodotto fra le due matrici risulterebbe così ottimizzata: caricamento in blocco sui registri delle due matrici, con calcolo di soli due indirizzi di memoria (il compilatore avrà avuto cura di allocare consecutivamente i dati relativi alla singola matrice); esecuzione delle operazioni aritmetiche facendo riferimento a dati già sui registri (la capacità dei processori evita ulteriori scambi di dati con la memoria); scaricamento in blocco della matrice risultato, con calcolo di un solo indirizzo di memoria.

L’ottimizzazione illustrata fa riferimento a particolari caratteristiche del calcolatore, ma anche del compilatore (e del linguaggio, visto che il compilatore riflette il linguaggio); oltre infatti ad avere effettivamente processori a grande numero di registri e con l’istruzione di caricamento/ scaricamento rapido dati, occorre esista un compilatore che si prenda cura di eseguire nell’ordine corretto la scaletta delle operazioni descritta. Il compilatore sarebbe di molto aiutato se fosse istruito, piuttosto che con le linee del linguaggio tradizionale sopra mostrate, con, ad esempio, la linea:

                                         R = U ∗ V

e nello stesso tempo il programma ne guadagnerebbe in chiarezza.

1.7 CAMPI FERMIONICI SUL RETICOLO

In questo paragrafo vedremo come si introducono sul reticolo i campi fermionici e quali sono le difficoltà che si incontrano.

Assumiamo come punto di partenza l’azione per il campo di Dirac libero nel continuo; nello spazio Euclideo essa assume la forma:

S (ψ) = ∫ d^{4}x [\overline{ψ} (x) γ^{μ}∂_{μ}ψ (x) + m\overline{ψ} (x) ψ (x)]  (1.7.1)

Il modo più immediato per discretizzare l’espressione ora scritta è sostituire all’integrale, la sommatoria su tutti i siti del reticolo:

∫ d^{4}x → ∑_{n} a^{4}  (1.7.2)

e sostituire alle derivate le corrispondenti differenze finite:

∂_{μ}ψ (x_{n}) → \frac{ψ (x_{n+μ}) − ψ (x_{n−μ})}{2a}  (1.7.3)

Vale la pena osservare che quella appena scritta non è l’unica possibile traduzione nel discreto della derivata; sono possibili definizioni di derivate sul discreto coinvolgenti valori dei campi assunti in punti anche non strettamente adiacenti al sito x_{n} nel quale si calcola la derivata, ma, al fine di preservare l’ipotesi di località, mai comunque coinvolgenti i valori in tutti i punti del reticolo. Sotto certi punti di vista, tali più complicate definizioni possono comportarsi meglio. Qualunque espressione per la derivata del discreto avessimo scelto, al limite per a → 0 essa deve tendere a ∂_{μ}ψ.

La più semplice azione locale che nel limite al continuo riproduca l’azione (1.7.1) è la cosiddetta azione naive:

S (ψ) = ∑_{n} a^{4} [∑_{μ} \overline{ψ} (x_{n}) γ^{μ} \frac{ψ (x_{n+μ}) − ψ (x_{n−μ})}{2a} + m\overline{ψ} (x_{n}) ψ (x_{n})]  (1.7.4)

corrispondente ad una lagrangiana:

L (x_{n}) = ∑_{μ} \overline{ψ} (x_{n}) γ^{μ} \frac{ψ (x_{n+μ}) − ψ (x_{n−μ})}{2a} + m\overline{ψ} (x_{n}) ψ (x_{n})  (1.7.5)

Imponiamo quindi che anche nel discreto la lagrangiana sia invariante sotto trasformazioni di gauge, le quali su campi fermionici hanno la forma:

\begin{aligned}ψ (x_{n}) & → ψ′ (x_{n}) = V (x_{n}) ψ (x_{n}) \\ \overline{ψ} (x_{n}) & → \overline{ψ}′ (x_{n}) = \overline{ψ} (x_{n}) V^{†} (x_{n}) \end{aligned}  (1.7.6)

Considerando la lagrangiana (1.7.5), il termine di massa è invariante a vista; il termine cinetico invece, risulta non invariante poiché la derivata dei campi ∂_{μ}ψ non si trasforma come i campi stessi. Introduciamo ora la derivata covariante D_{μ}(U)ψ, coinvolgente i campi di gauge U:

D̸ (U) ψ (x_{n}) = \frac{γ^{μ}U_{μ} (x_{n}) ψ (x_{n+μ}) − γ^{μ}U_{−μ} (x_{n}) ψ (x_{n−μ})}{2a}  (1.7.7)

Conoscendo il modo in cui si trasformano i campi di gauge U, si può dimostrare che sotto trasformazioni di gauge la derivata covariante si trasforma proprio come i campi stessi:

D̸ (U) ψ (x_{n}) → D̸ (U′) ψ′ (x_{n}) = V (x_{n})D̸ (U) ψ (x_{n})  (1.7.8)

La:

L (x_{n}) = ∑_{μ} \overline{ψ} (x_{n}) γ^{μ} \frac{U_{μ} (x_{n}) ψ (x_{n+μ}) − U_{−μ} (x_{n}) ψ (x_{n−μ})}{2a}m\overline{ψ} (x_{n}) ψ (x_{n})  (1.7.9)

risulta perciò invariante di gauge.

1.8 SPECIES DUBLING - FERMIONI DI WILSON

Consideriamo di nuovo la teoria libera sul reticolo e la lagrangiana naive (1.7.5). Il propagatore fermionico che si ottiene da essa presenta alcune anomalie; eseguendo i dovuti calcoli, si ottiene per esso:

S_{F} (p) = \frac{\frac{1}{a} ∑_{μ} γ^{μ} sin(ap_{μ}) + m}{\frac{1}{a^{2}} ∑_{μ} sin^{2}(ap_{μ}) − m^{2}}  (1.8.1)

il quale, per p finito, possiede un corretto limite al continuo

lim_{a→0} S_{F} (p) = \frac{∑_{μ} γ^{μ}p_{μ} + m}{p^{2} − m^{2}}  (1.8.2)

Se andiamo però a studiare dove si annulla il denominatore della (1.8.1) (per semplicità poniamoci nel caso m = 0), ci accorgiamo che la S_{F} (p) possiede, oltre al polo p = (0, 0, 0, 0), che è in comune con la teoria al continuo, anche altri 15 poli distinti, p = (±π/a, 0, 0, 0), p = (0, ±π/a, 0, 0), etc., tutti contenuti all’interno della zona di Brillouin del reticolo −π/a ≤ p^{μ} < +π/a. In corrispondenza ai 16 poli distinti dovremo considerare 16 specie fermioniche differenti.

Si potrebbe osservare che nel limite al continuo i 15 poli anomali tendono all’infinito; per cui mantenendo p finito, potremo linearizzare il propagatore in un intorno dell’unico polo rimasto finito. Ma non ha alcun senso pretendere di lasciare finito il momento di un propagatore: spesso infatti, il propagatore compare in contesti (ampiezze di transizione) ove il suo argomento p risulta integrato fra −∞ e +∞.

Non entreremo nei dettagli dell’analisi del fenomeno, noto sotto il nome di species-dubling, rimandando gli approfondimenti ai testi in bibliografia. Utilizzeremo però i risultati degli studi che hanno consentito di superare il problema, che hanno cioè fornito i mezzi per sbarazzarsi delle specie fermioniche in eccesso. Le formulazioni in questo senso più usate sono quella dei fermioni staggered, dovuta a Kogut e Susskind (Banks, Kogut, Susskind 1976), e quella di Wilson (Wilson 1975; Karsten, Smit 1981). Nel seguito ci riferiremo esclusivamente a quest’ultima che oggettivamente fornisce un approccio meno complesso.

Il metodo di Wilson consiste nell’aggiungere alla lagrangiana un termine in grado di eliminare dal propagatore i poli in eccesso.

Il termine di Wilson:

\frac{r}{2a}\overline{ψ} (x_{n}) [2ψ (x_{n}) − ψ (x_{n+μ}) − ψ (x_{n−μ})]  (1.8.3)

ha l’aspetto di un termine cinetico bosonico \overline{ψ}∂^{μ}∂_{μ}ψ moltiplicato per un fattore ra. Il fattore a fa sì che il termine di Wilson non contribuisca al limite al continuo (cosicché il limite al continuo della lagrangiana resti invariato). Il fattore r è invece arbitrario; usualmente nelle simulazioni su calcolatore viene posto a 1, ma nelle elaborazioni teoriche si preferisce mantenerlo indicato per evidenziare le parti che vi dipendono. L’eliminazione di un tale fattore non fisico in quanto arbitrario, sarà oggetto di una rinormalizzazione della teoria sul reticolo.

Anche il termine di Wilson può essere reso gauge invariante attraverso l’inserzione del campo U:

\frac{r}{2a}\overline{ψ} (x_{n}) [2ψ (x_{n}) − U_{μ} (x_{n}) ψ (x_{n+μ}) − U_{−μ} (x_{n}) ψ (x_{n−μ})]  (1.8.4)

Il termine di Wilson risolve il problema della replicazione delle specie fermioniche ma al prezzo di rompere esplicitamente una simmetria di cui godeva la lagrangiana nel continuo nel caso in cui la massa fermionica è nulla: la simmetria chirale. Una trasformazione chirale è una trasformazione in cui le componenti levogire (L) e destrogire (R) dei campi fermionici si trasformano differentemente:

\begin{matrix}ψ_{L}(x_{n}) → ψ′_{L}(x_{n}) = e^{−iα}ψ_{L} \\ ψ_{R}(x_{n}) → ψ′_{R}(x_{n}) = e^{iα}ψ_{R} \\ \overline{ψ}_{L}(x_{n}) → \overline{ψ}′_{L}(x_{n}) = e^{iα}\overline{ψ}_{L} \\ \overline{ψ}_{R}(x_{n}) → \overline{ψ}′_{R}(x_{n}) = e^{−iα}\overline{ψ}_{R} \end{matrix}  (1.8.5)

La perdita dell’invarianza rispetto a trasformazioni chirali è un fenomeno di una certa gravità, specialmento se l’intento è di descrivere sul reticolo una teoria elettrodebole, per la quale è fondamentale mantenere l’invarianza rispetto alla simmetria SU(2)_{L}. Questo comunque non sarà un problema di cui ci cureremo nel seguito.

1.9 QCD: TEORIA AL CONTINUO E FORMULAZIONE FUNZIONALE

Nell’introduzione abbiamo già visto la lagrangiana della teoria QCD al continuo:

\begin{aligned}L (x) = & ∑_{n_{f} =1}^{6} [i\overline{ψ}_{i} (x) γ_{μ} (δ_{ij}∂^{μ} − ig (T_{a})_{ij} A_{a}^{μ} (x)) ψ_{j} (x) − m\overline{ψ}_{i} (x) ψ_{i} (x)] + \\ & − \frac{1}{4} tr[F^{μν}(x)F_{μν}(x)] \end{aligned}  (1.9.1)

In essa le ψ sono i campi associati ai singoli quark. I quark dovrebbero esistere in sei varietà o sapori, diverse per massa e carica elettrica; alle ψ è perciò associato anche un indice di sapore n_{f} sul quale dovrebbe correre la sommatoria ma che noi non abbiamo mostrato nell’intento di non appesantire la notazione. Per ogni sapore, oltre all’indice di Dirac, dobbiamo considerare l’ulteriore indice i = 1, 2, 3, associato al grado di libertà di colore del quark.

Gli A_{a}^{μ}, a = 1, ..., 8, sono i campi associati ai gluoni, gli 8 bosoni vettori mediatori dell’interazione forte. L’indice a è l’indice di colore dei gluoni.

Le T_{a} sono i generatori dell’Algebra di Lie associata al gruppo SU(3), legate alle matrici λ_{a} di Gellmann tramite la:

T_{a} = \frac{1}{2}λ_{a}  (1.9.2)

Le F^{μν}(x) sono legate alle matrici T_{a} e ai campi A_{a}^{μ}(x) tramite le:

\begin{matrix}F^{μν} (x) = F_{a}^{μν} (x) T_{a} \\ F_{a}^{μν} (x) = ∂^{μ}A_{a}^{ν} (x) − ∂^{ν}A_{a}^{μ} (x) − gf_{abc}A_{b}^{μ} (x) A_{c}^{ν} (x) \end{matrix}

Vediamo ora lo sviluppo della teoria secondo il formalismo funzionale.

Il funzionale generatore della teoria è:

\begin{matrix}Z (J^{μ}, \overline{η}, η) = & c ∫ DUD\overline{ψ}Dψ e^{− ∫ d^{4}x \overline{ψ}(D̸(U)+m)ψ}· \\ e^{− ∫ d^{4}x [− \frac{1}{4} tr(F^{μν}F_{μν})]} e^{∫ d^{4}x (J^{μ}U_{μ}+\overline{η}ψ+\overline{ψ}η)} \end{matrix}  (1.9.3)

ove le η sono variabili di Grassmann, ossia sono elementi di un algebra anticommutante.

Per le proprietà con cui sono definite le variabili di Grassmann vale la (Itzykson, Zuber):

∫ D\overline{z}Dz e^{−\overline{z}Az+\overline{z}u+\overline{u}z} = det (A) e^{\overline{u}A^{−1}u}  (1.9.4)

Grazie alla quale possiamo ridurre l’espressione per il funzionale generatore (1.9.3) eseguendo immediatamente l’integrazione sulle ψ.

Z (J^{μ}, \overline{η}, η) = c ∫ DU det (D̸ (U) + m) e^{− ∫ d^{4}xd^{4}y \overline{η}(x)S(x,y;U)η(y)} e^{−S_{g}} e^{∫ d^{4}x J^{μ}(x)U_{μ}(x)}  (1.9.5)

(con S_{g} indichiamo l’azione relativa ai campi di gauge liberi).

S(x, y; U) dovrebbe essere (D̸(U) + m)^{−1} (l’inverso dell’operatore fra parentesi), ossia la funzione di Green dell’operatore (D̸(U) + m):

(D̸_{x} (U) + m)_{ij}^{αβ} S_{jk}^{βγ}(x, y) = δ^{(4)} (x − y) δ_{ik}δ_{αγ}  (1.9.6)

det(D̸(U) + m) è il determinante (funzionale) dell’operatore (D̸(U) + m), che sul reticolo assume la forma tradizionale di determinante di una matrice N × N (N numero di siti del reticolo moltiplicato inoltre per 3 e per 4).

figura 1.3. Correzioni da loop fermionico.

Dal funzionale generatore (1.9.5) possiamo ricavare tutte le funzioni di Green della teoria. Ad esempio il propagatore fermionico della teoria con interazione, risulterebbe:

⟨T(ψ(x)\overline{ψ}(0))⟩ = \frac{∂^{2}Z (J^{μ}, \overline{η}, η)}{∂η(0)∂\overline{η}(x)}|_{J^{μ},\overline{η},η≡0} = c ∫ DU S (x, 0; U) det (D̸ (U) + m) e^{−S_{g}}  (1.9.7)

Lo sviluppo in teoria delle perturbazioni del termine det(D̸(U) + m):

\begin{aligned}det & (D̸ (U) + m) = exp(ig ∫ dx_{1} S_{αβ} (x_{1} − x_{1})A̸_{βα} (x_{1}) + \\ & + \frac{g^{2}}{2} ∫ dx_{1}dx_{2} S_{αβ} (x_{1} − x_{2})A̸_{βγ} (x_{2}) S_{γδ} (x_{2} − x_{1})A̸_{δα} (x_{1}) + ⋯) \end{aligned}  (1.9.8)

mostra che tale termine rende conto di tutti i contributi che costituiscono correzioni da loop fermionico al propagatore fermionico. Cioè da correzioni che su diagrammi di Feynman risulterebbero come loop fermionici connessi a due o più gluoni, i quali sarebbero poi direttamente o indirettamente riassorbiti dal propagatore fermionico del primo ordine (figura 1.3).

figura 1.4. Propagatore al primo ordine con correzioni bosoniche.

Nella approssimazione quenched, viene trascurato il contributo dovuto a questo termine; è come se prendessimo in considerazione per il propagatore fermionico del primo ordine, solo correzioni agli ordini superiori di tipo bosonico (figura 1.4).

Quindi, il propagatore fermionico relativo ad un quark di sapore e colore qualsiasi, nella sua espressione finale ossia la più semplificata (e l’unica che prenderemo in considerazione in seguito), risulta essere:

⟨T(ψ(x)\overline{ψ}(0))⟩ = c ∫ DU S (x, 0; U) e^{−S_{g}}  (1.9.9)

A questo punto dovrebbero essere evidenti le analogie che legano l’ultima espressione con l’espressione (1.4.2) fornita precedentemente.

Valgono quindi tutte le considerazioni fatte precedentemente per il calcolo di osservabili relativi ad una teoria di pura gauge. Possiamo così fare uso dell’algoritmo di Metropolis anche per il calcolo di osservabili della teoria con interazione.

Nel nostro esempio (il calcolo del propagatore fermionico) l’unica difficoltà aggiuntiva sta nel calcolo della quantità S(x, 0; U). In sostanza si tratta di escogitare algoritmi efficienti su calcolatore in modo da risolvere l’equazione:

(D̸_{x} (U) + m)_{ij}^{αβ} S_{jk}^{βγ} (x, y; U) = δ^{(4)} (x − y) δ_{ik}δ_{αγ}  (1.9.10)

con S incognita della equazione.

Naturalmente l’equazione va risolta per ogni singola configurazione U_{i} dei campi di gauge che l’algoritmo di Metropolis genera.

Affronteremo questi ed altri problemi nel paragrafo che segue, dedicato soprattutto alle questioni di implementazione informatica.

1.10 L’ALGORITMO DI INVERSIONE

Il problema di calcolare la S tale che:

(D̸_{x} (U) + m)_{ij}^{αβ} S_{jk}^{βγ} (x, y; U) = δ^{(4)} (x − y) δ_{ik}δ_{αγ}  (1.9.10)

si traduce sul reticolo nel problema di risolvere l’equazione matriciale:

Mk = b  (1.10.1)

M è la matrice N ×N (N pari alla dimensione globale del reticolo N_{x}, N_{y}, N_{z}, N_{t}) che rappresenta l’operatore (D̸(U)+m) sul reticolo. In M dobbiamo includere oltre all’operatore che agisce sugli indici spazio-temporali reticolari, anche gli operatori che agiscono sull’indice di colore e sull’indice di Dirac dell’oggetto cui si applica, come del resto è evidenziato dalla presenza degli indici α, β, i, j che l’operatore (D̸(U) + m) mostra nella (1.9.10).

Sul reticolo l’operatore (D̸(U) + m) dovrà naturalmente contenere anche il termine di Wilson senza il quale ci si troverebbe di fronte al problema del raddoppiamento delle specie. L’operatore assumerà la forma:

(D̸ (U) + m) = \frac{1}{2a} [(γ^{μ} − r) U_{μ} (x_{n}) δ_{μ}^{+} − (γ^{μ} + r) U_{−μ} (x_{n}) δ_{μ}^{−}] + (m + \frac{2r}{2a})  (1.10.2)

con:

δ_{μ}^{+}χ (x_{n}) = χ (x_{n+μ})  (1.10.3)

Proseguendo nell’analisi della (1.10.1), k è il vettore incognita dell’equazione e b è il vettore in relazione al termine noto a destra della (1.9.10).

k e b e sono vettori perché, invece della (1.9.10), in realtà si sta considerando la:

(D̸_{x} (U) + m)_{ij}^{αβ} S_{jk}^{βγ} (x, 0; U) = δ^{(4)} (x − 0) δ_{ik}δ_{αγ}  (1.10.4)

cioè si fissa la sorgente nell’origine del sistema di riferimento ed inoltre si espande l’equazione nelle dodici equivalenti ottenute esplicitando gli indici di colore e di Dirac γ e k dell’origine.

Esistono numerosi algoritmi in grado di risolvere l’equazione (1.10.1), tutti basati su metodi di rilassamento; si arriva cioè alla soluzione per approssimazioni successive.

Un metodo molto semplice è quello dovuto a Jacobi. In esso, si tratta di trovare, attraverso una procedura iterativa, valori per k sempre più vicini al valore esatto soluzione dell’equazione. Lo scopo è di riuscire, dopo un certo numero di iterazioni, a ridurre al di sotto di un certo valore il residuo della equazione, che è definito come:

r = − [Mk − b]  (1.10.5)

Al limite per un residuo uguale a zero, avremo:

k = M^{−1}b  (1.10.6)

ossia k conterrà proprio la soluzione esatta dell’equazione.

La procedura ricorsiva utilizzata per trovare dei k sempre più vicini alla soluzione dell’equazione è la seguente: poniamo r_{n} sia il residuo relativo all’ennesimo valore di approssimazione k_{n} ottenuto per k al passo precedente.

r_{n} = − [Mk_{n} − b]  (1.10.7)

Definiamo poi il valore di approssimazione k_{n+1} come dato dalla:

k_{n+1} = k_{n} + ϵr_{n}  (1.10.8)

con ϵ parametro scelto abbastanza piccolo.

Vogliamo dimostrare che ripetendo la procedura infinite volte, le successive approssimazioni k_{n+m} tendono al valore soluzione dell’equazione. Mettiamo la k_{n+1} in funzione della k_{n}, sostituendo nella (1.10.8) la (1.10.7):

k_{n+1} = k_{n} − ϵMk_{n} + ϵb = ϵb + (1 − ϵM) k_{n}  (1.10.9)

la k_{n+2} a sua volta sarà data da:

k_{n+2} = ϵb + (1 − ϵM) k_{n+1} = ϵb + (1 − ϵM) ϵb + (1 − ϵM)^{2} k_{n}  (1.10.10)

Ora, posto che abbiamo scelto ϵ in modo tale che:

∥1 − ϵM∥ < 1  (1.10.11)

dove il simbolo ∥ ∥ denota il massimo autovalore dell’operatore incluso; partendo da una approssimazione qualsiasi k_{0} (anche uguale al vettore nullo) e ripetendo all’infinito la procedura:

\begin{aligned}lim_{n→∞} k_{n} & = lim_{n→∞} ϵb + (1 − ϵM) ϵb + (1 − ϵM)^{2} ϵb + ⋯ + (1 − ϵM)^{n−1} ϵb + (1 − ϵM)^{n} k_{0} \\ & = ∑_{n=0}^{∞} (1 − ϵM)^{n} ϵb = M^{−1}b \end{aligned}  (1.10.12)

otteniamo la soluzione dell’equazione.

Su un calcolatore, naturalmente, la procedura non viene ripetuta all’infinito ma è interrotta allorché il residuo scende al di sotto di un certo valore, piccolo compatibilmente con la potenza e la precisione della macchina.

Ovviamente non si starà qui a discutere sulla bontà del metodo di Jacobi o sulle argomentazioni che rendono di fatto inutilizzato il metodo di Jacobi, portandogli a preferire altri più sofisticati metodi (metodi di Gauss-Siedel o del conjugate-gradient soprattutto). Il metodo di Jacobi è uno dei tanti algoritmi di inversione escogitabili, forse il più semplice; gli altri non differiscono poi così tanto dal metodo di Jacobi; sostanzialmente, nella struttura e nella tipologia dei calcoli, si somigliano tutti. La macchina e il compilatore grazie ai quali il metodo di Jacobi risulta ben implementato, certamente sono adatti anche allo sviluppo degli altri metodi di inversione.

Studiamo ora un programma efficiente per calcolatore, il cui risultato sia la S(x, y; U) ottenuta attraverso l’algoritmo di inversione di Jacobi.

I due passi dell’algoritmo sono il calcolo del residuo:

r_{n} = −Mk_{n} + b  (1.10.13)

e il calcolo della successiva approssimazione per il vettore k:

k_{n+1} = k_{n} + ϵr_{n}

Il punto che rallenta maggiormente l’esecuzione del programma è il calcolo di Mk_{n}.

Come abbiamo già affermato, M sottintende oltre a matrici di Dirac agenti su indici di Dirac, e matrici di SU(3) agenti su indici di colore, soprattutto la matrice N × N agente sull’indice spazio-temporale del reticolo. Una matrice N × N anche nel caso di reticoli piccoli sarebbe grandissima; quella sottintesa da M però, ha la caratteristica di essere pressappoco diagonale. Infatti per la (1.10.2) che fornisce (D̸(U) + m) cui M è in relazione, il valore di ϕ(x_{n}) definito

ϕ_{i}^{α} (x_{n}) = (D̸_{x} (U) + m)_{ij}^{αβ} χ_{j}^{β} (x_{n})  (1.10.14)

dipende, per ogni punto del reticolo x_{n}, dal valore assunto da χ nello stesso punto x_{n} o al più nei punti immediati vicini x_{n+m} e x_{n−m}.

La macchina SIMD descritta nel paragrafo 1.6, sembrerebbe poter ben funzionare anche nella risoluzione di questo problema. Su di essa però dovremo fare una ulteriore richiesta: i nodi della macchina, ove per nodo intendiamo l’insieme di un processore locale e di una memoria locale, dovranno essere organizzati in struttura reticolare e su questa struttura dovrà essere consentito lo scambio dei dati almeno fra i nodi immediati vicini. Anche in questo caso, troveremmo una ottimizzazione nel frazionamento del reticolo globale N_{x}, N_{y}, N_{z}, N_{t} in tanti reticoli locali, ognuno assegnato al singolo nodo della macchina; per cui il vettore χ(x_{n}) relativo alla k dell’algoritmo, risulterebbe disperso sulle diverse memorie locali. Il calcolo del vettore ϕ procederà parallelamente sui diversi nodi.

La differenza rispetto al caso trattato nel paragrafo 1.6, sta nel fatto che qui l’algoritmo ha caratteristiche di “pseudolocalità”. Infatti per calcolare il valore di ϕ nel punto x_{n} si ha bisogno, oltre che del valore di χ nel punto x_{n}, anche dei valori di χ nei punti adiacenti (x_{n+1}, x_{n−1}); i quali potrebbero essere contenuti in una memoria locale diversa da quella che contiene i valori relativi al punto x_{n}. Ecco dunque la necessità di avere una macchina SIMD con possibilità di scambio di dati fra nodi vicini.

La applicazione degli operatori contenuti nella M = (D̸(U) + m) che agiscono sugli indici di colore e di Dirac di k = χ(x_{n}), può, nelle caratteristiche essenziali, essere trattata allo stesso modo in cui si trattò nel paragrafo 1.6 il prodotto di matrici SU(3). Ad esempio la saturazione dell’indice di colore j fra la matrice U contenuta in (D̸(U) + m) e il vettore χ:

∑_{j} U_{ij}χ_{j}^{β}  (1.10.15)

può essere eseguita rispettando la scaletta di operazioni: trasferimento sui registri dei dati associati alla matrice di SU(3) U; trasferimento sui registri dei dati associati allo spinore χ; quindi esecuzione della sommatoria su j per ogni i e β, senza ulteriori richieste di altri accessi in memoria; trasferimento in memoria dello spinore risultato ad indici i e β.

La applicazione di matrici di Dirac su oggetti ad indice spinoriale, può realizzarsi attraverso un altro tipo di ottimizzazione, questa volta a cura esclusiva del compilatore e del linguaggio pensato per la QCD.

Alla base della ottimizzazione sta un modo particolare di vedere l’operazione:

∑_{β} Γ^{αβ}χ_{j}^{β}  (1.10.16)

evitando, come sarebbe istintivo, di espandere la sommatoria. Inanzitutto osserviamo che la matrice Γ, a differenza di U, può essere considerata come una espressione costante. In più, si può sfruttare il fatto che le matrici di Dirac non possiedono una definizione univoca, ma sono determinate solo a meno di relazioni di anticommutazione e proprietà di hermitianità/antihermitianità; per esse dunque si possono scegliere particolari rappresentazioni nelle quali quasi tutti gli elementi delle matrici siano nulli, meno che alcuni i quali assumano solo i valori 1, −1, i, −i.

Si può ottimizzare l’operazione (1.10.16) osservando che in tali rappresentazioni lo spinore risultato deriva dallo spinore operando per semplice rimescolamento delle componenti, o al più per qualche cambiamento di segno o scambio fra le parti immaginarie e reali delle componenti. In un linguaggio in cui sia stato definito un operatore Γ agente in questa maniera, la (1.10.16) verrebbe tradotta a basso livello con semplici istruzioni di MOVE, senza l’esecuzione di alcuna istruzione di calcolo aritmetico.

1.11 IL CALCOLO DEGLI OSSERVABILI

In quest’ultimo paragrafo illustreremo con alcuni semplici esempi come è possibile ottenere, una volta noto il propagatore del quark, gli osservabili di interesse.

Le masse degli stati legati sono calcolate dallo studio dell’andamento del propagatore a grandi tempi; sia infatti Δ_{F}(x) il propagatore nell’Euclideo:

Δ_{F}(x) = ∫ \frac{d^{4}k}{(2π)^{4}} \frac{e^{−kx}}{k^{2} + m^{2}}  (1.11.0)

Chiamiamo C(x_{4}) il propagatore integrato su tutti i punti spaziali:

C(x_{4}) = ∑_{x⃗} Δ_{F}(x⃗, x_{4}) = ∫ \frac{dk_{4}}{2π} \frac{e^{−k_{4}x_{4}}}{k_{4}^{2} + m^{2}} =

= \frac{1}{m_{0}} e^{−m_{0}|x_{4}|} + contributi dovuti agli stati eccitati

(1.11.0)

A grandi tempi euclidei i contributi dovuti agli stati eccitati sono trascurabili; otteniamo così una misura diretta della massa dello stato fondamentale.

Un propagatore mesonico è dato da:

⟨\overline{ψ}(x)ψ(x)Γ\overline{ψ}(0)ψ(0)⟩

ove gli indici di colore dei campi fermionici sono saturati in modo da avere singoletti di colore e gli indici spinoriali sono saturati attraverso opportune combinazioni di matrici di Dirac al fine di ottenere i giusti numeri quantici J^{P S} del mesone considerato.

Il propagatore mesonico è ricavabile dall’integrale funzionale per derivazione funzionale rispetto alle variabili grassmaniane η ed \overline{η}, e risulta:

⟨\overline{ψ}(x)ψ(x)Γ\overline{ψ}(0)ψ(0)⟩ = c ∫ DU S(x, 0; U)ΓS(0, x; U)e^{−S_{g}}  (1.11.0)

facilmente calcolabile una volta che si è ottenuta la S mediante l’algoritmo di inversione.

Per i barioni, secondo il modello a quark costituiti da tre quark, il propagatore sarà invece:

⟨\overline{ψ}(x)\overline{ψ}(x)\overline{ψ}(x)ψ(0)ψ(0)ψ(0)⟩

e di conseguenza nell’integrale funzionale dovremo saturare oppurtunamente (a seconda dei valori J^{P S} del barione) gli indici di tre S.

Il linguaggio dovrà fare una certa attenzione affinché anche queste operazioni ricevano una implementazione adeguata, anche se non sono richieste per esse particolari ottimizzazioni (sono infatti al di fuori degli algoritmi per i quali sono richiesti grandi tempi di calcolo). Una volta definiti nel linguaggio i tipi e gli operatori adeguati non vi sono particolari problemi nell’eseguire anche queste saturazioni.

I primi interessanti risultati per spettri di massa adronici sono stati ottenuti da H. Hamber, G. Parisi (1981) e L. Maiani, G. Martinelli (1986); gli ultimi e più completi da Ape Collab. (1989, 1990, 1991).

La teoria è in grado di fornire risultati anche per quanto riguarda le costanti di decadimento degli adroni instabili; in particolare un importante obiettivo è quello di prevedere il valore della costante di decadimento f_{B} dei mesoni pesanti B sulla quale non esistono ancora validi risultati sperimentali.

CAPITOLO 2 TEORIA DEI LINGUAGGI FORMALI

2.1 INTRODUZIONE

Il linguaggio è l’insieme organizzato dei segni e dei suoni attraverso il quale avviene la comunicazione, ma manifestazioni del linguaggio possono essere considerate anche le accentuazioni della voce o la gestualità. Tutti noi abbiamo presenti vari esempi di linguaggio; fra questi certamente il linguaggio che usiamo quotidianamente per trasmettere informazioni o per fare riflessioni; ma ci preme di sottolineare che deve essere considerata una forma di linguaggio anche l’insieme di segni e simboli matematici grazie ai quali sono state esposte tutte le teorie fisiche o matematiche.

Fin dall’antichità ci si è posti il problema di fornire una descrizione adeguata e completa del linguaggio; la questione era già presente ai greci i quali sentivano la necessità, tipica della mentalità greca, di ricercare un ordine precostituito nella “babele” dei dialetti della lingua greca, delle variazioni locali dei singoli dialetti e delle mutazioni che la lingua subiva nel corso del tempo. L’obiettivo era quello di cristallizzare la lingua in una forma pura, costante nel tempo (come predicavano gli atticisti), ma anche quello più ambizioso di ricercare la lingua ideale, la famosa κoινή, ossia una lingua razionale, comune a tutti i popoli greci, con la quale risultasse agevole riflettere sui concetti più profondi e astratti. Nacque così la disciplina della grammatica intesa come “scienza” del linguaggio.

In tempi relativamente più recenti, ossia verso la fine degli anni ’50, fu sviluppata soprattutto per merito di Noam Chomsky una teoria matematica detta teoria dei linguaggi formali. Tale teoria ricercava una formalizzazione dei concetti di linguaggio e di sintassi nati in ambito non matematico; si trattava cioè di fornire per essi delle definizioni rigorose e operative, punto di partenza per lo sviluppo di una teoria in grado di descrivere compiutamente le molteplici e complesse manifestazioni del linguaggio.

L’obiettivo degli autori era quello di arrivare attraverso la teoria ad inquadrare in una solida struttura matematica i cosiddetti linguaggi naturali; la lingua italiana o la lingua inglese sono validi esempi di ciò che intendiamo per linguaggi naturali.

Dobbiamo dire che, in questa direzione, l’obiettivo è stato conseguito solo in parte; la teoria dei linguaggi formali però ha riscosso successo ed è stata utilissima nella descrizione dei linguaggi di programmazione, un tipo di linguaggi certamente meglio formalizzabile.

Descriveremo in questo capitolo i fondamenti della teoria dei linguaggi formali, soffermandoci in maniera particolare sulle parti di essa che saranno necessarie ad una compiuta comprensione degli argomenti che seguiranno, e trascurando altre parti, quelle ad esempio più interessanti in vista di una formalizzazione dei linguaggi naturali.

2.2 VOCABOLARIO

Ogni teoria matematica comincia col definire gli oggetti con cui avrà a che fare. I primi passi della teoria dei linguaggi formali consistono nella definizione dei concetti di vocabolario, di frase e di linguaggio formale.

Definizione 2.1 Il vocabolario di un linguaggio formale è l’insieme dei simboli “leciti” del linguaggio.

Una tale definizione di vocabolario coincide solo parzialmente col significato comune della parola; infatti sul libro che noi chiamiamo “vocabolario della lingua italiana”, non troviamo elencati verbi coniugati o sostantivi declinati. Verbi coniugati e sostantivi declinati sono a tutti gli effetti simboli leciti del nostro linguaggio; e per questo saranno elementi dell’insieme che abbiamo definito vocabolario.

Il vocabolario può essere insieme finito o anche infinito. Nel seguito, penseremo al vocabolario come un insieme anche grande, ma sempre finito di simboli.

2.3 DEFINIZIONE DI LINGUAGGIO FORMALE

Immaginiamo dato un vocabolario V:

V = {simbolo1, simbolo2, …}  (2.3.1)

La frase può essere definita come una “sequenza” di simboli di V.

Definizione 2.2 Chiamiamo frase l’elemento dell’insieme chiusura V^{∗} dell’insieme V, definito:

V^{∗} = {ϵ} ∪ V ∪ V^{2} ∪ V^{3} ∪ …  (2.3.2)

({ϵ} è l’insieme contenente la sola “frase nulla”).

V^{2} è l’insieme prodotto cartesiano di V, V^{2} = V × V, ossia l’insieme costituito da elementi risultanti da una qualsiasi possibile concatenazione di due soli simboli di V; V^{3} è l’insieme V × V × V, ossia l’insieme delle frasi costruite su V concatenando tre simboli; e in generale V^{n} è l’insieme delle frasi di lunghezza n su V.

Definizione 2.3 La lunghezza ∥f∥ di una frase f ∈ V^{∗} su V è il numero n per cui valga la f ∈ V^{n}.

In sostanza la lunghezza di una frase è il numero di simboli contenuti in essa, contando anche eventuali ripetizioni di uno stesso simbolo che dovessero occorrere lungo la frase.

Non vi sono limiti alla lunghezza di una frase, come stanno ad indicare i … nella definizione. Quindi, anche se il vocabolario contiene un numero finito di simboli, l’insieme V^{∗} delle frasi costruite a partire da quel vocabolario è infinito. Una frase di lunghezza eccedente il numero di simboli appartenenti a V conterrà certamente ripetizioni di uno stesso simbolo.

Il senso comune del termine frase non coincide propriamente con la nostra definizione; la frase indica comunemente la concatenazione di parole compresa fra un punto e l’altro di un testo, altrimenti detta proposizione. Nel senso della definizione che invece abbiamo dato, la frase coinciderebbe piuttosto con il testo intero, dall’inizio alla fine; una frase deve comprendere anche i segni di punteggiatura (punti, punti e virgola, due punti, etc.), i quali a tutti gli effetti fanno parte dell’insieme dei simboli del vocabolario.

Definizione 2.4 Un linguaggio formale è un insieme fissato (finito o infinito) di frasi costruite a partire da un vocabolario fissato V.

Un linguaggio formale è un sottoinsieme dell’insieme V^{∗}

L ⊆ V^{∗}  (2.3.3)

Dato un vocabolario, esistono diversi linguaggi formali che ne utilizzano i simboli; ognuno si differenzia dall’altro per la diversa scelta dell’insieme delle frasi che lo costituiscono.

2.4 RAPPRESENTAZIONE DEI LINGUAGGI FORMALI

Nel paragrafo precedente abbiamo visto che un linguaggio formale è un sottoinsieme di V^{∗}; esso può anche coincidere con l’insieme V^{∗} stesso, ma un tale linguaggio non presenta grande interesse.

Nel caso non coincida con l’insieme V^{∗} sarà necessario trovare una rappresentazione per esso.

Per linguaggi formali costituiti da un insieme finito di frasi, è possibile, ma spesso assolutamente sconveniente, utilizzare la rappresentazione estesa per gli insiemi; si possono cioè elencare tutte le frasi che compongono il linguaggio racchiudendole entro parentesi graffe:

L = {frase1, frase2, …}  (2.4.1)

La rappresentazione estesa è comunque esclusa nel caso di linguaggi costituiti da un numero infinito di frasi.

Il problema è dunque quello di trovare una rappresentazione finita del linguaggio, con la quale poter elencare le frasi che appartengono al linguaggio. In alcuni semplici casi potremmo utilizzare la classica rappresentazione finita degli insiemi, ottenuta specificando un set di condizioni da imporre sulle frasi; il linguaggio comprende le frasi per cui risultino verificate le condizioni. Ad esempio:

L_{1} = { x | x = a^{n}b^{n}, n ≥ 1 }

L_{1} è l’insieme le cui frasi sono termini matematici contenenti una stessa potenza dei simboli (del vocabolario) a e b.

Una tale rappresentazione non si adatta però a linguaggi più complessi, quali possono essere la lingua italiana o il linguaggio di programmazione FORTRAN; comunque può essere dimostrato che non tutti i linguaggi possiedono una rappresentazione del genere.

E’ necessario perciò definire un nuovo strumento con il quale possiamo essere in grado di rappresentare e descrivere i linguaggi. Nei prossimi paragrafi troveremo la soluzione al problema attraverso l’introduzione del concetto di grammatica formale che, come risultato degli studi svolti sull’argomento dagli inizi degli anni ’50 ad oggi, è risultato essere un conveniente strumento per la rappresentazione finita del linguaggio.

Il concetto di grammatica formale è centrale nella teoria dei linguaggi formali. Prima però di presentare la definizione matematica di grammatica formale, che, senza le dovute chiarificazioni, potrebbe risultare oscura, sarà bene cominciare a familiarizzare col concetto, osservando come esso non nasca poi dal nulla; infatti noi già conosciamo strumenti adatti a descrivere, sebbene non in modo matematicamente rigoroso, i linguaggi naturali: li chiamiamo grammatica e sintassi. La definizione di grammatica formale in qualche modo riprende questi concetti di grammatica e sintassi. Descrivendo grammatiche e sintassi per linguaggi naturali evidenzieremo analogie e differenze con le grammatiche per i linguaggi formali.

2.5 GRAMMATICA E SINTASSI DEI LINGUAGGI NATURALI

Nella nostra vita di tutti i giorni, per costruire le frasi attraverso le quali avvenga la comunicazione, utilizziamo in modo completamente automatico e senza averne coscienza gli strumenti grammatica e sintassi dei linguaggi naturali; in modo tanto automatico da averne dimenticato l’importanza. Proviamo perciò a trovare un esempio che ci rammenti l’essenzialità dei concetti di grammatica e sintassi e ci guidi nella successiva loro formalizzazione.

ogni 5 secondi cannoni da assedio sventrare spazio con un accordo tam-tuuumb ammutinamento di 500 echi per azzannarlo sminuzzarlo sparpagliarlo all’infinito

nel centro di quei tam-tuuumb spiaccicati (ampiezza 50 chilometri quadrati) balzare scoppi tagli pugni batterie tiro rapido Violenza ferocia regolarità questo basso grave scandere gli strani folli agitatissimi acuti della battaglia Furia affanno

orecchie occhi narici aperti attenti

…

Filippo Tommaso Marinetti: Bombardamento di Adrianopoli dal poema Zang Tumb Tumb

Lo studente che scrivesse qualcosa del genere nel suo compito di italiano, non riceverebbe di certo giudizi gratificanti. Il suo compito potrebbe essere rigettato come “privo di sintassi”. Del resto questo era proprio l’effetto che l’autore aveva volontariamente intenzione di ottenere, giacché altrove egli stesso afferma:

1. — BISOGNA DISTRUGGERE LA SINTASSI, DISPONENDO I SOSTANTIVI A CASO COME NASCONO.

dal Manifesto tecnico della letteratura futurista apparso sulla rivista Lacèrba, 1910

Naturalmente questa affermazione riflette l’animo in tumulto dell’uomo di lettere che l’ha espressa. L’uomo di scienza sarebbe molto più cauto. Osservando le opere del poeta egli, piuttosto che rassegnarsi ad affermare una completa assenza di sintassi, riuscirebbe comunque a rintracciare le regole di una qualche sintassi; una sintassi sicuramente diversa da quella tradizionale, ma pur sempre una valida sintassi. E non la negazione stessa di sintassi come invece vorrebbe il poeta.

Del resto Marinetti stesso contraddice i suoi veementi propositi di “paroliberismo” dedicando, senza accorgersene, i successivi punti del Manifesto tecnico proprio alla formulazione delle regole per questa nuova sintassi, nella quale il linguaggio risulta allo stesso modo rigidamente inquadrato. E tale sintassi definisce un nuovo linguaggio, costruito sullo stesso vocabolario della lingua italiana, ma sostanzialmente diverso dalla lingua italiana.

Questo esempio doveva servire a far luce sugli aspetti che possono essere ripresi in una formalizzazione dei concetti di grammatica e sintassi. Vorremmo spendere ancora due parole nella precisazione della differenza fra i termini grammatica e sintassi, a cui fino ad ora non abbiamo prestato grande attenzione.

Per i linguisti la grammatica indica lo studio morfologico di una lingua; ossia uno studio incentrato sulla forma che assumono le parole, relativamente alla loro struttura (radice, suffisso e desinenza). La grammatica è l’insieme delle regole che specificano come, ad esempio, dall’infinito di un verbo possano essere tratti tutti gli altri modi e tempi, o da un sostantivo come si passi da un genere all’altro. Generalmente nello studio grammaticale di una lingua vengono fornite le regole consuete di flessione verbale o nominale (o quant’altro), ed in seguito elencate le eccezioni alle regole.

La sintassi studia piuttosto i modi con cui le singole parole di una lingua si legano per formare frasi. L’attenzione viene spostata sulla struttura della frase. Ad esempio una valida struttura con cui costruire una frase può essere: soggetto, predicato verbale, complemento oggetto.

In una teoria formale dei linguaggi la distinzione fra grammatica e sintassi non è però così netta; infatti, benché in genere si preferisca definire il vocabolario di un linguaggio formale in modo da comprendervi tutti i simboli che possono occorrere in una frase del linguaggio, aggirando in questo modo il problema della flessione nominale o verbale di una parola ossia aggirando il problema che si pone la grammatica*, tuttavia uno studio grammaticale del linguaggio è fondamentale al successivo studio sintattico: di ogni termine di una frase ci è necessario conoscerne la rispettiva “valenza” grammaticale, la funzione precisa che esso assolve all’interno della frase; ossia se deve essere considerato articolo o sostantivo o verbo, e se verbo di quale modo, tempo o persona.

Dunque il concetto di grammatica formale che andremo ad introdurre nel prossimo paragrafo, in teoria dei linguaggi formali va inteso come un concetto che contiene in sé sia ciò che intendiamo per grammatica, sia ciò che intendiamo per sintassi; addirittura, in situazioni limite, risulta molto più vicino al concetto di sintassi di quanto non lo sia a quello di grammatica.

2.6 GRAMMATICHE FORMALI

Torniamo ora al nostro problema che era quello di trovare una conveniente rappresentazione finita con cui descrivere un linguaggio formale.

Chiamiamo V_{T} il vocabolario, finito e non vuoto, di un linguaggio formale; V_{T} è altresì detto insieme dei simboli terminali.

Sia ora V_{N} un altro insieme di simboli, detti simboli non-terminali; vedremo più avanti che V_{N} può essere considerato come il vocabolario di un metalinguaggio, ossia di un linguaggio usato per descrivere un altro linguaggio.

Gli insiemi V_{T} e V_{N} siano disgiunti (senza elementi in comune). Sia ora S un particolare elemento di V_{N}, detto simbolo di partenza.

Definizione 2.5 Una grammatica formale è una quadrupla

G = (V_{N}, V_{T} , S, Φ)  (2.6.1)

ove Φ è un insieme di elementi (α, β), Φ = { (α, β) | α ∈ (V_{T} ∪ V_{N})^{∗}V_{N}(V_{T} ∪ V_{N})^{∗}; β ∈ (V_{T} ∪ V_{N})^{∗} }. Gli elementi (α, β) di Φ sono detti regole di produzione, e usualmente indicati con la notazione α → β.

Le regole di produzione di una grammatica sono tali per cui a sinistra della freccia → compare sempre almeno un simbolo di V_{N}, opzionalmente seguito o preceduto da un numero qualsiasi di altri simboli appartenenti sia a V_{N} sia a V_{T}. Volendo fare il caso, come vedremo, di grammatiche particolarmente semplici, a sinistra porremo niente altro oltre il simbolo di V_{N} (come dire che esso sia seguito e preceduto da frasi nulle). A destra della freccia può comparire una qualsiasi frase composta di simboli di V_{N} e di V_{T}, compresa la frase nulla.

Definizione 2.6 Siano σ, ψ ∈ (V_{T} ∪ V_{N})^{∗} due frasi costruite sull’insieme dei simboli terminali e nonterminali. σ è detta essere derivazione diretta di ψ, scrivendo ψ ⇒ σ, se esistono due frasi ϕ_{1}, ϕ_{2} ∈ (V_{T} ∪ V_{N})^{∗}, tali che ψ = ϕ_{1}αϕ_{2} e σ = ϕ_{1}βϕ_{2}, e se α → β è una regola di produzione appartenente a Φ.

Attenzione al fatto che le frasi ψ, σ, ϕ_{1}, ϕ_{2}, come anche quelle chiamate prima α, β, in generale sono composte di simboli di V_{T} e anche di V_{N}; non hanno niente a che vedere con le frasi del linguaggio formale che stiamo tentando di descrivere attraverso la grammatica; le frasi del linguaggio formale sono infatti composte di simboli appartenenti al solo V_{T}.

* Dobbiamo sapere che per una macchina fa poca differenza ricordare tutte le 48×2000 forme verbali coniugate di una lingua come il francese, al posto delle 2000 forme all’infinito; al contrario, per un uomo è più facile memorizzare i 2000 verbi all’infinito e ricostruire poi le altre forme sulla base di un modello di flessione verbale, lasciandosi spesso guidare dal suo “senso estetico” nell’accorgersi dei punti ove il modello fallisce (iati o incontri consonantici considerati sgradevoli).

Definizione 2.7 La frase σ è derivazione della frase ψ (o anche ψ produce σ, o σ si riduce a ψ) scrivendo

ψ ⇒^{∗} σ, se esistono le frasi ϕ_{0}, ϕ_{1}, …, ϕ_{n−1}, ϕ_{n}, tali che: ψ = ϕ_{0} ⇒ ϕ_{1}, ϕ_{1} ⇒ ϕ_{2}, …, ϕ_{n−1} ⇒ ϕ_{n} = σ

A questo punto siamo in grado di fornire la rappresentazione di un linguaggio formale attraverso la grammatica ad esso associata (o più esattamente, attraverso una delle grammatiche ad esso associate). Definizione 2.8 Il linguaggio L(G) generato dalla grammatica G, è l’insieme delle frasi costruite sul vocabolario dei simboli terminali V_{T}, le quali siano derivazione del simbolo di partenza S:

L(G) = { σ | σ ∈ V_{T}^{∗}, S ⇒^{∗} σ }  (2.6.2)

Immaginiamo ad esempio di avere il seguente vocabolario:

        V_{T} = {il, un, cane, gatto, topo, mangia, uccide}

sul quale vogliamo costruire un semplice linguaggio, costituito di frasi dalla struttura: soggetto, predicato verbale, complemento oggetto.

A tal scopo definiamo un insieme di simboli non-terminali V_{N}:

V_{N} = {SOGGETTO, PREDICATO VERBALE, COMPLEMENTO OGGETTO, SOSTANTIVO, ARTICOLO}

A V_{N} aggiungiamo inoltre, il simbolo PROPOSIZIONE, a cui conferiamo la valenza di simbolo di partenza.

Alla definizione completa di una grammatica formale ora manca solo la definizione dell’insieme delle regole di produzione Φ; provvediamo immediatamente ponendo:

Φ ={

PROPOSIZIONE → SOGGETTO PREDICATO VERBALE COMPLEMENTO OGGETTO,
SOGGETTO → ARTICOLO SOSTANTIVO,
COMPLEMENTO OGGETTO → ARTICOLO SOSTANTIVO,
ARTICOLO → il,
ARTICOLO → un,
SOSTANTIVO → cane,
SOSTANTIVO → gatto,
SOSTANTIVO → topo,
PREDICATO VERBALE → mangia,
PREDICATO VERBALE → uccide

} Ora che abbiamo una grammatica formale (chiamiamola G_{1}) correttamente definita, vediamo qual’è il linguaggio “generato” da tale grammatica. Secondo la definizione che ne abbiamo dato, L(G) è l’insieme delle frasi σ che sono derivazione del simbolo di partenza. Prendiamo ad esempio la frase:

                                 il gatto mangia un topo

Essa appartiene ad L(G_{1}), poiché è composta di soli simboli scelti in V_{T} e poiché possiamo trovare una sequenza di frasi ϕ_{0},…,ϕ_{n} costruite sull’insieme (V_{T} ∪ V_{N}), l’una derivazione diretta dell’altra, in grado di connettere agli estremi opposti il simbolo di partenza S = PROPOSIZIONE e la frase il gatto mangia un

topo:
             PROPOSIZIONE ⇒ SOGGETTO PREDICATO VERBALE COMPLEMENTO OGGETTO ⇒
              ⇒ ARTICOLO SOSTANTIVO PREDICATO VERBALE COMPLEMENTO OGGETTO ⇒
              ⇒ il SOSTANTIVO PREDICATO VERBALE COMPLEMENTO OGGETTO ⇒
              ⇒ il gatto PREDICATO VERBALE COMPLEMENTO OGGETTO ⇒
              ⇒ il gatto mangia COMPLEMENTO OGGETTO ⇒
              ⇒ il gatto mangia ARTICOLO SOSTANTIVO ⇒
              ⇒ il gatto mangia un SOSTANTIVO ⇒
              ⇒ il gatto mangia un topo

Nella derivazione si può controllare siano state utilizzate solo regole di produzione appartenenti a Φ.

Anche le frasi:

                                 il gatto uccide il topo
                                 il cane uccide il gatto

evidentemente appartengono al linguaggio L(G_{1}) per gli stessi motivi.

Come si vede, è stata trovata un’altra maniera di descrivere un linguaggio che non sia quella di elencare tutte le frasi dalle quali è costituito. In questo caso particolare il linguaggio era finito (infatti comprendeva in totale 72 frasi); per cui alla descrizione del linguaggio attraverso la grammatica quasi avremmo preferito l’elenco delle 72 frasi.

Vediamo ora un esempio di linguaggio più vicino alla struttura di un comune linguaggio di programmazione, che, pur mantenendo una sostanziale semplicità a scopi dimostrativi, risulterà composto da un numero infinito di frasi. Per cui la sua rappresentazione attraverso una grammatica sarà difficilmente sostituibile. Immaginiamo di voler costruire un linguaggio le cui frasi siano tutte le possibili espressioni matematiche in cui compaiano prodotti e somme dei simboli a, b, …, z. La grammatica per tale linguaggio sarà la quadrupla:

G = {V_{T} , V_{N}, S, Φ}

V_{T} = {+, ∗, (,), a, b, …, z}

                           V_{N} = {ESPRESSIONE, TERMINE, FATTORE}
                           S = ESPRESSIONE

Φ = {

                             ESPRESSIONE → TERMINE
                             ESPRESSIONE → ESPRESSIONE + TERMINE
                             TERMINE → FATTORE
                             TERMINE → TERMINE ∗ FATTORE
                             FATTORE → (ESPRESSIONE)
                             FATTORE → a
                             FATTORE → b

⋯

                             FATTORE → z

} La differenza rilevante rispetto al precedente esempio, sta nell’uso della tecnica della ricorsività nella definizione dell’insieme delle regole di produzione Φ. La tecnica della ricorsività consiste nel far comparire uno stesso simbolo non-terminale a sinistra e a destra del simbolo →. Essa incorre ad esempio nella regola:

                                TERMINE → TERMINE ∗ FATTORE

una tale regola è in grado di produrre frasi a lunghezza qualsiasi. Grazie ad una ripetuta applicazione di essa infatti, possiamo produrre ad esempio la frase:

a ∗ t ∗ a ∗ b

come dimostreremo:

                               ESPRESSIONE ⇒ TERMINE ⇒
                                ⇒ TERMINE ∗ FATTORE ⇒
                                ⇒ TERMINE ∗ b ⇒
                                ⇒ TERMINE ∗ FATTORE ∗ b ⇒
                                ⇒ TERMINE ∗ a ∗ b ⇒
                                ⇒ TERMINE ∗ FATTORE ∗ a ∗ b ⇒

⇒ TERMINE ∗ t ∗ a ∗ b ⇒ ⇒ FATTORE ∗ t ∗ a ∗ b ⇒ ⇒ a ∗ t ∗ a ∗ b

Ed è chiaro che possiamo aggiungere quanti altri fattori vogliamo in coda, allungando così a piacere l’espressione. E’ garantito che la grammatica sarà sempre in grado di generarla.

Se al posto della regola TERMINE → TERMINE ∗ FATTORE avessimo usato una regola non ricorsiva come ad esempio:

                                TERMINE → FATTORE ∗ FATTORE

la grammatica non sarebbe stata in grado di riconoscere prodotti a più di due operandi.

Naturalmente la grammatica che abbiamo definito è in grado di riconoscere oltre a prodotti a numero qualsiasi di operandi, anche somme o espressioni comunque complicate, coinvolgenti un numero qualsiasi di livelli di inclusione entro parentesi. Come esempio dimostreremo che la frase:

a ∗ (c ∗ b + d)

appartiene al linguaggio L(G_{2}) mostrando, a differenza dei casi precedenti, il processo di riduzione della frase al simbolo di partenza S, piuttosto che quello di produzione della frase dal simbolo di partenza:

a ∗ (c ∗ b + d) ⇐^{∗}

                         ⇐^{∗} FATTORE ∗ (FATTORE ∗ FATTORE + FATTORE) ⇐
                         ⇐ FATTORE ∗ (TERMINE ∗ FATTORE + FATTORE) ⇐
                         ⇐ FATTORE ∗ (TERMINE + FATTORE) ⇐
                         ⇐ FATTORE ∗ (TERMINE + TERMINE) ⇐
                         ⇐ FATTORE ∗ (ESPRESSIONE + TERMINE) ⇐
                         ⇐ FATTORE ∗ (ESPRESSIONE) ⇐
                         ⇐ TERMINE ∗ (ESPRESSIONE) ⇐
                         ⇐ TERMINE ∗ FATTORE ⇐
                         ⇐ TERMINE ⇐
                         ⇐ ESPRESSIONE

Il processo di riduzione è quello seguito in realtà dalle macchine il cui compito è il riconoscimento del linguaggio.

Per terminare poniamo attenzione al fatto che la corrispondenza fra linguaggi formali e grammatiche non è biunivoca; nel senso che ad uno stesso linguaggio possono corrispondere diverse grammatiche; uno stesso linguaggio può essere generato da diverse grammatiche. Ove possibile, si tende a descrivere un linguaggio con la più “semplice” grammatica che sia in grado di generarlo. Questo perché orientativamente ad una grammatica semplice corrisponde un meccanismo di riconoscimento del linguaggio oggettivamente più semplice. Nel prossimo paragrafo spiegheremo cosa debba intendersi per “grammatica semplice”.

2.7 CLASSIFICAZIONE DELLE GRAMMATICHE

La definizione che abbiamo dato di grammatica formale è alquanto generale. E’ possibile imporre ulteriori restrizioni alla definizione, ottenendo grammatiche dal comportamento semplificato. A seconda della restrizione imposta si ottiene una differente classe di grammatiche. Presenteremo in questa sezione la classificazione di Chomsky delle grammatiche.

Alla prima classe, detta delle unrestricted grammars appartengono le grammatiche per cui vale la definizione generale senza ulteriori restrizioni.

La seconda classe è costituita da grammatiche verificanti, oltre le prescrizioni della definizione generale, anche la condizione che le regole di produzione della grammatica α → β siano tali che ∥α∥ ≤ ∥β∥.

Tale restrizione impedisce che β sia la frase nulla. Inoltre fa sì che il processo di produzione di una frase σ del linguaggio a partire dal simbolo iniziale S, termini in un numero finito di passi. Infatti, il processo di produzione può solo passare per frasi che sono sempre l’una più lunga della precedente (o al più uguale); se quindi nel corso del processo di produzione da S si fosse giunti ad una frase ϕ_{i} più lunga di σ, saremmo certi che quel processo non può in nessun caso condurre alla frase finale σ. E dunque un processo di produzione non può passare per frasi intermedie di lunghezza grande qualsivoglia per poi riconvergere alla frase finale σ (Chomsky 1963).

Un esempio di linguaggio generabile da una grammatica della seconda classe, è:

L_{2} = { a^{n}b^{n}c^{n}∥n ≥ 1 }

per il quale può essere stabilita la grammatica a regole di produzione:

                                       S → a S B C
                                       S → a B C

b B → b b
b C → b c

                                       C B → B C

c C → c c

Alla terza classe, detta delle grammatiche context-sensitive, appartengono tutte la grammatiche verificanti l’ulteriore condizione che le regole di produzione siano della forma χ_{1}Aχ_{2} → χ_{1}ωχ_{2}, con A singolo simbolo non-terminale, ω frase nonnulla, χ_{1}, χ_{2} frasi qualsiasi, opzionalmente nulle.

Il confronto della classe delle grammatiche context-sensitive con la precedente appare più chiaro se riscriviamo la condizione α → β, ∥α∥ ≤ ∥β∥, sotto la forma equivalente α = χ_{1}ψχ_{2} e β = χ_{1}ωχ_{2}, con χ_{1}, χ_{2} frasi qualsiasi (anche frasi nulle) e ∥ψ∥ ≤ ∥ω∥; da questo emerge che le grammatiche context-sensitive risultano nella semplificazione delle grammatiche della classe precedente per cui la frase ψ sia costituita da un singolo simbolo non-terminale.

L’applicazione di una regola di produzione χ_{1}Aχ_{2} → χ_{1}ωχ_{2}, riscrive A come ω solo nel contesto fra χ_{1} e χ_{2}; notiamo che la regola A → ω non è equivalente alla χ_{1}Aχ_{2} → χ_{1}ωχ_{2}, poiché grazie ad essa A può essere riscritta come ω sempre, e non limitatamente al caso in cui compare all’interno del contesto χ_{1}, χ_{2}.

Imponendo l’ulteriore restrizione per cui le regole di produzione α → β di una grammatica, oltre ad essere tali che ∥α∥ ≤ ∥β∥, per esse valga anche la α ∈ V_{N}, otteniamo la classe delle grammatiche context-free. In esse le regole di produzione contengono nella parte sinistra un solo singolo simbolo non-terminale; per cui esse possono essere applicate ovunque, indipendentemente dal contesto in cui il simbolo non-terminale del lato sinistro, compare.

Un esempio di linguaggio a grammatica context-free è:

L_{3} = { a^{n}ba^{n}∥n ≤ 1 }

generabile attraverso le semplici regole di produzione:

\begin{matrix}S → a B a \\ B → a B a \\ B → b \end{matrix}

Grammatiche context-free non sono in grado di rappresentare parti significative della lingua italiana, dato che la dipendenza dal contesto è spesso richiesta per analizzare propriamente la struttura di un periodo.

In gran parte la sintassi dei linguaggi di programmazione può essere descritta attraverso grammatiche context-free; questo grazie alla loro sostanziale semplicità strutturale. Tuttavia alcune parti dei linguaggi di programmazione non possono essere descritte con grammatiche context-free; per fare un esempio, l’uso dell’identificatore di una variabile all’interno di uno statement del linguaggio dipende dal contesto, dipende cioè dal fatto se tale identificatore della variabile è stato in precedenza dichiarato. Come altro esempio, il numero e il tipo dei parametri che compaiono nello statement di chiamata di una subroutine dipendono dalla precedente dichiarazione del prototype della subroutine.

Esiste infine una quinta classe di grammatiche, detta delle regular grammars. Una grammatica regolare contiene solo regole di produzione del tipo α → β, dove ∥α∥ ≤ ∥β∥, α ∈ V_{N} e β è della forma β = aB, dove a ∈ V_{T} e B ∈ V_{N}. Sono delle grammatiche molto semplici per cui vale la proprietà che in un processo di derivazione delle frasi del linguaggio a partire dal simbolo S, le successive frasi ϕ_{0}, ϕ_{n} in cui si svolge il processo stesso, incrementano la loro lunghezza ad ogni passo di 1.

Ecco un linguaggio a grammatica regolare:

L_{4} = { a^{n}ba^{m}∥n, m ≥ 1 }

                                         S → a S
                                         S → a B
                                         B → b C
                                         C → a C

C → a

Se indichiamo con T_{1}, T_{2}, T_{3}, T_{4}, T_{5}, rispettivamente gli insiemi di tutte le possibili grammatiche di classe 1, 2, etc., è evidente che vale la:

T_{1} ⊃ T_{2} ⊃ T_{3} ⊃ T_{4} ⊃ T_{5}  (2.7.1)

Non è altrettanto evidente che, indicato con L(T_{i}) l’insieme di tutti i linguaggi che è possibile generare con grammatiche di classe i, valgano le:

L(T_{2}) = L(T_{3})  (2.7.2)
L(T_{1}) ⊃ L(T_{3}) ⊃ L(T_{4}) ⊃ L(T_{5})  (2.7.3)

grazie alla quale si stabilisce una gerarchia fra le classi delle grammatiche. E’ chiaro che valendo la (2.7.1), resta comunque dimostrato che vale la:

L(T_{1}) ⊇ L(T_{2}) ⊇ L(T_{3}) ⊇ L(T_{4}) ⊇ L(T_{5})  (2.7.4)

Per dimostrare completamente la (2.7.3) basterebbe dimostrare che per ogni classe grammaticale esiste almeno una grammatica in grado di generare un linguaggio non altrimenti generabile da una grammatica della classe inferiore.

Dimostrare la (2.7.2) è invece molto più complicato. In realta si dimostra che la (2.7.2) è una equivalenza debole, ossia per ogni grammatica G di classe 2 esiste almeno una G′ di classe 3 in grado di generare lo stesso linguaggio ma non le stesse strutture sul linguaggio (vedremo in seguito cosa sono le strutture di un linguaggio). Potremmo mostrare come sia possibile generare attraverso una grammatica context-sensitive il linguaggio L_{2} che fornimmo come esempio di linguaggio della seconda classe. Di tutte le regole di produzione che proponemmo per la grammatica di L_{2}, l’unica che non obbedisce alle restrizioni richieste dalla classe delle grammatiche context-sensitive, pare essere la:

                                        C B → B C

Ma al suo posto, con risultato assolutamente identico, potremmo considerare le tre equivalenti regole contextsensitive:

                                        C B → C C′ ; C C′ → B C′ ; B C′ → B C

Una volta generato il contesto CB, il processo di produzione non può procedere altrimenti che giungendo a

BC.

Forniremo in appendice la dimostrazione della più semplice delle (2.7.3), rimandando per le altre ai testi in bibliografia (Chomsky 1963). L’importanza delle (2.7.2) e (2.7.3) sta nel fatto che, ad esempio, la seconda classe di grammatiche ha perso del tutto interesse ai nostri occhi: abbiamo infatti intrapreso uno studio delle grammatiche non per se stesso, ma perché finalizzato ai linguaggi che esse generano; se il risultato prodotto da due differenti classi di grammatiche è lo stesso, ci sforzeremmo di definire i linguaggi attraverso la classe grammaticale più semplice. Per capire questo punto è necessario si sappia che in corrispondenza ad ogni classe grammaticale c’è una classe di macchine in grado di “accettare” i linguaggi generati dalle grammatiche della classe; più è basso l’ordine gerarchico della classe grammaticale, più risulta semplice trovare gli algoritmi di funzionamento della macchina.

2.8 GRAMMATICHE CONTEXT-FREE

Dedicheremo un intero paragrafo alla classe delle grammatiche context-free perché per esse valgono importanti proprietà laddove le classi superiori presentano complicazioni. Vediamo ad esempio:

Teorema 2.1 L’insieme dei linguaggi context-free (l’insieme dei linguaggi cioè che è possibile generare attraverso grammatiche context-free) è chiuso sotto le operazioni di unione, riflessione, prodotto, chiusura infinita (Bar-Hillel, Perles, Shamir, 1960).

L_{3} = L_{1} ∪ L_{2}, L_{1}, L_{2} ∈ L(T_{CF}) ⇒ L_{3} ∈ L(T_{CF})  (2.8.1)
L_{REFL} = { a_{1}a_{2} ⋯ a_{n} | a_{n} ⋯ a_{1} ∈ L }, L ∈ L(T_{CF}) ⇒ L_{REFL} ∈ L(T_{CF})  (2.8.2)
L_{3} = L_{1} · L_{2} = { x | x = yz, y ∈ L_{1}, z ∈ L_{2} }, L_{1}, L_{2} ∈ L(T_{CF}) ⇒ L_{3} ∈ L(T_{CF})  (2.8.3)
L = ⋃_{n} L^{n}, L^{n} ∈ L(T_{CF}) ⇒ L ∈ L(T_{CF})  (2.8.4)

Grazie a questa e ad altre proprietà che sono state trovate, diventa possibile lavorare agevolmente sui linguaggi context-free.

A seconda del “tipo” delle regole di produzione contenute nell’insieme Φ di una grammatica contextfree, è possibile operare una sottoclassificazione all’interno della classe context-free stessa; per le sottoclassi possono poi essere stabilite ulteriori proprietà. Ad esempio si dimostra che grammatiche context-free senza regole di produzione del tipo A → B (A e B singoli non-terminali) sono equivalenti a grammatiche contextfree che invece ne sono in possesso.

Di fatto una sottoclassificazione è possibile sulla base della seguente distinzione fra i tipi delle regole di produzione: chiamiamo A → xBy (B unico simbolo non-terminale a destra) regola di produzione lineare; una regola di produzione lineare può essere a sua volta lineare destra (A → xB) o lineare sinistra (A → By). Chiamiamo A → x (x contenente solo simboli terminali) regola terminante.

Grammatiche context-free potranno così essere lineari (tutte le regole di produzione non-terminanti sono lineari), unilatere destre o sinistre (tutte le non-terminanti sono lineari destre o sinistre), metalineari, normali, sequenziali. Per esse può poi essere stabilita una gerarchia analoga a quella che fornimmo per le classi fondamentali delle grammatiche (Schützenberger, Chomsky, 1962).

I seguenti teoremi sono stati fondamentali a decidere del successo delle grammatiche context-free quali validi strumenti per descrivere linguaggi:

Teorema 2.2 (Decidibility) Esiste un algoritmo per determinare, data una grammatica context-free G, se il linguaggio L(G) da essa generato è vuoto, finito o infinito (Bar-Hillel, Perles, Shamir, 1960).

Per le grammatiche di classe superiore vale un analogo teorema, ma di senso contrario (Undecidibility).

Teorema 2.3 Dato un linguaggio context-free L, possiamo costruire un automa a push down storage (PDS) a controllo ristretto in grado di accettare L (Chomsky, 1963).

Per comprendere completamente l’importanza dell’ultimo teorema dovremmo affrontare almeno nei presupposti essenziali la teoria degli automi, cosa che però ci porterebbe abbastanza fuori argomento. Essenzialmente basti dire che un automa è una macchina in grado di reagire agli stimoli esterni in un modo che dipende anche da un suo stato interno; gli automi PDS reagiscono, oltre agli stimoli provenienti dall’esterno, anche a quelli provenienti da uno stack sul quale essi stessi hanno riposto dati.

Nel nostro caso, un automa può servire a leggere un testo e capire se esso appartiene ad un linguaggio (il linguaggio per cui esso è stato espressamente costruito); può cioè essere utile nell’analisi della correttezza sintattica del testo (fase di parsing). In più l’automa può anche servire a guidare la traduzione del significato (fase semantica); ad esempio, nel caso l’automa sia stato costruito per comportarsi come compilatore del linguaggio, guida la produzione del codice oggetto.

Più che una tradizionale macchina a congegni meccanici o elettronici, l’automa che dovrebbe servire a leggere e a tradurre il linguaggio potrebbe essere un programma per calcolatori detto parser.

Il parser possiede delle tavole sintattiche che lo istruiscono sul linguaggio che deve accettare; in sostanza le tavole sintattiche riflettono le regole di produzione e in generale l’intera grammatica in grado di generare il linguaggio.

Sulla base delle tavole sintattiche il parser interpreta gli stimoli che provengono dall’esterno (un file o il flusso di dati proveniente dalla tastiera in caso di sessione interattiva).

Riprenderemo questi argomenti e ne discuteremo altri quando si parlerà del parser utilizzato per l’APESE.

Nel seguito del capitolo, a meno che non specificato diversamente, faremo riferiremo esclusivamente a grammatiche context-free; questo in virtù delle proprietà di cui esse godono e che le fanno apparire interessanti.

2.9 SEMANTICA

Un linguaggio è il mezzo attraverso cui possono essere scambiate informazioni. Una grammatica si occupa solo della correttezza sintattica con cui tali informazioni sono scambiate; si disinteressa assolutamente del significato di tali informazioni. Il significato delle informazioni scambiate riguarda piuttosto la semantica.

Esistono programmi FORTRAN senza alcun senso logico, ma corretti dal punto di vista sintattico; per cui il compilatore li accetta senza mostrare i minimi segni di protesta.

Ma del resto una situazione analoga sussiste anche per i linguaggi naturali: un racconto kafkiano è ineccepibile per quanto riguarda la sua correttezza grammaticale e sintattica, talvolta assurdo o se vogliamo discutibile per quanto riguarda il contenuto che vuole comunicare.

La teoria dei linguaggi formali potrebbe rispondere a questo punto, solo con la “esasperazione” del concetto di grammatica, portando cioè la grammatica a livelli di complicazione tali da escludere dal linguaggio che essa genera le frasi senza senso; oltre un certo limite però non sembra possibile andare. Comunque nel seguito considereremo come frasi a pieno diritto del linguaggio, tutte quelle corrette sintatticamente rispetto alle regole della grammatica, siano esse sensate o assurde.

Spesso compilatori ben congegnati sono in grado di avvertire (nella fase di “traduzione semantica” del linguaggio) la presenza di alcune costruzioni linguistiche che hanno poco senso, e di produrre in tal caso dei messaggi di avvertimento; ma la compilazione è autorizzata ad interrompersi solo nel caso venga riscontrato un errore sintattico, non altrimenti.

2.10 ALBERI SINTATTICI E PARSING

Lo scopo di una grammatica non è soltanto quello di determinare l’insieme delle frasi che costituiscono il linguaggio. L’importanza di una grammatica risiede anche nel fatto che essa impone una struttura o “parse” alle frasi del linguaggio che essa genera. La struttura grammaticale è in seguito analizzata, allo scopo di correttamente comprendere il significato della frase.

Uno stesso linguaggio può essere generato da grammatiche diverse, le quali impongono strutture diverse sulle frasi di esso; alcune strutture potranno, da un punto di vista semantico, essere corrette, altre invece potranno essere errate e condurre a interpretazioni non valide delle frasi del linguaggio.

Non è possibile a questo punto mantenere netta la separazione fra sintassi e semantica come semplificativamente era stato imposto finora.

Introduciamo ora uno strumento utile che chiaramente mostra l’importanza della struttura grammaticale nella comprensione di una frase: gli alberi sintattici.

Gli alberi sintattici, anche detti alberi di parsing, sono dei grafici nei quali ritroviamo descritto, direi quasi in maniera statica, il processo di produzione di una frase dal simbolo di partenza. Hanno però il vantaggio che da essi emerge più chiaramente la struttura della frase nei termini delle classi sintattiche in cui è possibile scomporla; ove per classi sintattiche possiamo pensare i simboli non-terminali con i quali abbiamo costruito la grammatica.

Un albero sintattico è costituito di nodi e di rami. In corrispondenza ad ogni nodo vi è un simbolo terminale o non-terminale. In ogni albero troviamo distinto un particolare nodo, detto radice; è sempre marcato dal simbolo di partenza della grammatica. In genere il nodo radice è quello posto più in alto o quello più in basso.

All’estremo opposto della radice troviamo nodi detti foglie, ciascuno in corrispondenza ad un simbolo terminale; l’insieme dei nodi foglia, nel giusto ordine da sinistra a destra, rappresenta la frase che è in fase di “strutturazione” o “parsing”.

La figura 2.1 mostra l’albero sintattico relativo alla frase:

                                 il gatto mangia il topo

costruito seguendo le regole di produzione che fornimmo per l’esempio della grammatica G_{1}.

Dalla radice dipartono diversi rami; ciascuno raggiunge un nodo che rappresenta il simbolo contenuto nella parte destra di una delle possibili regole di produzione S → ABC … della grammatica; in particolare

figura 2.1. Albero sintattico

troveremo i soli simboli a destra della regola di produzione effettivamente responsabile, in ultima analisi, della produzione della frase che è in fase di parsing (nel nostro esempio, i simboli dell’unica regola PROPOSIZIONE →

SOGGETTO PREDICATO VERBALE COMPLEMENTO OGGETTO con cui ci si può ridurre al simbolo PROPOSIZIONE).

Dai nodi successivi alla radice dipartono altri rami che sviluppano l’albero secondo altre regole della grammatica; il processo continua finché non vengono raggiunti i nodi foglia.

Gli alberi sintattici sono significativi solo nel caso di grammatiche context-free; per grammatiche di classe superiore sono possibili rappresentazioni grafiche del processo di parsing, ma esse appaiono notevolmente meno significative.

Nel caso di grammatiche context-free, ogni nodo intermedio dell’albero può essere considerato come la radice di di un sottoalbero; questo in virtù del fatto che nella parte sinistra delle regole di produzione compaiono solo singoli simboli non-terminali, per cui è impossibile che diverse sottoparti dell’albero vengano di nuovo in connessione fra di loro. E’ altresì vero che i nodi intermedi possono essere in relazione ai soli simboli non-terminali, mentre necessariamente simboli terminali costituiscono i nodi foglia; in essi infatti il processo di produzione si arresta, non esistendo regole di produzione nelle quali essi compaiano a sinistra.

Dalla figura 2.1 mostrata, ciascuno, analizzando i diversi “livelli” dell’albero e i sottoalberi che traggono origine dai nodi non-terminali, avrà una visione immediata delle relazioni strutturali esistenti fra le diverse parti della frase.

Nell’esempio, il livello dell’albero più significativo ai fini della comprensione della frase è certamente il secondo (SOGGETTO PREDICATO VERBALE COMPLEMENTO OGGETTO). Nel sottoalbero che ha origine dal nodo SOGGETTO troviamo il terminale gatto che dobbiamo pensare quindi, nel momento in cui andiamo ad interpretare il significato della frase, come l’“esecutore” di una qualche “azione”; a questa interpretazione siamo condotti dalla presenza di un nodo PREDICATO VERBALE; l’interpretazione sarebbe stata diversa se al suo posto avessimo trovato un nodo PREDICATO NOMINALE. Ovviamente il terminale topo che troviamo nel sottoalbero di COMPLEMENTO OGGETTO, dovrà essere colui che subirà l’azione. Il livello ARTICOLO SOSTANTIVO, dei sottoalberi SOGGETTO e COMPLEMENTO OGGETTO è certamente meno significativo. Nel caso avessimo voluto aggiungere al nostro semplice linguaggio, sottoinsieme della lingua italiana, anche costruzioni con forme verbali passive, alla comprensione del significato della frase sarebbe stato necessario anche il livello successivo al nodo PREDICATO VERBALE, nel quale sarebbe stata specificata una costruzione a FORMA ATTIVA o una a FORMA PASSIVA; il ruolo “semantico” di SOGGETTO e COMPLEMENTO sarebbe nei due casi scambiato.

L’albero sintattico della figura 2.1 corrisponde alla seguente derivazione della frase dal simbolo di partenza:

PROPOSIZIONE ⇒ SOGGETTO PREDICATO VERBALE COMPLEMENTO OGGETTO ⇒
⇒ ARTICOLO SOSTANTIVO PREDICATO VERBALE COMPLEMENTO OGGETTO ⇒
⇒ il SOSTANTIVO PREDICATO VERBALE COMPLEMENTO OGGETTO ⇒
⇒ il gatto PREDICATO VERBALE COMPLEMENTO OGGETTO ⇒
⇒ il gatto mangia COMPLEMENTO OGGETTO ⇒
⇒ il gatto mangia ARTICOLO SOSTANTIVO ⇒
⇒ il gatto mangia il SOSTANTIVO ⇒
⇒ il gatto mangia il topo

Ma anche alla derivazione:

PROPOSIZIONE ⇒ SOGGETTO PREDICATO VERBALE COMPLEMENTO OGGETTO ⇒
⇒ SOGGETTO PREDICATO VERBALE ARTICOLO SOSTANTIVO ⇒
⇒ SOGGETTO PREDICATO VERBALE ARTICOLO topo ⇒
⇒ SOGGETTO PREDICATO VERBALE il topo ⇒
⇒ SOGGETTO mangia il topo ⇒
⇒ ARTICOLO SOSTANTIVO mangia il topo ⇒
⇒ ARTICOLO gatto mangia il topo ⇒
⇒ il gatto mangia il topo

In generale ad ogni albero sintattico possono corrispondere più derivazioni. Le due derivazioni mostrate sono assolutamente particolari: nella prima infatti, di ogni frase intermedia ϕ_{i} viene sviluppato sempre il simbolo non-terminale presente il più a sinistra, così fino alla frase di arrivo, quella costituita di soli simboli terminali; nella seconda si segue il criterio opposto, sviluppando sempre il simbolo non-terminale più a destra.

Definizione 2.9 Definiamo derivazione diretta canonica da sinistra, scrivendo ψ ⇒_{L} σ, la derivazione ψ = ϕ_{1}Aϕ_{2} ⇒ ϕ_{1}βϕ_{2} = σ ove A → β è una regola di produzione della grammatica, e ϕ_{1} ∈ V_{T}^{∗}. Altresì definiamo derivazione canonica da sinistra ψ ⇒_{L}^{∗} σ, una successione di derivazioni dirette canoniche da sinistra ψ ⇒_{L} ϕ_{1} ⇒_{L} ϕ_{2} ⇒_{L} … ⇒_{L} ϕ_{n} ⇒_{L} σ, leggendo “ ψ produce da sinistra σ”.

Per ogni frase del linguaggio σ ∈ V_{T}^{∗}, esisterà una sola sequenza di stringhe ϕ_{1}, ϕ_{2}, …, ϕ_{n}, nelle quali si realizzi il processo di produzione da sinistra a partire dal simbolo di partenza S; infatti allorché una ϕ_{i} contenga più di un simbolo non-terminale, per cui ci troveremmo di fronte alla scelta su quale riscrivere dapprima, la relazione ⇒_{L}^{∗} impone di riscrivere sempre quello più a sinistra; la frase ϕ_{i+1} è così determinata univocamente.

Definizione 2.10 Allo stesso modo possiamo definire una derivazione diretta canonica da destra ψ ⇒_{R} σ, ψ = ϕ_{1}Aϕ_{2}, σ = ϕ_{1}βϕ_{2}, nel caso in cui A → β è una regola della grammatica e ϕ_{2} ∈ V_{T}^{∗}. La derivazione canonica da destra ψ ⇒_{R}^{∗} σ, sarà una successione di derivazioni dirette canoniche da destra.

Possiamo considerare l’introduzione della relazione ⇒_{L}^{∗} o ⇒_{R}^{∗} come la definizione di un criterio per sviluppare ordinatamente i processi di produzione delle frasi del linguaggio. Quello che conta è infatti la struttura tracciata dall’albero sintattico; non l’ordine seguito per tracciare le singole parti dell’albero (cioè se siano stati tracciati prima alcuni sottoalberi invece di altri).

Passiamo ora al problema di trovare il modo con cui, data una grammatica ed una frase del linguaggio, costruire l’albero di parsing o anche, il che è equivalente, la sequenza delle frasi ϕ_{i} che costituiscono il processo di produzione della frase a partire dal simbolo di partenza S. Esistono molti modi con cui realizzare tale costruzione.

Il processo di parsing top-down costruisce l’albero partendo dalla radice e procedendo verso il basso fino a raggiungere le foglie. Il primo passo consiste nel tentare di riscrivere il simbolo di partenza S (la radice) secondo una delle regole di produzione in cui esso compare a sinistra.

Introduciamo un esempio. Sia G_{4} la grammatica il cui linguaggio generato è l’insieme delle stringhe costituite da una sequenza di lettere o cifre inizianti per lettera; stringhe di questo tipo, chiamate identificatori sono molto frequenti nei linguaggi di programmazione, essendo usualmente poste in relazione a nomi di variabili, di subroutine, etc..

G_{4} = {V_{T} = {a, b, …, z, 0, …, 9},

                           V_{N} = {IDENTIFICATORE, LETTERA, CIFRA},
                           S = IDENTIFICATORE,

Φ = {

                             IDENTIFICATORE → LETTERA
                             IDENTIFICATORE → IDENTIFICATORE CIFRA
                             IDENTIFICATORE → IDENTIFICATORE LETTERA
                             LETTERA → a

⋯

                             LETTERA → z
                             CIFRA → 0

⋯

                             CIFRA → 9

} }

La frase bu2 appartiene al linguaggio e infatti risulta essere un identificatore.

L’algoritmo di parsing top-down comincia col riscrivere il simbolo IDENTIFICATORE, la radice dell’albero; si trova però davanti a tre possibilità (le tre regole in cui IDENTIFICATORE compare a sinistra); seleziona allora una delle alternative, ad esempio la prima.

                                 IDENTIFICATORE ⇒ LETTERA

Siamo in grado di renderci subito conto che, avendo scelto questa via, non riusciremo a raggiungere la frase bu2; infatti dal non-terminale LETTERA è possibile produrre solo identificatori costituiti da una singola lettera. L’algoritmo tuttavia non è ancora in grado di accorgersi dell’errore, e prosegue nella costruzione dell’albero, enumerando tutte le possibili frasi che in questo modo possono essere prodotte. Solo a questo punto si accorge che per produrre la frase bu2 bisogna esplorare un’altra alternativa; torna quindi indietro e riscrive la radice usando un’altra regola, ad esempio:

                          IDENTIFICATORE ⇒ IDENTIFICATORE CIFRA

E in realtà ha scelto proprio la via giusta. A questo punto come prossimo passo, può scegliere di riscrivere il simbolo IDENTIFICATORE, oppure può rimandare e riscrivere prima il simbolo CIFRA. In particolare un processo di parsing top-down da sinistra verso destra riscriverà dapprima il simbolo IDENTIFICATORE, un processo di parsing top-down da destra verso sinistra riscriverà prima il simbolo CIFRA; il primo fornirà la successione di frasi ϕ_{i} relativa al processo di produzione da sinistra IDENTIFICATORE ⇒_{L}^{∗} bu2, mentre il

secondo la successione relativa al processo di produzione da destra IDENTIFICATORE ⇒_{R}^{∗} bu2.

Un algoritmo di questo tipo, detto backup, non è molto valido; scegliendo una delle possibili alternative con cui riscrivere un non-terminale e proseguendo nella generazione di tutte le possibili frasi del linguaggio a cui tale scelta conduce, può richiedere molto tempo prima che l’errore nella scelta venga rivelato.

Il processo di parsing bottom-up parte dalla frase e tenta di ridurla al simbolo di partenza. Ad esempio la frase bu2 può essere ridotta, in un processo di parsing da sinistra a destra, a LETTERA u 2 utilizzando la regola LETTERA → b; oppure a b u CIFRA in un processo di parsing bottom-up da destra a sinistra.

Osserviamo che un processo di parsing bottom-up da sinistra a destra fornisce la sequenza ϕ_{i} di frasi associata al processo di produzione da destra IDENTIFICATORE ⇒_{R}^{∗} bu2.

Nel caso in cui ci trovassimo di fronte a diversi possibili modi di ridurre parti della frase, possiamo procedere in maniera backup, tentando tutte le possibili alternative fino a trovare quella giusta, oppure esplorando i “dintorni” della sottofrase in esame cercando “suggerimenti” sulla via da seguire. Algoritmi che operano in quest’ultimo modo effettivamente esistono; per essi i tempi di parsing sono notevolmente inferiori.

2.11 GRAMMATICHE AMBIGUE

Nel paragrafo precedente si è lasciato intendere che ad ogni frase di un linguaggio L(G) corrisponde un solo albero sintattico, benché siano molteplici i processi derivativi che la producono. Vi sono dei casi in cui questo non è rigorosamente vero.

Sia G_{3} la grammatica:

G_{3} = {V_{T} , V_{N}, S, Φ} V_{T} = {+, ∗, a, b, …, z}

                         V_{N} = {ESPRESSIONE, LETTERA}
                         S = ESPRESSIONE

Φ = {

                           ESPRESSIONE → LETTERA
                           ESPRESSIONE → ESPRESSIONE + ESPRESSIONE
                           ESPRESSIONE → ESPRESSIONE ∗ ESPRESSIONE

}

la frase:

a + b ∗ c

appartiene al linguaggio L(G_{3}) generato da G_{3}. Ad essa corrispondono più derivazioni; due possibili derivazioni sono:

                       ESPRESSIONE ⇒ ESPRESSIONE + ESPRESSIONE ⇒
                        ⇒ ESPRESSIONE + ESPRESSIONE ∗ ESPRESSIONE ⇒^{∗}
                        ⇒^{∗} LETTERA + LETTERA ∗ LETTERA ⇒^{∗}

⇒^{∗} a + b ∗ c

e

                       ESPRESSIONE ⇒ ESPRESSIONE ∗ ESPRESSIONE ⇒
                        ⇒ ESPRESSIONE + ESPRESSIONE ∗ ESPRESSIONE ⇒^{∗}
                        ⇒^{∗} LETTERA + LETTERA ∗ LETTERA ⇒^{∗}

⇒^{∗} a + b ∗ c

Non è vero però che a tali due derivazioni corrisponde lo stesso albero sintattico. Le 2.2 e 2.3 mostrano la diversa struttura in cui è stata scomposta la stessa frase a + b ∗ c, seguendo due processi derivativi differenti. Naturalmente l’albero sintattico giusto, cioè quello in grado di fornire il corretto valore semantico alla frase a + b ∗ c, è quello mostrato dalla 2.3, dove le moltiplicazioni hanno priorità (vengono svolte prima) sulle addizioni.

L’esistenza di più di una struttura per una stessa frase di un linguaggio è intollerabile. Il compilatore del linguaggio, in corrispondenza a differenti strutture, produrrebbe differenti codici oggetto. Se si vuole che il compilatore realizzi valide traduzioni di tutte le frasi di un linguaggio, tale linguaggio deve essere definito senza ambiguità.

Definizione 2.11 Una frase generata da una grammatica è ambigua se esistono per essa più alberi sintattici che ne descrivono la struttura. Una grammatica è ambigua non appena generi almeno una frase ambigua.

Se vogliamo che la frase a + b ∗ c sia correttamente strutturata e interpretata, dobbiamo forzare la precedenza delle diverse operazioni, introducendo i simboli non-terminali/classi sintattiche TERMINE FATTORE, analogamente a quanto facemmo nell’esempio della grammatica G_{2}.

Notare che abbiamo parlato di grammatiche ambigue, non di linguaggi ambigui. Un linguaggio generato da una grammatica ambigua, potrebbe benissimo essere generato anche da una grammatica non ambigua.

figura 2.2. Albero con addizione prioritaria

Esiste tuttavia la possibilità di linguaggi intrinsecamente ambigui, per cui cioè non si possono trovare grammatiche non ambigue. Ecco un esempio di linguaggio intrinsecamente ambiguo (Chomsky, 1963):

L_{5} = { a^{i}b^{j}c^{k} | i = j oppure j = k }

Dal momento che non esistono algoritmi in grado di determinare in un tempo finito, se una certa grammatica context-free sia ambigua o meno, resta a completa discrezione di chiunque si accinga a definire un nuovo linguaggio, scegliere per esso una grammatica non ambigua. In suo aiuto possono essere fornite solo un insieme di condizioni sufficienti ma non necessarie, affinché una grammatica possa essere considerata senza dubbio non ambigua; nel caso una certa grammatica non verifichi tali condizioni, non è assolutamente detto che essa sia ambigua.

figura 2.3. Albero con moltiplicazione prioritaria

2.12 GRAMMATICHE RIDOTTE

Abbiamo lasciato in ultimo alcune questioni relative a particolarità e stranezze in cui è possibile imbatterci studiando grammatiche le quali sfruttino tutte le possibilità che la definizione generale di grammatica consente.

Eccone subito una: una grammatica context-free può contenere regole di produzione del tipo A → A. Naturalmente una tale regola di produzione è completamente inutile; infatti data una grammatica G per cui l’insieme delle regole di produzione Φ contiene la regola A → A ((A → A) ∈ Φ), possiamo sempre definire una nuova grammatica G′ , differente da G nel fatto che il suo insieme di regole di produzione non contiene A → A (Φ′ = Φ − {(A → A)}); vale la proprietà che G′ genera lo stesso linguaggio L(G) generato da G.

Il punto è se una regola di produzione A → A renda la grammatica ambigua; infatti durante un processo di parsing, una volta raggiunto il simbolo A, lo potremmo riscrivere un numero qualsiasi di volte, cosicché ad una stessa frase del linguaggio corrisponderebbero diversi alberi sintattici. I diversi alberi sintattici però non impongono strutture tanto diverse da consentire interpretazioni diverse; per ogni albero, ciascun simbolo non-terminale insiste sugli stessi simboli terminali della frase.

Tuttavia una tale regola sintattica infastidisce soprattutto il parser che sta ricostruendo la struttura della frase; durante il processo di parsing infatti, nel caso venga raggiunto un simbolo A da sopra (top-down) da sotto (bottom-up), il parser potrebbe entrare in un processo infinito di recursione riscrivendo il simbolo sempre secondo la regola A → A e diventando di fatto inoperante.

Le regole A → A vanno perciò categoricamente escluse. Come però vanno anche esclusi simboli nonterminali detti inattivi.

I simboli non-terminali inattivi sono dei simboli non in grado di produrre frasi terminali; non possono quindi comparire in un albero sintattico poiché il sottoalbero che da essi ha origine non avrebbe foglie terminali.

Una volta determinato l’insieme V_{N}^{i} dei simboli non-terminali inattivi di una grammatica G (ovviamente i simboli terminali sono tutti attivi), la grammatica G′ definita ponendo V′_{N} = V_{N} − V_{N}^{i} ed eliminando da Φ tutte le regole di produzione in cui compaiano, a destra o a sinistra, simboli inattivi, è equivalente alla grammatica G.

Anzi è possibile definire un algoritmo in grado di ridurre la grammatica G alla G′, eseguendo le opportune modifiche. In questo semplice caso mostreremo l’algoritmo, soprattutto per fornire un’idea di che cosa intendevamo e intenderemo in seguito per “algoritmo”.

                                   Algoritmo ``Attivo''
    primo passo:  trova l'insieme iniziale W_{1}
    di simboli non-terminali attivi.

W_{1} = {A|(A → ϕ) ∈ Φ, ϕ ∈ V_{T}^{∗}}

    secondo passo:  trova i successivi insiemi W_{k}
    di simboli non-terminali attivi.
        ripeti per k = 2, 3, . . .

W_{k} = W_{k−1} ∪ {A|(A → ϕ) ∈ Φ, ϕ ∈ (V_{T} ∪ W_{k−1})^{∗}}

        fino al momento in cui W_{k} = W_{k−1}.

    terzo passo:  definisce gli insiemi V′_{N} e Φ′
    per la nuova grammatica G′.

V′_{N} = W_{k}

Φ′ = {(A → ϕ)|(A → ϕ) ∈ Φ, ϕ ∈ (V_{T} ∪ V′_{N})^{∗}}

    fine.

Altri simboli inutili sono i simboli irraggiungibili. Un simbolo, terminale o non-terminale, è detto raggiungibile se appartiene all’insieme {α|α ∈ (V_{T} ∪ V_{N}), S ⇒^{∗} ϕ_{1}αϕ_{2}}; cioè è un simbolo che può comparire in una frase derivabile dal simbolo di partenza S. Altrimenti è detto irraggiungibile. I simboli irraggiungibili non compaiono mai in un processo di produzione.

La grammatica G′ ottenuta da G eliminando da V_{T} ∪ V_{N} tutti i simboli irraggiungibili e da Φ tutte le regole in cui essi compaiono, è equivalente a G. Nel senso che L(G) = L(G′).

Anche in questo caso è possibile fornire un algoritmo in grado di generare la grammatica G′ a partire da G.

Le grammatiche ottenute eliminando regole del tipo A → A, simboli non-terminali inattivi e simboli irraggiungibili (insieme a tutte le regole in cui tali simboli compaiono), sono dette ridotte. La loro relativa maggiore semplicità mette il parser in grado di operare con minore carico di lavoro.

Un ultima nota riguarda il fatto che grammatiche realmente ridotte sono ottenute applicando nell’ordine, prima l’algoritmo “Attivo”, poi l’algoritmo “Raggiungibile”. E’ possibile infatti che alcuni simboli siano raggiungibili solo attraverso simboli inattivi.

L’ultima questione che è necessario affrontare è quella sulle produzioni vuote.

Una regola di produzione vuota è una regola A → ϵ, ove ϵ è la frase nulla. A differenza della regola A → A, la produzione vuota è relegata fuori dalla condizione che definisce grammatiche context-free; infatti nella produzione vuota, la lunghezza della frase a destra è minore della frase a sinistra.

Una grammatica comprendente una produzione vuota non è quindi una grammatica context-free. Tuttavia è possibile dimostrare il seguente teorema:

Teorema 2.4 Indicando con T_{2} l’insieme di tutte le grammatiche context-free e con T_{2+ϵ} l’insieme di tutte le possibili grammatiche context-free a cui sia permesso contenere anche regole di produzione vuota, sussiste l’identità:

L(T_{2}) = L(T_{2+ϵ})  (2.12.1)

il che rende equivalenti le due classi T_{2} e T_{2+ϵ}.

Anzi, oltre alla dimostrazione del teorema, sarebbe possibile addirittura fornire un algoritmo in grado di generare, a partire da una grammatica G contenente produzioni vuote, una grammatica G′ ∈ T_{2} equivalente alla G .

L’uso delle regole di produzione vuota, benché evitabile, permette una più compatta e naturale descrizione dei linguaggi. Alcuni algoritmi di parsing consentono senza problemi il loro uso. Nella definizione del linguaggio APESE spesso ci si è valsi della introduzione di produzioni vuote, essendo apparso appunto più naturale.

2.13 AZIONI SEMANTICHE - LINGUAGGI DI PROGRAMMAZIONE

Sia L un linguaggio di programmazione e G una grammatica non ambigua ridotta in grado di generarlo. Data una frase del linguaggio, una volta costruita la corretta struttura sintattica, abbiamo tutti gli elementi sufficienti a comprendere il significato della frase, e quindi a tradurlo nel codice oggetto corrispondente.

Quello che possiamo fare è associare ad ogni regola di produzione della grammatica una “azione” corrispondente al significato che la regola sottintende. L’azione rappresenta la semantica associata alla regola.

Ad esempio supponiamo che il nostro parser a schema bottom-up, durante il parsing di una frase, riesca a ridurre (trovi una corretta riduzione per) la sottofrase a + b verso ESPRESSIONE + TERMINE fino al simbolo ESPRESSIONE, secondo la regola:

                            ESPRESSIONE → ESPRESSIONE + TERMINE

nel momento stesso della riduzione il parser dovrà eseguire una azione corrispondente alla regola; ovviamente in questo caso l’azione consisterà nell’effettuare una somma (o nel produrre codice oggetto che effettui la somma).

Analogamente, allorché il parser riduce una sottofrase secondo un’altra regola, eseguirà l’azione associata a quella regola.

Discussioni che precisino meglio questi argomenti, richiedono specificazioni sulle particolarità costruttive della macchina “parser” che viene utilizzata, e in generale dell’intero compilatore di cui il parser è solo una parte. Poiché ora ci interessa mantenere la trattazione il più possibile generale, tali argomenti resteranno in sospeso.

Termineremo invece il capitolo esaminando le soluzioni scelte nella realizzazione dei più comuni linguaggi di programmazione realizzati fino ad oggi.

Si è detto che non esistono algoritmi efficienti di parsing per linguaggi generati da grammatiche contextsensitive; per cui saremmo costretti a formulare linguaggi di programmazione generabili da grammatiche context-free. E’ possibile generare la maggior parte delle strutture tipiche di linguaggi di programmazione attraverso grammatiche context-free. Ma esistono parti dei linguaggi di programmazione dipendenti dal contesto, di cui non è possibile fare a meno. Si è accennato al fatto che nei programmi l’utilizzo di identificatori per variabili dovrebbe risultare consentito solo se prima ne sia stata fatta una dichiarazione. Ma voler imporre questa restrizione rende la grammatica del linguaggio context-sensitive.

Generalmente nella realizzazione dei compilatori per i linguaggi di programmazione si risolve il problema mediante quello che è detto metodo dell’attribute grammar. Esso consiste nel descrivere comunque il linguaggio attraverso una grammatica context-free, tralasciando quelle regole di produzione che lo avrebbero reso context-sensitive. Il controllo delle parti context-sensitive viene trasferito in ambito semantico.

Ad esempio ecco la regola di produzione e l’azione associata relativi alla istruzione di dichiarazione di una variabile per un generico linguaggio di programmazione:

            STATEMENT → integer IDENTIFICATORE
                azione:  riserva spazio in memoria per la nuova variabile,

                         inserisci la frase terminale sottintesa da IDENTIFICATORE
                         in una tabella.

dove integer è un simbolo terminale, IDENTIFICATORE è il simbolo non-terminale per il quale abbiamo già specificato (esempio G_{4}) le regole di produzione relative, e STATEMENT è un non-terminale collegato molto semplicemente ad un simbolo di partenza STATEMENT LIST ad esempio con le regole:

                         STATEMENT LIST → STATEMENT
                         STATEMENT LIST → STATEMENT LIST STATEMENT

La grammatica è istruita dalla regola:

                                VARIABILE → IDENTIFICATORE  (2.13.1)

ad accettare come valide variabili tutti i possibili identificatori, dichiarati o non.

L’azione relativa alla (2.13.1) dovrà essere però:

                 azione:  se la frase terminale sottintesa da IDENTIFICATORE

                          non è nella tabella, segnala l'errore.

Per cui verranno scartate le linee di programma in cui compaiono identificatori non dichiarati. Più esattamente le linee vengono accettate dalla sintassi (sono considerate cioè frasi del linguaggio) ma esse vengono considerate, in fase semantica, invalide.

Nel caso dell’APESE il problema delle strutture context-sensitive è stato risolto in modo del tutto differente; per l’APESE si è fatto uso di una nuova classe di grammatiche, detta delle evolving grammars, per cui esiste ed è stato trovato un valido algoritmo di parsing.

CAPITOLO 3 LINGUAGGI EVOLVENTI

3.1 INTRODUZIONE

Lo sviluppo e alla realizzazione di sistemi di compilazione tradizionali per linguaggi di programmazione, si scontra con la difficoltà legata alla riduzione della definizione del linguaggio ad uno schema context-free. Non esiste infatti a tutt’oggi un efficiente algoritmo di analisi sintattica per linguaggi context-sensitive.

La soluzione tradizionale è quella di escogitare artifici grazie ai quali rendere conto delle strutture context-sensitive di cui ogni linguaggio di programmazione, per quanto semplice, è necessariamente provvisto.

L’artificio più comune fino ad oggi è stato quello di trasferire il controllo delle strutture context-sensitive del linguaggio dalla fase sintattica alla fase semantica (dalla fase di riconoscimento del linguaggio alla fase della traduzione del suo significato).

In questo capitolo esporremo alcune inovazioni teoriche (Cabasino, Paolucci, Todesco 1992) e alcuni risultati pratici su recenti sviluppi della teoria dei linguaggi formali avutisi nell’ambito del progetto APE100, essenzialmente riassumibili in due nuovi concetti: le “grammatiche evolventi” e i “parser dinamici”.

In sostanza può essere anticipato che, piuttosto di forzare una predefinita grammatica context-free statica a riconoscere tutti i possibili contesti che gli specifici programmi sorgenti potrebbero presentarle, noi introdurremo grammatiche context-free evolventi le quali adattano se stesse allo specifico contesto.

Grammatiche context-free evolventi incorporano progressivamente regole di produzione specifiche per il programma sorgente sottoposto ad analisi sintattica, ed evolvono seguendo il contesto creato dal programma stesso, verso una grammatica context-free specifica al programma.

Per grammatiche context-free evolventi esiste un algoritmo di parsing efficace; esso regola il funzionamento dei “parser dinamici”, il capostipite dei quali è lo ZzParser, sviluppato all’interno del gruppo APE.

Grazie ai parser dinamici è possibile un parsing esclusivamente sintattico di molti linguaggi di programmazione. Inoltre i parser dinamici consentono rigidi controlli sintattici sui tipi e l’overload degli operatori. Infine lasciano aperti i linguaggi di programmazione a possibili ulteriori evoluzioni e personalizzazioni a cura dell’utente ultimo, specificate utilizzando il linguaggio evolvente stesso.

3.2 GRAMMATICHE EVOLVENTI ED EVOLUZIONI GRAMMATICALI

Basandoci sui concetti esposti nel precedente capitolo, siamo ora in grado di fornire la seguente definizione:

Definizione 3.1 Una evoluzione di grammatiche è una successione di grammatiche

E = {G^{i}},  i = 0, …, n  (3.2.1)

tale che i singoli elementi della successione

G^{i} = (V_{T} , V_{N}^{i}, S, Φ^{i})  (3.2.2)

siano grammatiche aventi gli stessi insiemi di simboli terminali e lo stesso simbolo di partenza, e per i cui insiemi di simboli non terminali e delle regole di produzione valgano le:

V_{N}^{i+1} ⊇ V_{N}^{i}  (3.2.3)
Φ^{i+1} ⊇ Φ^{i}  (3.2.4)

Una evoluzione di grammatiche è intuitivamente vista come un progressivo “accrescimento” della grammatica di partenza G^{0}. Sicché noi possiamo descrivere l’evoluzione come una successione di passi del tipo:

G^{i+1} = G^{i} + ΔG^{i}  (3.2.5)

ove con ΔG^{i} intendiamo la doppia:

ΔG = (ΔV_{N}, ΔΦ)  (3.2.6)

e con l’operazione ‘+’:

G^{i+1} = (V_{T} , V_{N}^{i} ∪ ΔV_{N}^{i}, S, Φ^{i} ∪ ΔΦ^{i})

ΔG^{i} specifica i nuovi simboli non terminali e le nuove regole di produzione da aggiungere a G^{i}.

Naturalmente anche per le evoluzioni di grammatiche vale lo schema di classificazione di Chomsky. Se G^{i} è una grammatica context-free e ΔΦ^{i} è un insieme di regole di produzione context-free, la risultante grammatica G^{i+1} sarà ancora una grammatica context-free. Nel seguito al solito, restringeremo la trattazione per prendere in considerazione esclusivamente evoluzioni di grammatiche context-free.

Fin qui i nuovi concetti potrebbero non presentare particolare interesse. Prendere in considerazione la evoluzione di grammatiche, cioè l’intero insieme delle G^{i} che ci portano a G^{n} per passi successivi, potrebbe sembrare una complicazione rispetto al prendere in considerazione la singola ultima grammatica G^{n}, la più completa della serie.

Immaginiamo però che i singoli processi di accrescimento ΔG^{i} della evoluzione siano associati ciascuno ad una certa regola di produzione Φ_{μ}^{i} ∈ Φ^{i} di G^{i}

ΔG^{i} (Φ_{μ}^{i}) = (ΔV_{N}^{i} (Φ_{μ}^{i}) , ΔΦ^{i} (Φ_{μ}^{i}))  (3.2.7)

Con questo si intende dire che ΔG^{i} (Φ_{μ}^{i}) specifica i nuovi simboli non-terminali e le nuove regole di produzione da aggiungere a G^{i} nel momento in cui la regola Φ_{μ}^{i} viene utilizzata in un processo di parsing per produrre (o per ridurre) una frase.

Definizione 3.2 Una grammatica evolvente G è una grammatica avente almeno una regola di produzione Φ_{μ} associata ad un non vuoto ΔG = ΔG (Φ_{μ}).

Definizione 3.3 Una evoluzione grammaticale iniziante da una grammatica evolvente G^{0} è una evoluzione di grammatiche G^{i} ciascuna connessa alla successiva da un gradino di evoluzione associato ad una regola di produzione ΔG^{i} (Φ_{μ}^{i})

E (G^{0}) = {G^{i}|G^{i} = G^{i−1} + ΔG^{i−1} (Φ_{μ}^{i−1}) , i = 1, …, n}  (3.2.8)

La Φ_{μ}^{i} è una regola di produzione della grammatica G^{i}.

Una grammatica evolvente G^{0} è cioè la “capostipite” di una evoluzione di grammatiche.

Il processo di accrescimento grammaticale avviene a parsing-time. Ossia, allorché il parser utilizza per ridurre o per produrre una frase una regola Φ_{μ}^{0} di G^{0} cui è associato un gradino di evoluzione ΔG^{0} (Φ_{μ}^{0}), la grammatica cambia (e con essa le tabelle grammaticali del parser) verso la nuova grammatica G^{1} = G^{0} + ΔG^{0} (Φ_{μ}^{0}).

Naturalmente anche la G^{1} possederà regole di produzione associate a gradini di evoluzione ΔG^{1} (Φ_{μ}^{1}), ad esempio la stessa Φ_{μ}^{0} ∈ Φ^{0} che per definizione appartiene anche a G^{1} (Φ^{1} = Φ^{0} ∪ ΔΦ^{0} (Φ_{μ}^{0})); anzi, nell’insieme ΔΦ^{0} (Φ_{μ}^{0}) delle nuove regole di produzione da aggiungere a Φ^{0} per ottenere Φ^{1}, potrà essercene qualcuna associata a sua volta ad un altro gradino di evoluzione ΔG.

Il processo di evoluzione grammaticale può continuare per tutto il tempo in cui il parser è impegnato nell’analisi di una frase, fino alla conclusione di essa; la grammatica cambia ogni volta che viene utilizzata una regola di produzione associata ad un gradino di evoluzione ΔG nonnullo.

Durante il parsing di una frase, il processo di evoluzione grammaticale può seguire strade diverse. Ricordiamo infatti che esistono diversi processi di produzione di una stessa frase a partire dal simbolo di partenza S (o diversi processi di riduzione della frase al simbolo di partenza); ognuno differente dall’altro per l’ordine in cui vengono riscritti (o ridotti) i simboli terminali e non terminali, e quindi per l’ordine con cui vengono utilizzate le regole di produzione durante la derivazione.

Si rende così necessario definire un ordine preferenziale di utilizzo delle regole di produzione durante il parsing, in modo da fissare univocamente, per ogni frase, l’ordine degli step di accrescimento della grammatica che intervengono durante il parsing stesso.

Qui però non è sufficiente specificare semplicemente una derivazione canonica da destra o da sinistra per determinare senza ambiguità l’evoluzione grammaticale (ossia l’insieme ordinato delle G^{0}, G^{1}, …, G^{n}). Infatti la specificazione di una derivazione canonica “laterale” fissa univocamente l’insieme ordinato delle frasi ϕ_{0}, …, ϕ_{n}, attraverso il quale si svolge la derivazione; fissa altresì univocamente l’insieme ordinato delle regole di produzione, diciamo Φ_{1}, …, Φ_{n}, che sono utilizzate durante il parsing; non fissa però un verso con cui percorrere l’insieme ordinato delle regole di produzione, il quale ovviamente dipenderà dalla modalità di parsing (top-down o bottom-up) che si sta seguendo.

Nel caso ad esempio, di parsing top-down percorrente una derivazione canonica da destra

ϕ_{0} ⇒_{R}^{∗} ϕ_{1} ⇒_{R}^{∗} ⋯ ⇒_{R}^{∗} ϕ_{n}

le frasi ϕ_{0}, …, ϕ_{n} del processo di derivazione vengono raggiunte secondo il verso indicato dall’ordine crescente dell’indice; allo stesso modo le regole di produzione Φ_{1}, …, Φ_{n}, grazie alle quali sono possibili le singole derivazioni dirette, vengono utilizzate durante il parsing nell’ordine dell’indice crescente.

Per inciso:

Definizione 3.4 Chiamiamo derivazione diretta canonica da destra secondo la regola Φ_{μ}, scrivendo ψ ⇒_{R}^{Φ_{μ}} ϕ, la derivazione diretta canonica da destra di ϕ da ψ, ψ ⇒_{R} ϕ, eseguita utilizzando la regola Φ_{μ} = α → β della

grammatica. Indicheremo in modo analogo con ψ ⇒_{L}^{Φ_{μ}} ϕ, una derivazione diretta canonica da sinistra secondo la regola Φ_{μ}.

Scriviamo:

ϕ_{0} = S ⇒_{R}^{Φ_{1}} ϕ_{1} ⇒_{R}^{Φ_{2}} … ϕ_{n−1} ⇒_{R}^{Φ_{n}} ϕ_{n} = σ  (3.2.9)

per intendere il processo di parsing top-down percorrente una derivazione canonica da destra (leggendo l’espressione da sinistra a destra seguiamo proprio l’ordine temporale utilizzato dal parser nel costruire la sequenza delle ϕ_{i}); ora, nel caso alle regole Φ_{1}, …, Φ_{n} siano associati degli step evolutivi ΔG (Φ_{i}) nonnulli, avremmo come risultato una evoluzione grammaticale:

G_{0}
G_{1} = G_{0} + ΔG (Φ_{1})
G_{2} = G_{1} + ΔG (Φ_{2}) = G_{0} + ΔG (Φ_{1}) + ΔG (Φ_{2})
⋯
G_{n−1} = G_{n−2} + ΔG (Φ_{n−1}) = G_{0} + ΔG (Φ_{1}) + ⋯ + ΔG (Φ_{n−1})
G_{n} = G_{n−1} + ΔG (Φ_{n}) = G_{0} + ΔG (Φ_{1}) + ⋯ + ΔG (Φ_{n})

(3.2.10)

Nel caso invece di parsing bottom-up percorrente la derivazione canonica da destra, l’ordine di raggiungimento delle frasi ϕ_{0}, …, ϕ_{n} e delle regole di produzione Φ_{1}, …, Φ_{n}, è quello invece indicato dal verso decrescente dell’indice. Usiamo la convenzione per cui:

ϕ_{n} = σ ⇐_{R}^{Φ_{n}} ϕ_{n−1} ⇐_{R}^{Φ_{n−1}} … ϕ_{1} ⇐_{R}^{Φ_{1}} ϕ_{0} = S  (3.2.11)

indica un processo di parsing bottom-up (il parser raggiunge le frasi ϕ_{i} proprio nell’ordine che avremmo leggendo da sinistra a destra). Per esso l’evoluzione grammaticale risulterà diversa:

G_{0}
G′_{1} = G_{0} + ΔG (Φ_{n})
G′_{2} = G′_{1} + ΔG (Φ_{n−1}) = G_{0} + ΔG (Φ_{n}) + ΔG (Φ_{n−1})
⋯
G′_{n−1} = G′_{n−2} + ΔG (Φ_{2}) = G_{0} + ΔG (Φ_{n}) + ⋯ + ΔG (Φ_{2})
G_{n} = G′_{n−1} + ΔG (Φ_{1}) = G_{0} + ΔG (Φ_{n}) + ⋯ + ΔG (Φ_{1})

(3.2.12)

Notiamo che G′_{1} è differente da G_{1}, come anche da G_{n−1}; pur tuttavia oltre a G_{0} (la grammatica di partenza della evoluzione), anche la grammatica di arrivo G_{n} è la stessa. E’ necessario però precisare che alla strana coincidenza non può non contribuire una semplificazione che in questo caso abbiamo fatto: notiamo infatti che nel parsing sono state utilizzate tutte regole di produzione, le Φ_{1}, …, Φ_{n}, già appartenenti a Φ^{0}, l’insieme delle regole di produzione relativo alla grammatica di partenza G^{0}; non è stato cioè necessario utilizzare le nuove regole di produzione degli insiemi ΔΦ che venivano aggiunti durante il parsing.

3.3 DEFINIZIONE DI LINGUAGGIO EVOLVENTE

Studieremo ora i risultati cui conducono le nostre considerazioni su grammatiche evolventi ed evoluzioni grammaticali.

Definizione 3.5 Indichiamo con ψ ⇒_{R}^{Φ^{i}} σ la derivazione canonica da destra della frase σ dalla frase ψ eseguita

utilizzando regole di produzione dell’insieme Φ^{i}.

Visto infatti che con una evoluzione grammaticale stiamo in realtà prendendo in considerazione più grammatiche contemporaneamente, ci sarà necessario specificare attraverso quale grammatica è possibile una certa derivazione.

Definizione 3.6 Definiamo derivazione evolutiva canonica da destra, indicando ψ ⇒_{R}^{Φ^{0}···Φ^{n}} σ, la derivazione

di σ da ψ lungo una evoluzione grammaticale da G^{0} a G^{n}:

σ ⇐_{R}^{Φ^{0}} α ⇐_{R}^{Φ_{μ}^{0}} β ⇐_{R}^{Φ^{1}} γ ⇐_{R}^{Φ_{μ}^{1}} ⋯ ⇐_{R}^{Φ_{μ}^{n−1}} ω ⇐_{R}^{Φ^{n}} ψ  (3.3.1)

Non è arbitrario l’aver scelto di rappresentare per la (3.3.1) la derivazione “reduttiva” (frase σ a sinistra e frase ψ a destra); si vuole infatti intendere che le successive frasi α, β, …, vanno raggiunte attraverso un processo di parsing bottom-up canonico da destra. L’accrescimento grammaticale per la (3.3.1) avviene da sinistra a destra (G^{0} ⊆ G^{n}); ogni step evolutivo che fa passare da una grammatica G^{i} ad una G^{i+1} è evidenziato dalla derivazione diretta ottenuta utilizzando la regola di produzione Φ_{μ}^{i} associata allo step evolutivo stesso ΔG^{i}(Φ_{μ}^{i}).

Definizione 3.7 Definiamo linguaggio evolvente il linguaggio generato da una evoluzione grammaticale, ossia l’insieme di frasi che è possibile ottenere attraverso una derivazione evolutiva canonica da destra a partire dal simbolo di partenza S della evoluzione, eseguita però con la specifica modalità di parsing bottomup:

L[E(G^{0})] = { σ|σ ∈ V_{T}^{∗}, σ ⇐_{R}^{Φ^{0}···Φ^{n}} S }  (3.3.2)

Naturalmente per la G^{0} della (3.3.2) si deve intendere una grammatica context-free evolvente (ossia con almeno una regola di produzione associata ad uno step evolutivo ΔG nonnullo). Altrimenti con la (3.3.2) ricadremmo nella ordinaria definizione di tradizionale linguaggio context-free.

Vogliamo ora mostrare come le possibiltà fornite da grammatiche evolventi siano ben maggiori di quelle che è possibile avere da tradizionali grammatiche context-free.

Esistono linguaggi generati da grammatiche evolventi che non è possibile generare altrimenti con grammatiche context-free. Ad esempio il linguaggio costituito dall’insieme delle stringhe:

L = { x | x = ycy, y ∈ {a, b}^{∗} }  (3.3.3)

ossia un linguaggio in cui compare due volte (prima del simbolo C e dopo) una identica sequenza di simboli terminali a e b; per inciso, questo linguaggio astrae il problema di testare la dichiarazione di un identificatore (la prima occorrenza di y) prima del suo uso (la seconda occorrenza). Non è possibile generare il linguaggio (3.3.3) con una comune grammatica context-free; di fatto si può dimostrare che il linguaggio (3.3.3) appartiene, nella classificazione di Chomsky, alla classe dei linguaggi context-sensitive. Una evoluzione grammaticale context-free (che cioè ha origine da una grammatica evolvente G^{0} context-free e che si sviluppa per step evolutivi contex-free), è invece in grado di generarlo facilmente. E’ sufficiente che G^{0} sia:

G^{0} = G_{ev} = {
V_{T} = {a, b, c},
V_{N}^{0} = {S, D, I, U_{0}},
S = S,
Φ^{0} = {

                          S → D U_{0}
                          D → I D

D → c : ΔG^{i} = {ΔV_{N} = {}, ΔΦ = {U_{i} → ϵ}}
I → a : ΔG^{i} = {ΔV_{N} = {U_{i+1}}, ΔΦ = {U_{i} → a U_{i+1}}}
I → b : ΔG^{i} = {ΔV_{N} = {U_{i+1}}, ΔΦ = {U_{i} → b U_{i+1}}}
}
}

Nell’esempio i due punti separano le regole di produzione dai rispettivi steps evolutivi ad esse associati. Il simbolo di partenza S può essere riscritto nei due simboli non terminali D e U_{0}; D è in grado di generare qualsiasi sequenza di a e b seguita da c; U_{0} genera solo la stessa sequenza generata da D. La stringa babcbab appartiene al linguaggio (3.3.3); durante il parsing (bottom-up canonico da destra) di essa, la grammatica cambia quattro volte:

G^{4} = G^{0} + {ΔV_{N} = {U_{1}, U_{2}, U_{3}},
ΔΦ = {(U_{0} → b U_{1}), (U_{1} → a U_{2}), (U_{2} → b U_{3}), (U_{3} → ϵ)}}

L’albero sintattico associato alla stringa è mostrato in figura 3.1. Risulta evidente in questo esempio come la grammatica si adatti allo specifico contesto che si trova davanti: il simbolo U_{i} si adatta a produrre la i-esima occorrenza di simbolo terminale che compare nella stringa y a sinistra di c.

Nella definizione di linguaggi evolventi, è importante la specificazione della modalità di parsing da utilizzare nel riconoscimento delle frasi del linguaggio; osserviamo infatti che, se per l’esempio in esame si fosse scelta una modalità di parsing bottom-up canonica da sinistra (ossia, per i risultati cui siamo giunti nel capitolo precedente, una modalità in cui la frase viene letta da destra a sinistra), il risultato sarebbe stato che la stringa babcbab non appartiene al linguaggio.

Come ultima considerazione, le grammatiche G^{i} di una evoluzione in generale contengono diversi simboli irraggiungibili o non terminali inattivi; ad esempio, il simbolo U_{0} della G_{ev} = G^{0} è un simbolo inattivo. Tali simboli potrebbero però divenire attivi e raggiungibili in un tempo successivo, allorché l’evoluzione abbia aggiunto regole di produzione appropriate. Sostituire all’interno di una evoluzione una grammatica G^{i}, così come ottenuta dall’aggiunta dei diversi step evolutivi, con la sua grammatica ridotta, sarebbe perciò un errore.

figura 3.1. Albero sintattico di un linguaggio evolvente.

3.4 PROPRIETA’ DELLE GRAMMATICHE EVOLVENTI

Una evoluzione grammaticale in generale non può raggiungere una G qualsiasi. Data cioè una grammatica evolvente di partenza G^{0} e una grammatica qualsiasi context-free G, non è detto che esista un processo di accrescimento grammaticale G^{0}, G^{1}, …, G^{n}, tale che il risultato finale sia proprio la G (G^{n} = G).

L’evoluzione grammaticale dipende strettamente dalle caratteristiche della grammatica di partenza G^{0}; in particolare, dipende dalle regole di produzione della grammatica di partenza associate a step evolutivi ΔG nonnulli.

Benché affermammo la necessità di dover considerare una evoluzione grammaticale come un processo illimitato di crescita (ogni singola grammatica di una evoluzione che inizia da una grammatica evolvente è a sua volta una grammatica evolvente), di fatto le evoluzioni in generale hanno dei limiti oggettivi.

Prendiamo l’esempio della grammatica G_{ev} del paragrafo precedente: l’evoluzione grammaticale che ha origine da essa, di fatto termina non appena viene letto il simbolo c; in tale occasione c viene ridotto a D e per l’ultima volta viene utilizzata una regola di produzione associata a uno step evolutivo ΔG nonnullo. Dopo c deve necessariamente trovarsi la ripetizione della frase y; ma la seconda occorrenza di y viene ridotta utilizzando regole di produzione associate a step evolutivi nulli. Quindi con y il processo di parsing è terminato; infatti non possiamo aggiungere altri simboli a destra poiché l’albero sintatico è ormai completo e da S non possono essere tracciati ulteriori rami. Insieme al processo di parsing naturalmente termina anche l’evoluzione grammaticale. Resta così dimostrato che l’evoluzione grammaticale a grammatica iniziale G_{ev} non può generare linguaggi altri che:

L = { x | x = ycy, y ∈ {a, b}^{∗} }  (3.4.1)

L’esempio della grammatica evolvente G_{ev} è interessante ma non mostra altre potenzialità dello strumento evoluzione grammaticale.

Definiamo ora invece una nuova grammatica evolvente che indicheremo col simbolo G_{a}. G_{a} = {V_{T} , V_{N}, S, Φ}

V_{T} = {A, …, Z, a, …, z, 0, …, 9, , -, >, [eol]}

Per la G_{a} cioè, consideriamo come insieme dei simboli terminali l’alfabeto delle lettere maiuscole e minuscole più altri speciali caratteri: le cifre numeriche, lo spazio bianco, il trattino, il carattere > e il carattere (di controllo) [eol] che indica il termine di una linea. Lo spazio bianco è necessario a separare visualmente una parola dall’altra, visto che questa grammatica considera la parola non come un singolo simbolo terminale, ma come una frase di simboli terminali; vedremo poi l’uso degli altri speciali caratteri.

                 V_{N} = {STATEMENT LIST, STATEMENT,

                       FRASE, NON TERMINALE, TERMINALE,
                       IDENTIFICATORE MAIUSCOLO, IDENTIFICATORE MINUSCOLO,
                       LETTERA MAIUSCOLA, LETTERA MINUSCOLA, CIFRA}

L’insieme dei simboli non-terminali contiene il simbolo di partenza STATEMENT LIST collegato al simbolo STATEMENT attraverso le regole di produzione:

                      STATEMENT LIST → STATEMENT LIST STATEMENT [eol]
                      STATEMENT LIST → STATEMENT [eol]

le quali stabiliscono che ogni singolo statement del linguaggio deve risiedere su una separata linea. V_{N}

inoltre contiene i simboli IDENTIFICATORE MAIUSCOLO, IDENTIFICATORE MINUSCOLO, LETTERA MAIUSCOLA,

LETTERA MINUSCOLA, CIFRA, ovviamente collegati fra di loro attraverso le usuali regole di produzione:

           IDENTIFICATORE MAIUSCOLO → IDENTIFICATORE MAIUSCOLO LETTERA MAIUSCOLA
           IDENTIFICATORE MAIUSCOLO → IDENTIFICATORE MAIUSCOLO CIFRA
           IDENTIFICATORE MAIUSCOLO → [blank] LETTERA MAIUSCOLA
           IDENTIFICATORE MINUSCOLO → IDENTIFICATORE MINUSCOLO LETTERA MINUSCOLA
           IDENTIFICATORE MINUSCOLO → IDENTIFICATORE MINUSCOLO CIFRA
           IDENTIFICATORE MINUSCOLO → [blank] LETTERA MINUSCOLA

Ogni identificatore inizia con uno spazio bianco nella necessità di essere separato visualmente dal precedente. Omettiamo di scrivere le ovvie regole di produzione “terminanti” relative ai non-terminali LETTERA MAIUSCO-

LA, LETTERA MINUSCOLA, CIFRA. V_{N} contiene infine i simboli FRASE, NON TERMINALE, TERMINALE, per cui

vengono fornite le regole di produzione:

                FRASE → FRASE TERMINALE
                FRASE → FRASE NON TERMINALE
                FRASE → TERMINALE
                FRASE → NON TERMINALE
                TERMINALE → IDENTIFICATORE MINUSCOLO
                NON TERMINALE → IDENTIFICATORE MAIUSCOLO :
                     :  ΔG = (ΔV_{N} = 'IDENTIFICATORE MAIUSCOLO', ΔΦ = {})
                STATEMENT → NON TERMINALE -> FRASE :
                     :  ΔG = (ΔV_{N} = {}, ΔΦ = 'NON TERMINALE' → 'FRASE')

In particolare, le ultime due regole sono le uniche di Φ ad essere associate a step evolutivi ΔG nonnulli. Con la notazione 'IDENTIFICATORE MAIUSCOLO' o 'FRASE, intendiamo la frase, la sequenza di simboli terminali \underline{dominata} dai non-terminali IDENTIFICATORE MAIUSCOLO e FRASE. Facciamo attenzione nell’ultima regola di produzione a distinguere il simbolo → dal simbolo \underline{terminale} della grammatica ->.

Con questo ultimo blocco di regole di produzione, possiamo ritenere completo l’insieme Φ delle regole di produzione della nostra grammatica G_{a}; a meno che non pensiamo di introdurre anche le regola di produzione:

                                         FRASE →

nel caso potessimo e volessimo prendere in considerazione grammatiche a regole di produzione vuote.

Chiamiamo Z_{a} il linguaggio L(G_{a}) generato dalla grammatica G_{a}; Z_{a} è il linguaggio generato non dalla evoluzione grammaticale E(G_{a}) cui G_{a} dà luogo, ma proprio dalla grammatica di partenza G_{a}, considerata alla stregua di una normale grammatica context-free, trascurando il suo attributo di evolvenza.

Chi è il linguaggio Z_{a}?

Qualcuno avrà riconosciuto in Z_{a} proprio il linguaggio pseudomatematico che abbiamo usato fino ad ora per descrivere grammatiche formali all’interno della teoria dei linguaggi formali.

Le frasi di esso consistono in stringhe poste su differenti linee (terminate cioè da [eol]); ognuna è composta da un simbolo terminale -> preceduto da un identificatore a lettere maiuscole (che rappresenta un simbolo non-terminale) e seguito da una FRASE costituita da una qualsiasi sequenza di identificatori a lettere maiuscole (simboli non-terminali) o identificatori a lettere minuscole (simboli terminali).

Ad esempio la frase:

              PROPOSIZIONE -> SOGGETTO PREDICATO VERBALE COMPLEMENTO OGGETTO

appartiene a Z_{a}.

Cosa succede ora quando andiamo a ridurre questa frase?

L’effetto è quello di far procedere l’evoluzione grammaticale verso una nuova grammatica G^{1}, più evoluta rispetto alla G_{a} = G^{0} per il fatto di contenere i nuovi simboli non-terminali e la nuova regola di produzione:

         ΔV_{N} = {PROPOSIZIONE, SOGGETTO, PREDICATO VERBALE, COMPLEMENTO OGGETTO}
         ΔΦ_{N} = {(PROPOSIZIONE → SOGGETTO PREDICATO VERBALE COMPLEMENTO OGGETTO)}

La seguente sequenza di stringhe (ciascuna regolarmente terminata da [eol]), appartiene ancora al linguaggio Z_{a}:

              PROPOSIZIONE -> SOGGETTO PREDICATO VERBALE COMPLEMENTO OGGETTO
              SOGGETTO -> ARTICOLO SOSTANTIVO
              COMPLEMENTO OGGETTO -> ARTICOLO SOSTANTIVO
              ARTICOLO -> il
              ARTICOLO -> un
              SOSTANTIVO -> cane
              SOSTANTIVO -> gatto
              SOSTANTIVO -> topo
              PREDICATO VERBALE -> mangia
              PREDICATO VERBALE -> uccide
              STATEMENT -> PROPOSIZIONE

l’ultima riga serve a connettere il non-terminale PROPOSIZIONE, il simbolo di partenza dell’esempio della grammatica G_{1}, al nuovo simbolo di partenza STATEMENT LIST della grammatica G_{a} e quindi dell’evoluzione grammaticale che da essa ha origine. Grazie a queste linee abbiamo condotto l’evoluzione grammaticale verso una grammatica G^{n} contenente gli stessi simboli non-terminali e le stesse regole di produzione dell’esempio della grammatica G_{1}. Il linguaggio generato dall’evoluzione comprende lo stesso insieme di frasi del linguaggio L(G_{1}) generato da G_{1}. Subito dopo la sequenza di linee mostrata infatti, possiamo tranquillamente aggiungere le linee:

                                 il gatto mangia un topo
                                 il gatto uccide il topo

⋯

Il parser le riconosce come appartenenti al linguaggio L[E(G_{a})] generato dall’evoluzione.

E’ chiaro che la grammatica G_{a} è notevolmente più “potente” rispetto all’esempio della grammatica G_{ev} del paragrafo precedente; questo in virtù della sua proprietà di essere “illimitata”. Di fatto una evoluzione E(G_{a}) a partire dalla grammatica evolvente G_{a}, può condurre verso una qualsiasi grammatica appartenente alla classe delle context-free, e quindi il linguaggio generato dalla evoluzione L[E(G_{a})] può essere un qualsiasi linguaggio appartenente a L(T_{CF}).

In realtà queste ultime affermazioni vanno parzialmente ridimensionate. La grammatica cui si giunge attraverso l’evoluzione a partire da G_{a}, non è realmente qualsiasi: si può vedere che la grammatica raggiunta consiste nell’“unione” G_{a}∪G della grammatica di partenza G_{a} con una grammatica G questa volta realmente qualsiasi; infatti l’evoluzione consiste nell’aggiungere nuovi simboli non-terminali e nuove regole di produzione alla grammatica di partenza: la grammatica di arrivo contiene dunque come sottoinsieme la grammatica di partenza.

Se noi però modifichiamo leggermente la definizione di step evolutivo da associarsi ad una regola di produzione in modo da comprendervi, oltre agli insiemi di simboli non-terminali e delle regole di produzione da aggiungere alla grammatica, anche quelli che devono essere eliminati dalla grammatica finora raggiunta, potremmo realmente conseguire il risultato per cui sia possibile raggiungere attraverso l’evoluzione una grammatica qualsiasi: infatti, una volta aggiunte le regole di produzione e i simboli relativi ad una grammatica G qualsiasi, con un meccanismo analogo potremmo eliminare le regole di produzione e i simboli relativi alla grammatica di partenza G_{a}, riducendoci così alla sola G.

Tuttavia è falsa l’affermazione secondo la quale il linguaggio L[E(G_{a})] generato dalla evoluzione può essere un qualsiasi linguaggio di L(T_{CF}). Ogni frase x appartenente a L[E(G_{a})] assume la forma generale x = yz, dove y è una frase del linguaggio Z_{a} di partenza necessaria a definire la forma successiva del linguaggio, z è invece la frase di un linguaggio qualsiasi L scelto in L(T_{CF}).

Essere in grado di descrivere agevolmente un qualsiasi linguaggio in L(T_{CF}) ancora non può essere considerato sufficiente se lo scopo è di prendere in esame linguaggi di programmazione; sappiamo infatti che essi per via di certi particolari costrutti non appartengono alla classe dei context-free. Altre potenzialità dei linguaggi evolventi ancora non emerse ci possono consentire di superare anche questo ostacolo.

Definiamo una grammatica evolvente G_{b} ed un linguaggio ad essa associato Z_{b}, che rispetto a Z_{a} ha in più la possibilità di poter esprimere su ogni singola linea, oltre alle nuove regole di produzione da aggiungere a G_{b} per far procedere l’evoluzione, anche gli step evolutivi ΔG da associare alle nuove regole di produzione (con Z_{a} era possibile specificare solo regole di produzione a step evolutivi nulli).

Immaginiamo ad esempio che la linea:

            STATEMENT -> declare integer IDENTIFICATORE MINUSCOLO
                :  { STATEMENT -> print integer 'IDENTIFICATORE MINUSCOLO' }

sia una valida frase del linguaggio Z_{b}. Una volta che il parser l’abbia ridotta, l’evoluzione grammaticale ha fatto un passo avanti e si è passati ad una nuova grammatica G^{1} contenente la nuova regola di produzione:

                   STATEMENT → declare integer IDENTIFICATORE MINUSCOLO

a cui è associato lo step evolutivo nonnullo:

ΔG = (ΔV_{N} = {},

                  ΔΦ = {STATEMENT → print integer ′IDENTIFICATORE MINUSCOLO′})

Il linguaggio generato dall’evoluzione ora è in grado di accettare frasi del tipo:

                                declare integer stanislao
                                print integer stanislao

ma di rifiutare una frase del tipo:

                                 print integer gianmarco

se non sia stata preceduta dalla frase dichiarativa:

                                declare integer gianmarco

E’ chiaro che una grammatica evolvente G_{b} può essere la capostipite di una evoluzione grammaticale in grado di generare linguaggi appartenenti ad una classe ben più ampia di L(T_{CF}); grazie al linguaggio Z_{b} che ci consente di accrescere le grammatiche della evoluzione con regole di produzione a step evolutivi nonnulli, possiamo addirittura giungere alla definizione di linguaggi context-sensitive.

Spero che a questo punto risulti chiara l’affermazione:

Il linguaggio usato per specificare l’evoluzione grammaticale può essere il linguaggio generato dall’evoluzione grammaticale stessa.

Naturalmente per linguaggio generato dall’evoluzione grammaticale si intende quello dovuto ai passi dell’evoluzione grammaticale precedenti; non può certo essere quello dovuto ai seguenti. In sostanza Z_{b} è il linguaggio generato dalla prima grammatica della evoluzione G_{b} = G^{0}; con Z_{b} stiamo specificando i successivi passi G_{1}, G_{2}, …, della evoluzione grammaticale. In generale possiamo specificare una nuova grammatica G^{i+1} della evoluzione attraverso non soltanto il linguaggio generato dalla prima grammatica L(G^{0}), ma anche attraverso quello generato da tutte le successive, fino anche a L(G^{i}).

Grazie a questa proprietà è possibile costruire un linguaggio aggiungendo strutture via via più complicate, definite utilizzando le strutture più semplici introdotte in precedenza. Nella realizzazione dell’APESE si è fatto un uso intensivo di questa proprietà.

3.5 PARSER DINAMICI

Naturalmente una volta sviluppata la teoria dei linguaggi e delle grammatiche evolventi, è leggittima la domanda: esiste per i linguaggi evolventi un algoritmo di parsing efficiente?

La risposta è si. Il parser dinamico è un parser che utilizza proprio tale algoritmo. Un parser dinamico può essere un convenzionale parser LR (a lettura left-right), provvisto di tavole di parsing inizializzate ad implementare la grammatica di partenza G^{0}, con la fondamentale caratteristica di essere in grado di cambiare le sue tavole a parsing-time, cioè durante la fase di parsing del flusso in entrata.

Di fatto per il progetto APE100 è stato sviluppato un parser dinamico chiamato ZzParser, implementante una grammatica iniziale G_{z} che all’atto pratico è una ulteriore elaborazione della grammatica dell’esempio G_{b}; il linguaggio Zz che ne risulta è certamente un ambiente più agevole di sviluppo.

Rispetto al linguaggio Z_{b}, fra le altre cose, il linguaggio Zzconsente di associare alle regole di produzione in ingresso, oltre agli step evolutivi ulteriori, anche le “azioni semantiche”; le azioni semantiche sono specificate attraverso routine C linkate al parser, da eseguirsi al momento della riduzione della regola ad esse associata.

Il parser Zz è il cuore del compilatore per l’APESE; la definizione della grammatica per l’APESE è avvenuta attraverso il linguaggio Zz.

Ulteriori specifiche sul linguaggio Zz sono contenute nel documento “ZzLanguage” riportato in bibliografia.

3.6 FORMULAZIONI ALTERNATIVE

La teoria dei linguaggi evolventi è una teoria ancora relativamente giovane, sebbene è possibile ravvisare in nuce alcune idee fondamentali già in alcuni articoli del 1963. La teoria dei linguaggi evolventi ha ricevuto stimoli importanti soprattutto negli ultimi 3 o 4 anni. Chiunque fosse interessato ad avere un quadro completo dello sviluppo della teoria, dai primi pioneristici lavori fin quasi alle sistematizzazioni dei giorni nostri, può consultare l’articolo di Christiansen (cfr. la bibliografia, p.37) ove è riportato un succinto riepilogo dei passi principali.

A causa della sua relativa “giovinezza” la teoria si presenta ancora non sistematizzata soddisfacentemente; alcuni punti andrebbero precisati ulteriormente. Attualmente poi esistono diverse formulazioni concorrenti, differenti fra di loro sia per quanto riguarda gli aspetti più squisitamente teorici, sia per quanto riguarda le applicazioni conseguenti.

In questa tesi è stata illustrata la formulazione dovuta a Cabasino, Paolucci, Todesco (nel seguito indicata come formulazione CPT), quella cioè a mio giudizio personale ritenuta più soddisfacente. Accenneremo tuttavia ai lavori di altri autori che hanno condotto ad idee e formulazioni alternative della teoria; soprattutto con lo scopo di fornire le motivazioni della mia scelta.

Il primo lavoro che sicuramente contiene alcune idee alla base della teoria è quello del 1989 di Heering, Klint, Rekers sulla generazione incrementale dei parser. La riflessione degli autori verte sulla osservata inefficienza dei sistemi tradizionali di parsing e di generazione di parser. La procedura nei sistemi tradizionali consisteva nella generazione di un parser specializzato per il riconoscimento di un singolo linguaggio, attraverso un generatore di parser, ad esempio yacc (yet another compiler of compilers), standard dei sistemi UNIX; il generatore di parser è nel suo lavoro ovviamente guidato dalla grammatica context-free fornitagli per rappresentare il linguaggio che deve essere riconosciuto.

Gli autori sono piuttosto preoccupati dalla eccessiva perdita di tempo che una tale procedura può causare durante la fase di design di un linguaggio e del relativo compilatore. Ogni volta che infatti si decide una modifica anche lieve al linguaggio e alla grammatica che lo rappresenta, è necessario azzerare tutto il lavoro fino a quel momento svolto ricominciando con la generazione di un nuovo diverso parser. Bisogna considerare che spesso viene privileggiata l’efficienza del parser rispetto al generatore che lo produce, come è nel caso di

yacc.

Attraverso una diversa redistribuzione delle operazioni della procedura da compiere, gli autori giungono alla realizzazione di un sistema in grado di “aggiornarsi” in tempi minori nel caso di modifiche non sostanziali alla grammatica; il sistema possiede poi diverse altre interessanti caratteristiche, ad esempio il fatto che vengono generate le sole parti del parser necessarie al riconoscimento dei costrutti linguistici effettivamente incontrati.

Si è comunque, per quello che mi è dato di vedere, ancora abbastanza lontani dal concetto di parser dinamico, un parser in grado di ricevere modifiche non soltanto nella fase iniziale, ma anche durante il parsing stesso; e inoltre con l’importante caratteristica di far coincidere realmente le due fasi di generazione del parser e del parsing in una unica fase.

Le prime idee sulle grammatiche modificabili sono dovute invece a Burshteyn, illustrate nel suo articolo del 1990 (Burshteyn 1990a), riprese da Christiansen (Christiansen 1990) e in seguito formalmente sistemate in un suo secondo articolo (Burshteyn 1990b).

La formulazione di Burshteyn differisce in molti punti dalla formulazione CPT. In alcuni la differenza è lieve: ad esempio, secondo Burshteyn in una grammatica modificabile possono ricevere modificazioni non soltanto gli insiemi dei simboli non-terminali e delle regole di produzione, ma anche quello dei simboli terminali.*

In genrale però le due formulazioni sono contrastanti. Burshteyn definisce una grammatica bottom-up modificabile (l’equivalente della grammatica evolvente della formulazione CPT) come una 7-pla:

G = {V, T, P, S, R, P_{s}, M}  (3.6.0)

* Nella formulazione CPT una tale eventualità non è stata presa in considerazione perché priva di effettive applicazioni pratiche.

Dovrebbe risultare piuttosto difficile far presente allo ZzParser che il simbolo celoe è un nuovo simbolo terminale del linguaggio da usarsi nelle dichiarazioni di variabili; non tanto per lo ZzParser, che sarebbe anche in grado di accettare una tale indicazione, quanto per l’operatore nel cercare il simbolo sulla sua tastiera.

dove V e T sono gli insiemi finiti o anche \underline{infiniti} rispettivamente dei simboli non-terminali e terminali; P è l’insieme dei finito o \underline{infinito} delle regole di produzione “context-free”; S è il simbolo scelto in V, detto di partenza. Inoltre è l’insieme di tutte le ricostruzioni possibili grazie a P; P_{s} è il sottoinsieme di P delle regole iniziali; M è una macchina di Turing.

La formulazione di Burshteyn presenta numerosi punti deboli. Il primo, più evidente, sta nel fatto che, come abbiamo ampiamente precisato nel capitolo precedente, le grammatiche formali sono state introdotte quali convenienti rappresentazioni finite dei linguaggi; sarebbe perciò un contraddire i propositi iniziali il voler definire grammatiche a insiemi di simboli terminali, non-terminali e delle regole di produzione infiniti.

Altro punto debole è il far consistere la grammatica anche nell’insieme di tutti le possibili ricostruzioni ottenibili avendo presente P: non occorre dimostrare che un tale insieme ha una grandezza (numero di elementi) paragonabile a quella di L stesso (l’insieme di tutte le frasi del linguaggio); anzi, come mi è apparso in seguito, anche maggiore.

Infine i particolari importanti che descrivono in quale modo e quando la grammatica si modifica sono tutti celati in quella specie di scatola nera che è la macchina di Turing M; M è l’unico elemento a determinare realmente le caratteristiche della grammatica e quindi del linguaggio accettato, risultando del tutto ininfluenti sia P che V e T.

Mi è risultata evidente un punto: l’autore nel momento in cui formalizzava la teoria, aveva presente piuttosto le applicazioni nella sua visione limitate alla sola risoluzione del problema del trasferimento in fase sintattica del riconoscimento dei costrutti context-sensitive del linguaggio. In questo contesto una grammatica modificabile è da vedersi in relazione ad un unico linguaggio, anche della classe context-sensitive, mentre l’infinitezza dell’insieme P delle regole di produzione si fa consistere in un insieme precostituito di semplici successioni del tipo:

                                      VARIABILE → a
                                      VARIABILE → b

⋯

                                      VARIABILE → z
                                      VARIABILE → aa
                                      VARIABILE → ab

⋯

La macchina di Turing determina poi, dipendentemente dagli stimoli ricevuti, quali delle regole della successione andranno in effetto, diventeranno attive e quali resteranno latenti.

In questo modo è stata comunque trovata un’alternativa al metodo dell’Attribute Grammar che in fase sintattica indicava semplicemente:

                                VARIABILE → IDENTIFICATORE

Nel caso volessimo utilizzare la formulazione di Burshteyn per descrivere Z_{b} o peggio ancora Zz, saremmo costretti a pensare P come l’insieme di tutte le in assoluto possibili regole di produzione, e V come l’insieme di tutti i simboli non-terminali immaginabili; così formulate, le definizioni per P e V non possono essere considerate operative. Infatti attraverso Z_{b} è possibile aggiungere alla grammatica non soltanto le regole scelte da un insieme P fissato in origine:

                                   P = {VARIABILE → a,

                                        VARIABILE → b,

⋯,

                                        VARIABILE → z,
                                        VARIABILE → aa,
                                        VARIABILE → ab,

⋯ } ma regole di produzione realmente qualsiasi.

Dunque Burshteyn ha presente il concetto di grammatica modificabile o evolvente, ma non ancora quello di linguaggio evolvente, di un linguaggio cioè dalle caratteristiche di Z_{b} o di Zz.

Per quanto riguarda le applicazioni pratiche della teoria, sia Burshteyn che Christiansen insistono su uno sviluppo, un miglioramento del metodo tradizionale dell’Attribute Grammar per il riconoscimento dei costrutti context-sensitive di un linguaggio. I cosiddetti attributi dei non-terminali vengono “sentiti” non più dalla fase semantica, ma sono stati passati alla fase sintattica.

Nell’approccio che presentiamo in questa tesi, al non-terminale con specifica di attributi vengono sostituiti diversi distinti non-terminali. Il riconoscimento dei costrutti context-sensitive è completamente sintattico.

CAPITOLO 4 IL LINGUAGGIO APESE

4.1 INTRODUZIONE

L’APESE è un linguaggio di programmazione evolvente, specificatamente ideato per i ricercatori che lavorano nel campo della fisica teorica alla risoluzione di problemi della teoria di campo QCD in simulazioni su reticolo. E’ realizzato per funzionare su macchine della famiglia APE100, ma compilatori APESE potrebbero essere prodotti per qualsiasi altro calcolatore.

I concetti alla base dell’APESE sono tali che l’APESE stesso può essere configurato per l’utilizzo in altri campi della fisica teorica, della chimica teorica o degli altri settori applicativi e nella compilazione verso altre macchine a elaborazione parallela o sequenziale. In questa direzione sono già stati compiuti altri tentativi di utilizzo nel campo della fluidodinamica.

Il linguaggio ha come punto di riferimento e riprende nelle strutture essenziali il tradizionale linguaggio usato nel campo scientifico, il FORTRAN, tenendo presenti e sviluppando inoltre elementi tipici di altri linguaggi di programmazione più potenti quali il C e l’ADA.

Una motivazione per la realizzazione di un nuovo linguaggio di programmazione è stata l’esigenza di un utilizzo efficiente delle macchine espressamente costruite per essere indirizzate alla risoluzione dei particolari problemi della fisica teorica. Il nuovo linguaggio può essere considerato l’anello di congiunzione fra l’esigenza di potenza e velocità di calcolo che il programmatore desidera per i propri specifici algoritmi e la macchina in grado di soddisfare questa esigenza; è in questo senso un linguaggio specializzato, specificamente efficiente.

Il compilatore di un tale linguaggio dunque da una parte risente delle specifiche degli algoritmi della fisica teorica verso cui è indirizzato, dall’altra della struttura hardware della macchina.

E’ bene precisare che l’utilizzo sulle macchine della serie APE100 di un compilatore FORTRAN non avrebbe mai consentito di giungere nella risoluzione di quei particolari problemi, a livelli di efficienza tali quali quelli che un programmatore APESE anche inesperto avrebbe a disposizione.

Un’altra motivazione che ha spinto alla realizzazione del linguaggio APESE deriva proprio dalla sua caratteristica “specializzazione”: il fisico teorico che fa uso del linguaggio APESE nella risoluzione dei suoi problemi, ritrova in esso gli elementi algebrici, gli operatori, e in generale le algebre con cui ha a che fare quotidianamente, allorché utilizza il foglio di carta e la penna.

Il vantaggio è una minore perdita di tempo: una volta che si sia impostato il problema nei termini del linguaggio matematico nel quale i fisici sono certamente piú familiari, non ci si trova di fronte al compito della traduzione verso il linguaggio in cui invece è familiare il calcolatore.

Infine la maggiore leggibilità del programma è pregio non indifferente in un’epoca che vede il lavoro di ricerca organizzato in equipes sempre più numerose, per cui risulta difficile per il singolo conoscere in poco tempo il lavoro e i progressi fatti dagli altri se questi sono codificati in simboli chiari alla macchina ma oscuri all’uomo.

Pur essendo l’APESE, nella sua versione finale rilasciata ufficialmente, un linguaggio essenzialmente indirizzato a programmatori che lavorano nel campo delle simulazioni della teoria di campo QCD su reticolo, ammette infiniti altri ulteriori sviluppi; sviluppi intesi sia nel senso di ulteriori strumenti e magari addirittura personalizzazioni per quanto riguarda l’orientamento QCD del linguaggio, sia nel senso di evoluzioni verso altri, distanti campi della fisica teorica. Non si nasconde però il fatto che questi sviluppi sono possibili solo per coloro i quali abbiano raggiunto una certa familiarizzazione col concetto di linguaggio evolvente e una certa esperienza nell’utilizzo dei parsers evolventi; in particolare nell’utilizzo del parser del linguaggio APESE, lo ZzParser, e del linguaggio iniziale con cui lo ZzParser è stato configurato, lo ZzLanguage.

Gli obiettivi che sono stati tenuti presenti durante il lavoro di realizzazione del progetto, sono stati in effetti due: il massimo della specificità e perciò della eleganza, del linguaggio nei confronti del campo della fisica teorica verso cui è rivolto; il massimo della efficienza nei confronti della risoluzione di quei problemi della fisica teorica sulla particolare macchina. Obiettivi questi niente affatto in competizione fra loro; anzi al contrario, il processo di conseguimento dei due si è trovato spesso a procedere coerentemente.

4.2 L’APESE

Il linguaggio APESE è stato definito utilizzando il linguaggio Zz, i cui tratti essenziali sono stati già delineati nel precedente capitolo.

La definizione della grammatica, cioè dell’insieme dei simboli non-terminali e delle regole di produzione relative all’APESE sono perciò contenute in files .ZZ, in grado di essere letti dallo ZzParser.

Il compilatore del linguaggio APESE è costituito dallo ZzParser e da un blocco di routine scritte in linguaggio C, linkate allo ZzParser.

Lo ZzParser assolve alla funzione di parsing del flusso di dati in entrata, ossia dei programmi scritti in linguaggio APESE. Naturalmente questo è reso possibile dal fatto che lo ZzParser è un parser dinamico. Al momento della sua invocazione lo ZzParser è in grado di accettare e riconoscere solo frasi del linguaggio Zz. Dopo che però gli sono stati fatti leggere i files .ZZ contenenti le definizioni per l’APESE, lo ZzParser modifica le proprie tavole di parsing e diventa così abile ad accettare un nuovo linguaggio. Il nuovo linguaggio è nient’altro che l’unione algebrica Zz∪ APESE di due linguaggi: il linguaggio Zz, il cui ulteriore utilizzo da parte del programmatore può essere utile a consentire nuove evoluzioni del linguaggio verso strutture ancora più complesse e compatte; il linguaggio APESE, di per se già sufficientemente completo e pronto ad affrontare qualsiasi problematica di tipo scientifico, in particolare problemi di simulazione della teoria QCD su reticolo.

Le routine C linkate allo ZzParser rappresentano la “fase semantica” del compilatore per il linguaggio APESE. Genericamente ciascuna di esse produce linee di codice oggetto sulla base degli argomenti che gli vengono passati; oppure non produce linee di codice oggetto ma si limita a preparare gli argomenti da passare ad un’altra routine (ad esempio questo è il caso della routine che riserva spazio in memoria per le variabili). Le routine C sono richiamate dallo ZzParser in corrispondenza alla riduzione delle regole di produzione definite per l’APESE; il linguaggio Zz infatti consente di specificare oltre alle nuove regole di produzione da aggiungere alla grammatica, ed oltre agli ulteriori steps evolutivi da associare alle nuove regole di produzione, anche le azioni, viste come chiamate alle routine C, da compiersi in corrispondenza alle nuove regole di produzione.

Essenzialmente l’APESE consta di due parti distinte: un nucleo, detto STD, rappresentante un linguaggio elementare, autoconsistente, assolutamente simile ad altri linguaggi di orientamento FORTRAN, contenente i tipi fondamentali di dati numerici, gli operatori con cui essi agiscono e le strutture di controllo; una estensione al nucleo STD, detta QCD, specificamente orientata alla programmazione di simulazione della QCD su reticolo, e contenente tipi e operatori di più frequente utilizzo nella teoria.

Le definizioni in linguaggio Zz relative alle due parti dell’APESE sono contenute in distinti files; cosicché il programmatore che non intenda avvalersi delle strutture del linguaggio orientate allo sviluppo della teoria su reticolo QCD in simulazioni su calcolatore, può evitare di sottoporre allo ZzParser le linee relative alla definizione della estensione QCD. Il prodotto che ottiene è un linguaggio scientifico simile al FORTRAN, con l’aggiunta di strutture di controllo del parallelismo e con la non trascurabile caratteristica di evolvenza; se ha una conoscenza sufficiente del linguaggio Zz e della teoria delle grammatiche formali, può avvalersi di tale caratteristica per definire tipi, operatori o genericamente strutture di personale utilizzo, creando così un suo proprio linguaggio personalizzato. Della caratteristica di evolvenza può avvalersi naturalmente anche chi abbia già incluso le estensioni QCD, potenziando così ulteriormente le possibilità dell’APESE.

4.3 NOTAZIONI - FORMALISMO DI BACKUS-NAUR

Per la descrizione del linguaggio APESE, utilizzeremo un formalismo molto comune in teoria della programmazione: il formalismo di Backus-Naur (BNF).

Incontreremo i simboli:

                                   <  >   ::=  |   {  }

che non devono essere considerati simboli del linguaggio APESE; propriamente essi sono meta-simboli del linguaggio BNF usato per descrivere l’APESE.

Il simbolo ‘::=’ è l’equivalente della freccia verso destra ‘→’, simbolo che abbiamo avuto modo di usare spesso nel corso dei capitoli 2 e 3. Il simbolo ‘::=’ cioè, compare ad indicare una regola di produzione; anzi più precisamente separa le parti sinistra e destra di una regola di produzione: a sinistra vi è il singolo simbolo non-terminale (così nello schema context-free), che può essere riscritto secondo la frase specificata a destra.

Affinché i simboli non-terminali siano distinti dai terminali, il formalismo BNF usa racchiudere i nonterminali entro le parentesi angolari ‘<’ e ‘>’.

Il simbolo ‘|’ che compare sempre a destra del simbolo ‘::=’, separa le differenti frasi in cui il simbolo non-terminale a sinistra può essere riscritto; in sostanza, consente di elencare su una stessa linea tutte le regole di produzione che hanno la caratteristica comune di avere lo stesso simbolo non-terminale nella parte sinistra; le frasi comprese fra un ‘|’ e l’altro possono tutte essere ridotte al simbolo non-terminale a sinistra del ‘::=’. Ad esempio:

                                     <A>::=<B>|<C>

sta per le regole di produzione:

\begin{matrix}A → B \\ A → C \end{matrix}

Infine i simboli ‘{’ e ‘}’, stanno ad indicare frasi composte da zero o più ripetizioni dei simboli contenuti all’interno di essi. Per fare un esempio:

                                      <A>::={<B>}

equivale a:

\begin{matrix}A → \\ A → A B \end{matrix}

Non forniremo lo sviluppo BNF del non-terminale <ident>, sul quale già ci siamo soffermati a lungo; ma alcune precisazioni su di esso sono necessarie. Un valido <ident> ha una lunghezza massima di 255 caratteri, e può anche contenere il simbolo ‘ ’ underscore. Il linguaggio è case-insensitive.

Il non-terminale <int> indica una evenienza di costante intera:

<int>::=<cifra>{<cifra>} <int>::= −<cifra>{<cifra>}

Il linguaggio APESE interpreta come valide le costanti intere comprese nell’intervallo −(2^{31} − 1) e (2^{31} − 1); questo rispecchiando la struttura hardware a 32 bit della parte della macchina che sovrintende al controllo del calcolo intero.

Il simbolo <real> indica una costante reale; le costanti reali vengono riconosciute per la presenza del punto ‘.’ decimale, o anche dalla specifica di un esponente (inteso in base 10):

<real>::=<cifra>{<cifra>}.{<cifra>}
<real>::= − <cifra>{<cifra>}.{<cifra>}
<real>::=<cifra>{<cifra>}.{<cifra>} E<cifra>{<cifra>}|

<cifra>{<cifra>}.{<cifra>} E − <cifra>{<cifra>}

<real>::= − <cifra>{<cifra>}.{<cifra>} E<cifra>{<cifra>}|

− <cifra>{<cifra>}.{<cifra>} E − <cifra>{<cifra>} Le costanti complesse <complex> sono costituite da una coppia di costanti reali separate da una virgola e racchiuse entro parentesi tonde.

<complex>::= (<real>, <real>)

Infine il simbolo <num> indicherà una qualsiasi evenienza di costante numerica: <int>, <real> o <complex>.

Possiamo considerare come simbolo di partenza della grammatica per l’APESE un ipotetico <statement list>, connesso a <statement> attraverso:

<statement list>::=<statement>

<statement list>::=<statement list> [eol] <statement> <statement list>::=<statement list> ; <statement>

ossia su ogni linea di programma deve trovarsi uno statement del linguaggio; in alternativa, diversi statement possono trovarsi su una stessa linea separati da ‘;’.

4.4 STD

La libreria STD, il nucleo del linguaggio, mette a disposizione i tipi più semplici, ossia integer, real, complex, delle classiche strutture dati, variabili e array. L’utente ha il controllo della modalità di allocazione dei dati attraverso gli storage modifiers static, register, physical register. I paragrafi seguenti descriveranno il sublinguaggio definito nella libreria STD, sottolineando ove occorra, differenze e analogie col linguaggio FORTRAN.

4.5 DICHIARAZIONE DI COSTANTI

Lo statement constant consente di associare a compilation time un valore numerico intero, reale o complesso ad un identificatore (simbolo); l’identificatore manterrà sempre lo stesso valore numerico lungo tutto il corso del programma. E’ in sostanza l’equivalente dello statement FORTRAN PARAMETER.

La sintassi relativa alla dichiarazione di una costante è:

<statement>::= constant <ident> = <expr> {, <ident> = <expr>}

L’espressione numerica <expr> può essere costituita da un valore numerico immediato, da un’altra costante definita precedentemente, da una vera e propria espressione coinvolgente le une e le altre. L’espressione viene valutata a compilation time.

Le linee seguenti sono validi statement con cui dichiarare costanti:

constant pi = 3.141592

constant N = 100, pi double = 2 ∗ pi

Il compilatore si comporta differentemente a seconda che incontri uno statement di dichiarazione intera o floating point; è un riflesso della struttura hardware dei calcolatori APE100 i quali trattano separatamente i due tipi di dato.

Per le costanti intere si limiterà a sostituire il simbolo, ovunque esso compaia, con il corrispondente valore numerico. Per le costanti floating point allocherà invece locazioni di memoria, provvedendo anche a che siano inizializzate al corretto valore nel momento del caricamento del programma; ogni riferimento lungo tutto il corso del programma al simbolo verrà tradotto in un riferimento alla locazione di memoria riservata per la costante.

La dichiarazione di costanti floating-point ha quindi un suo costo, trascurabile, se vogliamo, nella maggior parte dei casi.

4.6 DICHIARAZIONE DI VARIABILI E ARRAY STATICI

Lo statement di dichiarazione di variabili permanentemente allocate in memoria è:

integer <ident>{, <ident>} real <ident>{, <ident>} complex <ident>{, <ident>}

L’identificatore dichiarato per la variabile può essere utilizzato ovunque lungo il corso del programma in operazioni aritmetiche o in statement di assegnazione (a sinistra di un uguale).

Per gli array devono essere dichiarate le dimensioni:

integer <ident>[<int>{, <int>}] real <ident>[<int>{, <int>}] complex <ident>[<int>{, <int>}]

Al momento dell’utilizzo, la specifica dell’elemento dell’array avviene esprimendo come indice una espressione intera racchiusa entro parentesi quadre; nel caso di array pluridimensionali, occorrerà esprimere più indici, ciascuno separato dal successivo mediante una virgola.

Ecco un esempio di corrette dichiarazioni:

constant Nmax = 100, Mmax = 200
integer count
real a[50], b
complex z[Nmax, Mmax]

Per le variabili intere viene riservato spazio nella memoria globale intera della macchina. Le variabili floatingpoint sono invece allocate parallelamente nella memoria locale di ciascuna Floating Point Unit (FPU). APE100 è infatti strutturato in modo che la gestione della elaborazione intera sia centralizzata e a cura della CPU; la memoria relativa ai dati di tipo intero è perciò globale (una per tutta la macchina). La gestione della elaborazione floating-point è invece locale e a cura delle FPU; ciascuna lavora in parallelo eseguendo le stesse istruzioni delle altre ma operando su dati prelevati dalla propria memoria locale.

Per ogni variabile complex dichiarata vengono in realtà riservate 2 celle di memoria consecutive. Per i vettori complex viene riservato uno spazio di memoria pari al doppio della dimensione del vettore; ad ogni elemento del vettore sono associate due celle di memoria consecutive.

L’uso di variabili floating-point static causa ogni volta trasferimento dati da e verso memoria e registri, operazioni inevitabilmente lente. La libreria QCD che definisce tipi più complessi rispetto agli elementari real e complex, consente di ridurre sensibilmente i tempi di fetch dei dati in memoria sfruttando vantaggiosamente le caratteristiche di APE100 (gran numero di registri, possibilità di spostamento rapido di interi blocchi di dati fra memoria e registri); viene minimizzato il numero di trasferimenti fra memoria e registri, per mezzo di una più compatta e trasparente allocazione dei registri. Per cui il programmatore che si trova nella necessità di utilizzare una struttura dati identificante una matrice appartenente al gruppo SU(3), sarà sconsigliato dal dichiarare:

complex u[3, 3]

e consigliato ad utilizzare:

su3 u 4.7 DICHIARAZIONE DI VARIABILI REGISTER

L’utilizzo di variabili allocate sui registri anziché in memoria consente di diminuire i tempi di trasferimento dati fra memoria e registri.

La sintassi di dichiarazione di variabili register (ragionevolmente non potranno essere dichiarati register array) è ottenuta postponendo al tipo del quale si voglia dichiarare la variabile, la keyword register:

<type> register <ident> {, <ident>}

Ad esempio:

real register energy, vel complex register psi

La principale restrizione che limita l’utilizzo di variabili register è che ogni operazione del sorgente la quale introduca un salto o un entry-point (label), causa la perdita del valore immagazzinato nel registro associato (dinamicamente) alla variabile. La causa di questa restrizione è da ricercarsi nelle caratteristiche dell’ottimizzatore di microcodice che interviene a compilazione avvenuta.

Il programmatore deve fare perciò attenzione a che la variabile register sia stata correttamente inizializzata e che non abbia perduto l’inizializzazione prima che venga utilizzata (o riutilizzata). Cioè non deve introdurre label o istruzioni che inseriscano label fra l’inizializzazione e l’utilizzo di una variabile register.

4.8 DICHIARAZIONE DI VARIABILI PHYSICAL REGISTER

Il programmatore che trovi grande ostacolo a migliori performances del proprio prodotto nella limitazione cui soffrono le variabili register (perdita del valore in seguito a istruzione di salto o entry-point), può trovare un rimedio nella dichiarazione di variabili physical register. Il valore di tali variabili è sistemato permanentemente in registri cui esse sono staticamente associate, ed è perciò disponibile sempre.

Logicamente è possibile dichiarare solo un numero limitato di variabili physical register, compatibilmente con la dimensione del set di registri dei processori; inoltre si deve essere coscienti del costo di una tale scelta, consistente in maggiori difficoltà per il compilatore nella gestione delle normali operazioni a causa del ridotto set di registri che gli sono disponibili dopo la dichiarazione di molte variabili physical register

Il consiglio è di fare un utilizzo molto limitato delle variabili physical register, limitatamente a quei casi in cui certi valori ricorrano frequentemente lungo il corso dei programmi.

La sintassi di dichiarazione di variabili physical register è molto simile a quella relativa a variabili register:

<type> physical register <ident>

Come esempio di dichiarazione:

real physical register time 4.9 OPERATORI

Sui tipi fondamentali della libreria STD si può agire attraverso i consueti operatori aritmetici:

∗ / + −

Una espressione numerica è costituita da una combinazione qualsiasi di variabili o costanti di quello stesso tipo fatte interagire attraverso gli operatori aritmetici. A causa della struttura hardware della macchina che vede separate nettamente le parti in cui hanno luogo le elaborazioni intera e floating-point, il linguaggio non ammette conversioni automatiche da intero a reale; conversioni da reale a complesso si applicano invece dove è necessario.

Per il tipo complex, esistono gli operatori postfissi di estrazione .re e .im, i quali consentono il riferimento separato alle parti reali o immaginarie di qualsiasi oggetto di tipo complex, sia esso una costante, una variabile o un elemento di array. In particolare, la variabile o l’elemento di array complex a cui sia stato applicato un operatore di estrazione, viene considerato alla stregua di una variabile real che risieda in una delle due locazioni (dipendentemente dall’operatore usato) della variabile complex.

<real var>::=<complex var>.re <real var>::=<complex var>.im

Esiste poi sempre per il tipo complesso, l’operatore (postfisso) ‘˜ ’, il quale esegue l’operazione di coniugazione complessa sul fattore complesso cui si applica.

<complex fact>::=<complex fact>˜

L’ordine di valutazione di una espressione numerica avviene rispettando la scala delle priorità degli operatori, che è (dalla più alta alla più bassa):

\begin{matrix} & ˜ − (unario) \\ ∗ & / \\ + & − \end{matrix}

e per operatori a stessa priorità vale la regola dell’associatività a sinistra. Le parentesi tonde ‘(’ e ‘)’ consentono di forzare la priorità di valutazione dell’espressione contenuta al loro interno.

Infine lo statement di assegnazione del valore di una espressione ad una variabile (semplice o elemento di array) avviene attraverso il simbolo ‘=’:

<statement>::=<var> = <expr>

Presentiamo alcuni esempi generali di impiego degli strumenti finora descritti:

k = i + j + 1
z = (1.0, 2.0)
x = z.re
y = z.im
z prime = z˜
mod2 = z ∗ z˜

4.10 CONDIZIONI LOGICHE ED ESPRESSIONI BOOLEANE

Il controllo del flusso del programma avviene ad opera di relazioni logiche fra espressioni numeriche. Relazioni logiche fra espressioni intere controllano globalmente l’intera macchina; relazioni fra espressioni floatingpoint controllano il flusso del programma localmente (rompendo cioè il parallelismo nella esecuzione) o anche globalmente, per mezzo degli operatori di conversione da condizione logica locale a condizione logica globale all e any. Gli operatori elementari di relazione logica che fissano delle condizioni logiche mettendo in relazione espressioni numeriche, sono:

== != > < >= <=

corrispondenti rispettivamente agli operatori FORTRAN:

.EQ. .NE. .GT. .LT. .GE. .LE.

Una relazione fra espressioni di tipo intero verrà interpretata dal compilatore come una condizione logica globale:

<global logical condition>::=<integer expr> == <integer expr>|

<integer expr> != <integer expr>| <integer expr> > <integer expr>| <integer expr> < <integer expr>| <integer expr> >= <integer expr>| <integer expr> <= <integer expr>

Una relazione fra espressioni di tipo reale sarà una condizione logica locale:

<local logical condition>::=<real expr> == <real expr>|

<real expr> != <real expr>| <real expr> > <real expr>| <real expr> < <real expr>| <real expr> >= <real expr>| <real expr> <= <real expr>

Sono ottenibili condizioni logiche composte attraverso l’utilizzo degli operatori booleani ‘!’ (operatore unario .NOT.), ‘&&’ (operatore binario .AND.), ‘||’ (operatore binario .OR.). Si utilizzano nella sintassi:

! <logical condition> <logical condition> && <logical condition> <logical condition> || <logical condition> ove le <logical condition> possono essere globali o locali; la condizione logica composta risulterà globale o locale dipendentemente dai termini dell’espressione booleana; in una espressione logica non possono mai essere mescolate condizioni globali con condizioni locali. Sono possibili espressioni comunque complicate coinvolgenti condizioni logiche dello stesso tipo e operatori booleani, tenendo presente che il compilatore le valuta attraverso la tabella delle priorità degli operatori (dalla più alta alla più bassa):

!

&& ||

associando inoltre da sinistra a destra operatori a stessa priorità. La precedenza degli operatori in una espressione può essere alterata facendo uso di parentesi tonde ‘(’ ‘)’.

Condizioni logiche locali possono essere convertite in condizioni logiche globali attraverso l’uso dell’operatore unario prefisso all (la condizione logica globale risultante è vera se la condizione locale espressa a destra dell’operatore è vera localmente su tutti i nodi) o dell’operatore unario prefisso any (la condizione globale risultato è vera se è vera su almeno un nodo la condizione locale operando).

<global logical condition>::= all <local logical condition> <global logical condition>::= any <local logical condition>

4.11 ISTRUZIONI DI CONTROLLO DEL FLUSSO GLOBALE

In APESE esistono statement di controllo del flusso globale del programma molto simili a quelli di altri linguaggi. Essi modificano l’esecuzione dipendentemente dai valori assunti da condizioni logiche globali (sia intere che floating-point).

Il modo più immediato di avere il controllo sul flusso è mediante l’istruzione if, la cui sintassi di invocazione non differisce poi molto da quella in uso nel FORTRAN:

if(<global condition>) {<statement>; } else {<statement>; } endif

Esistono poi in APESE altri statement vicini ai cosiddetti linguaggi a programmazione strutturata, quali il while:

while(<global condition>) {<statement>; } endwhile

il quale ripete l’esecuzione del blocco di statement fintantoché la <global condition> risulta verificata; inoltre lo statement repeat:

repeat

{<statement>; } until(<global condition>)

simile al while, con la differenza che il check sulla condizione globale viene eseguito ogni volta al termine della esecuzione del blocco.

Inoltre c’è il classico for (il DO FORTRAN) nella sintassi:

for <integer var> = <integer expr> to <integer expr> step <integer expr> {<statement>; } endfor 4.12 ISTRUZIONI DI CONTROLLO DEL FLUSSO LOCALE

Lo statement where introduce una variazione locale nella esecuzione del programma sulla base del check di una condizione logica locale:

where(<local logical condition>) {<statement>; } endwhere

L’effetto del where è quello di eseguire il blocco di statement tra parentesi graffe solo sui nodi nei quali risulti verificata la condizione logica, lasciando in pausa i nodi nei quali non è verificata. In realta sul calcolatore APE100, i nodi nei quali non è verificata la condizione continuano l’esecuzione del programma, essendo però in essi disabilitate le operazioni di scrittura in memoria o sui registri; il che è equivalente a pensarli in esecuzione di un programma costituito da tutti NOP (NOP è l’operazione nulla).

4.13 SUB

La libreria SUB definisce per l’APESE le strutture del linguaggio che controllano il trasferimento del flusso del programma verso sottoblocchi del programma, subroutine e function.

Lo scopo di subroutine e function è quello di suddividere grandi blocchi di calcolo in piccole parti più semplici. Inoltre subroutine e function celano i dettagli costitutivi del loro modo di agire e di funzionare che spesso non è necessario conoscere (spesso anzi possono essere state scritte da persona diversa da quella che poi le utilizza), essendo però nello stesso tempo immediate nell’indicare l’effetto e i risultati della loro azione.

Il risultato che si ottiene è uno stile di programmazione più trasparente e chiaro; inoltre va anche considerata la maggiore facilità con cui in un secondo tempo è possibile apportare modifiche a programmi che risentano di una tale struttura.

4.14 DICHIARAZIONE DI SUBROUTINE E FUNCTION

Subroutine e function devono essere dichiarate prima di essere richiamate all’interno della parte principale del programma.

Le linee:

declare subroutine <ident> <type> <ident>()

ove <type> può stare per integer, real, complex o qualunque altro tipo più complesso definito in APESE, informano il compilatore rispettivamente dell’esistenza di una subroutine e di una function aventi il nome indicato (sotteso da <ident>).

La subroutine ha il carattere di uno statement creato dal programmatore. La function invece ritorna un valore numerico dello stesso tipo in cui è stata dichiarata, calcolandolo a partire dagli eventuali suoi argomenti; per cui può essere utilizzata all’interno di espressioni numeriche.

E’ conveniente dichiarare della subroutine o della function, oltre che il nome anche gli argomenti, ovverosia il prototipo; il prototipo è la specifica del numero totale degli argomenti necessari al sottoblocco, dei tipi degli argomenti stessi, dell’ordine con cui essi andranno espressi al momento della chiamata e al momento della definizione del sottoblocco. Il prototipo si dichiara facendo seguire al nome della subroutine o della function l’elenco dei tipi degli argomenti:

declare subroutine <ident>( <type> {, <type>} )

Gli argomenti possono essere passati dal programma principale secondo due modalità: per puntatore o per valore.

Il passaggio di una variabile o di un intero array per puntatore (di default) consente a che il programma principale e il sottoblocco condividano la variabile o l’array passato come argomento; essi cioè fanno riferimento, magari utilizzando nomi diversi, alle stesse locazioni di memoria, ed ogni variazione di valore apportato ad esse da uno dei due blocchi viene necessariamente risentito dall’altro.

Il passaggio per valore di una variabile, dell’elemento di un array o anche di una costante o di una espressione numerica coinvolgente variabili e costanti, fa si invece che nella subroutine o nella function sia dichiarata, in corrispondenza all’argomento, una nuova variabile interna alla subroutine o alla function stessa; la variabile viene inizializzata al valore passato dal programma principale ogni volta che viene richiamato il sottoblocco; ad ogni variazione del valore di tale variabile all’interno della subroutine o della function, non corrisponderà una analoga variazione della eventuale variabile indicata, nel blocco principale, quale argomento della subroutine o della function. La dichiarazione di passaggio per valore di un certo argomento va specificata esprimendo, dopo il tipo dell’argomento, la keyword value.

Ecco alcuni esempi di dichiarazione di sottoblocchi:

declare subroutine swap(complex, complex) real radice ennesima(real value, integer value)

La specificazione del prototipo al momento della dichiarazione della subroutine o function non è obbligatoria; tuttavia è consigliabile, poiché si concede al compilatore la possibilità di prevenire banali errori del programmatore (chiamata della subroutine o function con numero di argomenti insufficiente o eccedente, o posti in posizione diversa da quella in cui la subroutine o function li interpreterebbe correttamente), controllo che altrimenti non avverrebbe.

Il programmatore che conosce profondamente il sistema di compilazione tuttavia, può avvalersi della possibilità di non specificare il prototipo, nell’intento di avere a disposizione uno strumento che si presta ad operazioni più “ardite” (ad esempio, passaggio come argomento di una variabile di un certo tipo che viene riletta dal sottoblocco come di un altro tipo). E’ inutile dire che un non corretto e non cosciente utilizzo di tali risorse conduce a risultati disastrosi.

4.15 INVOCAZIONE DI SUBROUTINE E FUNCTION

Una volta dichiarate, subroutine e function possono essere invocate ovunque all’interno del programma principale. Le subroutine sono invocate specificando su una nuova linea di programma, il nome e gli eventuali argomenti tra le parentesi tonde (nome più parentesi tonda aperta e chiusa immediatamente dopo nel caso di subroutine senza argomenti).

Le function vanno invocate esprimendo come per le subroutine nome e argomenti, in un contesto di programma compatibile con il tipo per loro dichiarato. Esse possono perciò comparire ad esempio in espressioni numeriche del loro stesso tipo o a sinistra di una variabile di nuovo del loro stesso tipo.

Nel caso per il sottoblocco siano stati dichiarati argomenti da passarsi per puntatore, è sufficiente esprimere nella posizione dovuta, all’interno delle parentesi tonde, la variabile o l’array che nella situazione specifica deve essere passato.

Nel caso di argomenti da passarsi per valore, occorre far precedere all’espressione (genericamente) il cui valore calcolato si intende passare, la keyword value of.

Ad esempio:

a = radice ennesima(b by value, n by value)
swap(u, w)

Ogni invocazione ad una subroutine o ad una function introduce nel listato assembler una label, delimitando così un blocco di ottimizzazione oltre il quale non sopravvivono i valori di variabili register.

4.16 DEFINIZIONE DI SUBROUTINE E FUNCTION

I corpi costituenti le subroutine e le function vanno posti separati dal blocco costituente il programma principale (oltre lo statement di end). L’inizio del blocco relativo ad una subroutine o ad una function va evidenziato attraverso la linea di titolo (header):

subroutine <ident>(<argument list>)

per le subroutine; <type> function <ident>(<argument list>)

per le function.

Sulla linea di header vanno dichiarati nomi e tipi degli argomenti e la modalità con cui avviene il loro passaggio fra programma principale e sottoblocco; i nomi dichiarati identificheranno l’argomento localmente e saranno invisibili al main; potranno però rappresentare variabili e array conosciuti anche dal main ma con nome differente (caso di passaggio per puntatore), oppure vere e proprie variabili locali, solo inizializzate al valore specificato alla chiamata nel main (passaggio per valore).

La dichiarazione del singolo argomento avviene esprimendone il tipo, il nome e la keyword value nel caso di passaggio per valore:

<argument>::=<type> <ident>|<type> <ident> value

Subito dopo la linea di header, racchiuso fra graffe inizia il blocco costituente la subroutine o function. In esso potranno essere utilizzate le variabili dichiarate come argomenti nella linea header; oppure variabili globali, viste cioè (con lo stesso nome) da main e sottoblocchi; o ancora nuove variabili dichiarate localmente all’interno delle graffe.

Prima dell’ultima graffa chiusa racchiudente il blocco della subroutine o function, deve essere stata sistemata l’istruzione di return:

return

per le subroutine; return(<expr>)

per function. La <expr> deve essere dello stesso tipo dichiarato per la function.

L’istruzione di return causa la restituzione immediata del controllo al programma principale che aveva chiamato il sottoblocco, il quale può così riprendere l’esecuzione esattamente dal punto nel quale era stata lasciata; nel caso delle function, l’istruzione return ha il compito di indicare il valore numerico da ritornare al programma principale che lo aveva richiesto.

4.17 QCD

La libreria QCD definisce tipi e operatori utili per la risoluzione dei problemi di simulazione della teoria di campo QCD su reticolo; pertanto gli utenti che lavorano in altri campi della fisica non avranno la necessità di includere tale libreria. In tal caso potranno avvalersi del solo kernel dell’APESE o includere librerie proprie definenti tipi e operatori di personale utilizzo.

Nella libreria QCD sono definiti i tipi su3, spinor, colorspin, spinspin, al solito nelle strutture dati variabile semplice o array mono o pluridimensionale, e nelle modalità di allocazione static, register e physical register, imitando in questo i tipi semplici definiti in STD.

4.18 TIPO SU3 - DICHIARAZIONE DI VARIABILI E ARRAY

Dati dichiarati di tipo su3 sono in relazione a elementi appartenenti al gruppo algebrico SU(3), ossia il gruppo di matrici speciali e unitarie la cui rappresentazione fondamentale è in 3 × 3 dimensioni, o più esattamente il gruppo generato dall’algebra di Lie composta dall’insieme chiuso rispetto a relazioni di commutazione, di 3 × 3 − 1 generatori hermitiani a traccia nulla, linearmente indipendenti. La dichiarazione di una variabile di tipo su3 avviene in maniera simile alla dichiarazione dei tipi più semplici contenuti in STD.

su3 <ident>

Dal momento della dichiarazione in poi all’identificatore sarà associata una variabile che potrà essere in relazione di volta in volta ad un qualsiasi elemento del gruppo SU(3). Le variabili su3 potranno essere dichiarate static, register, o physical register a seconda che l’utente per esse riservi spazio in memoria o desideri evitare i lunghi tempi di trasferimento dati fra memoria e registri, allocandole direttamente sui registri. Potranno essere dichiarati anche array mono o pluri-dimensionali di tipo su3 (in tal caso rigorosamente in memoria). La sintassi di dichiarazione di variabili su3 è estremamente simile a quella definita in STD per i tipi più semplici.

Forniamo alcuni esempi di dichiarazione:

\begin{matrix}su3 temp \\ su3 U[Nx, Ny, Nz, Nt, Nmu], random[20] \end{matrix}

L’utente accede ai dati di tipo su3 leggendoli come fossero i dati dell’elemento del gruppo SU(3) posto nella sua rappresentazione fondamentale, ossia di matrice 3 × 3 a elementi complessi. Egli infatti in qualunque momento può accedere ad un qualsiasi elemento della matrice semplicemente indicandone riga e colonna; l’accesso avviene tramite l’operatore postfisso ‘.’:

<su3 object>.[<num riga>, <num colonna>]

ove al posto di <num riga> e <num colonna> possiamo esplicitare una qualsiasi espressione di tipo intero (naturalmente il cui risultato sia compreso fra 0 e 2, totale del numero di righe o colonne di una matrice di SU(3) ); nel caso l’<su3 object> sia una variabile di tipo statico, l’espressione intera può anche essere di tipo calcolato a run-time (ossia comprendente variabili il cui valore non può che essere noto a run-time); se <su3 object> è una variabile di tipo register o physical register l’espressione intera specificante la riga o la colonna può coinvolgere solo costanti il cui valore sia noto già a compilation-time; questo è la conseguenza delle particolari caratteristiche costruttive con cui è stato realizzato APE.

In generale dati 9 numeri complessi non è dato un elemento appartenente al gruppo SU(3); cosicché l’utente non può inizializzare arbitrariamente i 9 termini della matrice quale egli vede attraverso l’operatore di estrazione. Né tantomeno egli può aspettarsi che il compilatore per ogni elemento di tipo su3 conservi tutti i 9 termini complessi associati ad esso nella rappresentazione fondamentale; sarebbe un inutile utilizzo di memmoria e in generale di risorse.

Nel caso limite si può individuare un elemento del gruppo SU(3) attraverso soli 8 reali; ossia attraverso i coefficienti delle matrici di Gellmann nella rappresentazione esponenziale delle matrici del gruppo:

e^{iα_{i}λ^{i}}, i = 1, …, 8

Visto che però nei programmi è maggiormente sentita l’esigenza di accedere agli elementi di SU(3) attraverso la loro rappresentazione di matrici 3 × 3 complesse, e visto che per ottenere il singolo complesso individuato dal numero di riga e di colonna a partire dagli 8 reali della rappresentazione esponenziale sono necessari un numero notevole di calcoli, si è deciso di descrivere dati di tipo su3 attraverso una rappresentazione intermedia: il compilatore conserva di ogni matrice le sole prime due righe, per un totale di 6 complessi ovverosia 12 reali. Ne risulta un effettivo guadagno di memoria rispetto alla situazione nella quale erano ritenute le tre righe (18 reali), ma anche una non eccessiva difficoltà nella ricostruzione degli elementi dell’ultima riga mancante. Ulteriori precisazioni e i particolari di funzionamento riguardanti questo argomento sono rinviati al capitolo successivo.

4.19 OPERATORI AGENTI SUL TIPO SU3

Definite le caratteristiche generali del tipo su3, diventa prioritaria una sistemazione elegante ed efficiente dell’algebra definita in APESE per il nuovo tipo. Gli obiettivi sono quelli indicati nel capitolo 1: è infatti assolutamente necessario, in vista del conseguimento di un risultato veramente elegante ed efficiente, tenere presenti le applicazioni reali cui principalmente tutto questo lavoro è finalizzato.

Per il tipo su3 è stata definita tutta una serie di operatori unari e binari; nella definizione di ciascuno si sono seguiti due principi generali: l’operatore dovrà mostrare elegantemente e chiaramente il risultato della propria azione celando allo stesso tempo la complicata realtà di operazioni in cui la sua azione si traduce. Operazioni che in ultima analisi sono quelle da eseguirsi sui termini elementari del tipo, ossia i dati di tipo reale che al livello hardware sono gli unici ad essere conosciuti dalla macchina.

Il primo operatore che deve essere definito nella libreria QCD per il tipo su3 è quello che rappresenta l’operazione del gruppo SU(3); in APESE è stato indicato col simbolo ‘∗’ infisso tra i due termini sui quali opera:

<su3 object> ∗ <su3 object>

Essendo il gruppo SU(3) per definizione chiuso sotto la sua operazione, l’operatore ‘∗’ conduce a partire da due elementi del gruppo verso un altro elemento nel gruppo. Ad esempio nel caso sia richiesto che la variabile di tipo su3 a contenga, rappresenti l’elemento del gruppo ottenuto facendo operare b e c, altrettanti elementi del gruppo/variabili di tipo su3 precedentemente inizializzate, sarà sufficiente in APESE esprimere la linea:

a = b ∗ c

Il compilatore penserà da solo a inserire nel listato assembler tutte le azioni in cui tale operazione deve tradursi. Per dare un’idea di tutto quanto avviene, immaginiamo che il compilatore per ogni elemento di tipo su3 conservi i 9 valori complessi costituenti la matrice 3 × 3 della rappresentazione fondamentale del gruppo; l’operazione del gruppo sarà allora rappresentata dal prodotto righe per colonne fra le matrici della rappresentazione. La traduzione al livello inferiore dell’operazione b ∗ c dovrebbe dunque risultare:

a.[0, 0] = b.[0, 0] ∗ c.[0, 0] + b.[0, 1] ∗ c.[1, 0] + b.[0, 2] ∗ c.[2, 0]
a.[0, 1] = b.[0, 0] ∗ c.[0, 1] + b.[0, 1] ∗ c.[1, 1] + b.[0, 2] ∗ c.[2, 1]
a.[0, 2] = b.[0, 0] ∗ c.[0, 2] + b.[0, 1] ∗ c.[1, 2] + b.[0, 2] ∗ c.[2, 2]
a.[1, 0] = b.[1, 0] ∗ c.[0, 0] + b.[1, 1] ∗ c.[1, 0] + b.[1, 2] ∗ c.[2, 0]
a.[1, 1] = b.[1, 0] ∗ c.[0, 1] + b.[1, 1] ∗ c.[1, 1] + b.[1, 2] ∗ c.[2, 1]
a.[1, 2] = b.[1, 0] ∗ c.[0, 2] + b.[1, 1] ∗ c.[1, 2] + b.[1, 2] ∗ c.[2, 2]
a.[2, 0] = b.[2, 0] ∗ c.[0, 0] + b.[2, 1] ∗ c.[1, 0] + b.[2, 2] ∗ c.[2, 0]
a.[2, 1] = b.[2, 0] ∗ c.[0, 1] + b.[2, 1] ∗ c.[1, 1] + b.[2, 2] ∗ c.[2, 1]
a.[2, 2] = b.[2, 0] ∗ c.[0, 2] + b.[2, 1] ∗ c.[1, 2] + b.[2, 2] ∗ c.[2, 2]

Queste linee producono un risultato per a assolutamente corretto ma si dimostrano del tutto inefficienti per quanto riguarda il tempo di esecuzione necessario a portare a termine l’operazione e il consumo di risorse.

E’ necessario ad ogni costo evitare di inserire in un programma APESE una serie di linee di programma sul genere di quelle mostrate; ed è anche necessario abituarsi a pensare che il compilatore APESE non si comporti nei confronti dell’operatore ‘∗’ fra su3 e degli altri che vedremo, alla stregua di un semplice “espansore”. E dall’analisi del codice oggetto risultato della compilazione possiamo capire perché questa soluzione è inefficiente.

Dobbiamo infatti considerare che ogni singola linea del sorgente risultante da una programmazione diretta o da una espansione, viene compilata separatamente dalle altre (il compilatore possiede un campo di ottimizzazione i cui limiti vanno da un carattere di “a capo” all’altro, di fatto riuscendo a vedere una sola linea alla volta); dunque ognuna delle 9 espressioni nelle quali l’operazione b ∗ c si espande viene valutata separatamente dalle altre e, in particolare, ogni fattore delle espressioni viene caricato (o ricaricato) sui registri ogni volta che compare. In totale avremmo 54 (9 × 6) trasferimenti da memoria a registri, molti dei quali sono inutili ripetizioni di trasferimenti di uno stesso elemento (ciascun elemento di b o di c compare, e quindi viene ritrasferito, per ben 3 volte).

Possiamo migliorare l’ASSEMBLER relativo all’operazione b∗c semplicemente eseguendo i trasferimenti da memoria verso registri di tutti gli elementi costituenti b e c una volta per tutte, immediatamente prima dell’esecuzione delle operazioni aritmetiche. Sfruttando il fatto che gli elementi di b o di c sono allocati consecutivamente, si può far uso della operazione rapida di trasferimento la quale indirizza globalmente l’intero insieme di dati; si risparmiano così molte delle elaborate operazioni per il calcolo degli indirizzi, inevitabili allorché si effettuano accessi ai singoli elementi costituenti gli insiemi di dati relativi a b o a c.

Nella successiva valutazione delle espressioni, i singoli elementi a fattori della espressione saranno riferiti non alle locazioni di memoria nelle quali normalmente risiedono, ma ai registri sui quali sono stati appena trasferiti e dove temporaneamente risiedono.

Infine, dopo che sono state valutate le 9 espressioni, si provvede a trasferire i risultati in un solo blocco verso le locazioni di memoria consecutive relative alla variabile statica a, risparmiando ulteriori operazioni di indirizzamento.

Si capisce ora perché in APESE è sconsigliabile dichiarare matrici di SU(3) sotto forma di vettori di complessi; in tutti i casi in cui siano prese in considerazione le operazioni “tradizionali” del gruppo SU(3), il compilatore si mostra infatti notevolmente più efficiente nei confronti di dati dichiarati di tipo su3.

L’operazione ‘∗’ trova frequente utilizzo in tutte le applicazioni di simulazione della teoria QCD su calcolatore; un esempio semplice di utilizzo è quello fatto nell’algoritmo di Metropolis per passare dalla configurazione di gauge U alla configurazione U′:

U[mu, x, y, z, t] = U[mu, x, y, z, t] ∗ random[seed]

ove random[] è il vettore di tipo su3 relativo alla tabella di matrici SU(3) random con la quale far avvenire le transizioni.

L’altro operatore definito nella libreria QCD e frequentemente usato nelle applicazioni è l’operatore unario postfisso ‘˜ ’; applicato ad un elemento del gruppo, ne produce l’elemento inverso, sempre esistente per ipotesi.

<su3 object>˜

Dopo l’istruzione:

a = b˜

in a è contenuto l’elemento del gruppo in relazione a b^{−1}.

L’istruzione di alto livello dovrebbe tradursi in un’operazione di coniugazione hermitiana (infatti U^{†} = U^{−1} per matrici di SU(3)); ossia una operazione di trasposizione, seguita poi da una coniugazione complessa di tutti i 9 elementi della matrice. Nel listato Assembler risultato della compilazione tuttavia, risultano anticipate tutte le operazioni di trasferimento da memoria verso registri; l’ordine delle operazione è quindi: caricamento veloce sui registri del blocco dei 9 complessi (18 reali) costituenti la matrice 3×3 dalla locazione di memoria riservata a b; applicazione di 9 operazioni CHANGE SIGN ai registri relativi alle parti immaginarie dei 9 complessi (l’operazione di coniugazione complessa); scambio di posto fra alcuni elementi della matrice sui registri (trasposizione); infine scaricamento veloce del risultato verso le locazioni di memoria associate ad a.

Naturalmente sono possibili espressioni comunque complicate coinvolgenti oggetti di tipo su3 e gli operatori ‘∗’ e ‘˜ ’, tenendo presente che la priorità di ‘˜ ’ è più forte di ‘∗’ ma l’ordine di valutazione può essere forzato attraverso le parentesi tonde ‘(’ ‘)’.

Nella libreria QCD sono definiti altri comodi strumenti per operare più agevolmente sul tipo su3. E’ definita la keyword su3 one a rappresentare l’elemento più particolare del gruppo SU(3), l’elemento identità; può essere pensata come una costante e utilizzata all’interno di espressioni di tipo su3 come un qualsiasi altro oggetto dello stesso tipo. Per cui:

a = su3 one

inizializza la variabile di tipo su3 a a valere l’elemento identità del gruppo.

Per il tipo su3 esiste infine un ultimo operatore: l’operatore unario tr (traccia). Richiede quale argomento un oggetto di tipo su3 e restituisce un valore complesso: precisamente il valore ottenuto dalla somma dei tre elementi costituenti la diagonale della matrice 3 × 3 rappresentante l’elemento di SU(3).

<complex fact>::= tr(<su3 expr>)

Un esempio generale di utilizzo dell’intero set di operatori è il calcolo dell’azione di pura gauge sulle placchette (mostrato parzialmente per le placchette relative ad un singolo punto x, y, z, t del reticolo):

S = 0.0

for mu = 0 to 3 for nu = 0 to 3 S = S + tr(U[mu, x, y, z, t] ∗ U[nu, x + 1, y, z, t]

∗ U[mu, x, y + 1, z, t]˜ ∗ U[nu, x, y, z, t]˜ )
+ tr(U[mu, x, y, z, t]˜ ∗ U[nu, x + 1, y, z, t]˜
∗ U[mu, x, y + 1, z, t] ∗ U[nu, x, y, z, t])

endfor endfor

per ottenere l’azione completa i contributi di tutte le placchette del reticolo devono essere sommati. Vanno eseguiti oltre ai loop su μ e ν, anche quelli sul punto x, y, z, t del reticolo.

Credo questo possa essere considerato un esempio elegante ed efficiente di implementazione APESE. Potremmo, se volessimo, spingere l’ottimizzazione a livelli ancora superiori; notiamo infatti che per il calcolo dell’azione sulla singola placchetta sono necessari solamente quattro matrici su3, ma ognuna compare due volte nella espressione. Potremmo dunque definire, facendo uso delle potenzialità che lo ZzParser ci mette a disposizione, un operatore plaquette(mu, nu, x, y, z, t) che, da solo, si preoccupa di calcolare il contributo all’azione dovuto dalla placchetta individuata dall’estremo in basso a sinistra x, y, z, t e dalle direzioni mu, nu. L’ottimizzazione risulterebbe nell’evitare il duplice trasferimento dati da memoria a registri per ciascuna U[mu, x, y, z, t] della placchetta.

S = 0.0

for mu = 0 to 3 for nu = 0 to 3 S = S + plaquette(mu, nu, x, y, z, t) endfor endfor

Non è stato tuttavia ritenuto conveniente definire nella libreria QCD, e dunque come standard dell’APESE, l’operatore plaquette; infatti è parso troppo specifico per l’applicazione cui era destinato (non avrebbe avuto alcun altro utilizzo altrove, in programmi di altro tipo). Inoltre l’operatore plaquette è troppo legato alla particolare formulazione della teoria QCD scelta su reticolo; nel caso infatti si trovasse per la teoria un’azione più valida rispetto a quella dovuta a Wilson, l’operatore plaquette non sarebbe più di alcuna utilità. Chiunque d’altra parte fosse realmente interessato alla definizione di un operatore plaquette, può provvedere per conto suo, nei propri moduli, facendo uso delle proprietà di evolvenza del linguaggio.

Infine è necessario evitare di considerare il processo di ottimizzazione come un processo senza limiti; i limiti più severi li pone l’hardware della macchina. In teoria si può anche pensare ad un operatore action il quale si preoccupi da solo di calcolare l’azione relativa ad una certa configurazione, sommando su tutti i singoli contributi di placchetta per ogni x, y, z, t, mu, nu. E’ impensabile tuttavia che esista una macchina in grado di contenere sui propri registri tutte le variabili di link di un reticolo per eseguire il calcolo della azione nel modo più ottimizzato possibile.

4.20 TIPO SPINOR

Nella libreria QCD è definito il tipo spinor. Esso si riferisce a strutture algebriche in utilizzo nella meccanica quantistica relativistica: gli spinori di Dirac.

Come per il tipo su3 e i tipi più semplici, sono possibili dichiarazioni di variabili di tipo spinor nelle modalità di allocazione static, register, physical register, attraverso la sintassi:

spinor <ident>

spinor register <ident>

spinor physical register <ident>

Per accedere alle quattro componenti complesse da cui sono costituiti gli oggetti di tipo spinor si utilizza l’operatore di estrazione postfisso ‘.’, cui si fa seguire entro parentesi quadre l’espressione numerica specificante l’elemento da prendere in considerazione:

<spinor object>.[<integer expr>]

Nel caso in cui lo <spinor object> sia una variabile statica, l’intero da sistemare entro le quadre può anche essere una espressione valutabile a run-time, il cui valore cioè è conosciuto solo durante l’esecuzione; deve tuttavia essere un valore noto a compilation-time nel caso lo <spinor object> sia residente sui registri (variabile register o risultato di una espressione di tipo spinor).

Un esempio di dichiarazione e inizializzazione di una variabile spinor è:

\begin{matrix}spinor u \\ u.[0] = 1.0 \\ u.[1] = 0.0 \\ u.[2] = 0.0 \\ u.[3] = 0.0 \end{matrix}

Nel caso del tipo spinor, il compilatore si limita semplicemente a conservare tutte le quattro componenti complesse quali il programmatore vede attraverso l’operatore di estrazione ‘.’; per cui eventuali richieste ad esempio di normalizzazione su dati di tipo spinor, sono a cura esclusiva del programmatore; il compilatore dal canto suo, è pronto ad accettare per un dato di tipo spinor un qualsiasi insieme di quattro complessi.

Per il tipo spinor è stata definita un’aritmetica elementare comprendente le operazioni di somma ‘+’ e prodotto per complessi ‘∗’; gli operandi di una espressione di tipo spinor possono combinarsi fra loro in modo da realizzare una qualsiasi combinazione lineare.

Parimenti, il controllo della normalizzazione del risultato ottenuto non è automatico, ma resta all’attenzione dell’utente il quale dovrà accertare, oltre alla normalizzazione dei termini di una espressione spinor, anche che la scelta dei coefficienti della combinazione lineare sia valida.

<spinor expr>::=<spinor expr> + <spinor term> <spinor term>::=<complex term> ∗ <spinor fact>

L’interessante novità del tipo spinor è la definizione di particolari operatori che agiscono su di esso: le matrici di Dirac. In meccanica quantistica relativistica, le matrici di Dirac sono matrici 4 × 4 che agiscono (si applicano) agli spinori a quattro componenti di Dirac; l’operazione viene vista analiticamente come la saturazione dell’indice di colonna della matrice con l’indice dello spinore.

v_{α} = Γ_{αβ}u_{β}

Di fronte a tale operazione il compilatore cerca di mostrarsi il più possibile efficiente. Non è infatti così ingenuo da eseguire l’espansione ovvia e inefficiente:

v_{0} = Γ_{00}u_{0} + Γ_{01}u_{1} + Γ_{02}u_{2} + Γ_{03}u_{3}
v_{1} = Γ_{10}u_{0} + Γ_{11}u_{1} + Γ_{12}u_{2} + Γ_{13}u_{3}
v_{2} = Γ_{20}u_{0} + Γ_{21}u_{1} + Γ_{22}u_{2} + Γ_{23}u_{3}
v_{3} = Γ_{30}u_{0} + Γ_{31}u_{1} + Γ_{32}u_{2} + Γ_{33}u_{3}

ma si comporta in modo analogo a quanto visto per l’operazione ‘∗’ fra tipi su3. Isola cioè le operazioni di trasferimento dati fra memoria e registri, eseguendole tutte immediatamente prima o immediatamente dopo la fase di elaborazione numerica ed evitando trasferimenti plurimi di stessi dati.

Agisce poi nel senso di una ulteriore ottimizzazione. Ricordando che le matrici di Dirac non sono definite univocamente, essendo richiesto per esse il semplice verificarsi di certe relazioni di anticommutazione o di condizioni di hermitianità/antihermitianità, possiamo scegliere loro rappresentazioni grazie alle quali si trovino in forme particolarmente comode.

La rappresentazione scelta in genere per esse è tale per cui quasi tutti gli elementi di ciascuna matrice sono nulli ad eccezione di soli quattro (su un totale di 4 × 4 = 16); ognuno dei quattro è disposto su una riga differente della matrice.

Il compilatore ottimizzerà l’operazione di applicazione della matrice di Dirac allo spinore, evitando di eseguire le moltiplicazioni per zero.

Più oltre, considerando che nella rappresentazione scelta come standard per la libreria QCD, gli unici elementi nonnulli possono assumere i soli valori 1, −1, i, −i, sono possibili per il compilatore ulteriori margini di ottimizzazione: le operazioni di moltiplicazione per tali valori infatti, possono essere ridotte in termini di semplici trasferimenti o scambi di posto dei valori delle parti immaginarie e reali, con opzionale cambiamento di segno in certi casi; ad esempio:

a = −i ∗ b

si traduce:

a.re = b.im ; a.im = −b.re

Nella libreria sono definite le fondamentali gamma0, gamma1, gamma2, gamma3, facendo riferimento alla rappresentazione di esse :

γ^{0} = ( \begin{matrix}1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \\ 0 & 0 & −1 & 0 \\ 0 & 0 & 0 & −1 \end{matrix} )

γ^{1} = ( \begin{matrix}0 & 0 & 0 & −i \\ 0 & 0 & −i & 0 \\ 0 & i & 0 & 0 \\ i & 0 & 0 & 0 \end{matrix} )

γ^{2} = ( \begin{matrix}0 & 0 & 0 & −1 \\ 0 & 0 & 1 & 0 \\ 0 & 1 & 0 & 0 \\ −1 & 0 & 0 & 0 \end{matrix} )

γ^{3} = ( \begin{matrix}0 & 0 & −i & 0 \\ 0 & 0 & 0 & i \\ i & 0 & 0 & 0 \\ 0 & −i & 0 & 0 \end{matrix} )

detta di Pauli-Dirac nell’Euclideo. Oltre alle fondamentali, sono state definite nella libreria anche varie combinazioni delle fondamentali fra di loro e con le matrici γ^{5} e C. Ad esempio la gamma01 o la gamma25 sono le matrici combinazione γ^{0}γ^{1} e γ^{2}γ^{5}. Nella rappresentazione scelta come standard dalla libreria QCD la matrice γ^{5} ha la forma:

γ^{5} = ( \begin{matrix}0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 1 \\ 1 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 \end{matrix} )

L’operatore può utilizzare le matrici di Dirac in espressioni:

<spinor fact>::=<matrice di dirac> ∗ <spinor fact>

il risultato, come si vede, è di tipo spinor.

Ad esempio in APESE è possibile scrivere le linee:

spinor u, p slash u
real p[4],
p slash u = p[0] ∗ gamma0 ∗ u + p[1] ∗ gamma1 ∗ u+
p[2] ∗ gamma2 ∗ u + p[3] ∗ gamma3 ∗ u

in cui è facile ravvisare una espressione p̸u, ove nella notazione slash di Feynman p̸ sta per γ^{μ}p_{μ}. Sarebbe stato di nessuna difficoltà introdurre in APESE un operatore slash, definendolo in modo da eseguire efficientemente l’espressione mostrata sopra, risultando allo stesso tempo in una forma più compatta e leggibile; di fatto si è constatato che un tale operatore non è di alcuna utilità per i programmi verso i quali è rivolto l’APESE (ognuno se ne può convincere osservando gli algoritmi e le espressioni di cui si è discusso nel capitolo 1); di conseguenza non è stato introdotto nelle librerie standard del linguaggio. Il programmatore APESE che tuttavia necessitasse dell’operatore slash per i propri particolari scopi, può comunque, facendo uso della proprietà di evolvenza del linguaggio, definirlo a partire dagli operatori già definiti gamma0, gamma1, gamma2 e gamma3.

La libreria QCD mette a disposizione dell’operatore un ulteriore potente strumento di facile utilizzo: la possibilità di definire nuove matrici di Dirac in aggiunta a quelle predefinite nella libreria. L’istruzione preprocessata:

#diracdef <ident> <matrix>

permette di associare al simbolo <ident> la matrice di Dirac specificata alla sua destra; dal momento della sua definizione in poi, <ident> sarà un operatore matrice di Dirac analogo agli operatori già definiti.

#diracdef gammaI 1 0 0 0

0 1 0 0 0 0 1 0 0 0 0 1

Il compilatore penserà autonomamente a predisporre le ottimizzazioni già in uso per le matrici di Dirac predefinite. La definizione è considerata corretta se su ogni riga c’è un solo elemento nonnullo.

4.21 TIPI COLORSPIN E SPINSPIN

Nel capitolo 1 si è parlato dell’algoritmo di inversione come dell’algoritmo in grado di risolvere la:

(D̸_{x}(U) + m)_{ij}^{αβ}S_{jk}^{βγ}(x, 0; U) = δ^{(4)}(x − 0)δ_{ik}δ_{αγ}  (4.21.1)

a k e γ fissati.

L’operatore (D̸_{x}(U) + m)_{ij}^{αβ} contiene matrici di Dirac e matrici di SU(3) che operano rispettivamente sugli indici di spin e di colore dell’oggetto S cui si applicano.

S, per k e γ, fissati è un oggetto che ha un indice di colore e un indice di spin, e chiaramente, per la manifesta dipendenza da x, risulta dichiarato su reticolo. Risulta conveniente perciò dichiarare S come un vettore su reticolo di un nuovo tipo APESE: il tipo colorspin.

colorspin <ident>

colorspin register <ident>

colorspin physical register <ident>

contraddistinto da due indici interni: un indice di colore e un indice di spin.

Vediamo come risulterebbe la dichiarazione per S:

colorspin S[3, 4, N x, N y, N z, N t]

gli indici dichiarati a dimensione 3 e 4 sono in relazione agli indici k e γ che vanno fissati prima di eseguire il ciclo completo delle istruzioni dell’algoritmo.

Dalla (4.21.1) risulta anche l’inutilità dell’introduzione di un tipo “colorcolorspinspin”: infatti negli algoritmi di inversione, in genere non si opera mai sugli indice di colore e di Dirac cosiddetti della “sorgente”; praticamente essi fungono da spettatori nella maggior parte delle equazioni nelle quali compaiono, e non si può trarre alcun guadagno nel comunicare al compilatore che tali oggetti vanno trattati come unità non scorporabili. Il vantaggio nella introduzione di un tipo colorcolorspinspin risulta non in un aumento dell’efficienza nell’esecuzione, ma al massimo in maggiore eleganza dei programmi sorgenti risultanti.

Gli accessi all singole componenti del tipo colorspin avvengono al solito attraverso l’operatore di estrazione postfisso ‘.’:

<complex object>::=<colorspin object>.[<color index, spin index>]

All’interno delle parentesi quadre dobbiamo esplicitare, nell’ordine, una espressione intera a risultato compreso fra 0 e 2, relativa all’indice di colore; una espressione intera a risultato compreso fra 0 e 3, relativa all’indice di spin. Come per gli altri tipi, l’espressione intera deve essere calcolabile a compilationtime nel caso il <colorspin object> sia residente sui registri; può essere valutabile a run-time nel caso il <colorspin object> sia una variabile statica.

Per ogni oggetto di tipo colorspin, il compilatore conserva l’intero insieme delle 12 componenti complesse ottenibili al variare, entro i valori possibili, degli indici di colore e di spin; anche per il tipo colorspin, il compilatore non considera richieste di normalizzazione.

Per poter efficientemente implementare l’algoritmo di inversione occorre che siano definiti per il tipo colorspin gli operatori matrici di Dirac i quali agiscano lasciando come spettatore l’indice di colore; inoltre l’operazione di applicazione di una matrice di SU(3) all’indice di colore.

<colorspin term>::=<su3 term> ∗ <colorspin fact> <colorspin fact>::=<dirac matrix> ∗ <colorspin fact>

Lo stesso set di matrici di Dirac è disponibile sia per il tipo colorspin sia per il tipo spinor. Le nuove definizioni di matrici di Dirac si applicano anche al tipo colorspin.

Ad esempio:

T = U[0] ∗ gamma0 ∗ S + U[1] ∗ gamma1 ∗ S + U[2] ∗ gamma2 ∗ S + U[3] ∗ gamma3 ∗ S

rappresenta l’applicazione dell’operatore U^{μ}γ_{μ} contenuto in (D̸_{x}(U) + m)_{ij}^{αβ}, alla variabile di tipo colorspin S.

Naturalmente queste operazioni devono ricevere ottimizzazioni analoghe a quelle usate per le operazioni sui tipi spinor e su3. E’ chiaro infatti che quanto più esse risultano ottimizzate, tanto più velocemente il calcolatore giungerà attraverso l’algoritmo di inversione alla soluzione.

Per il tipo colorspin come per il tipo spinor, sono definite le operazioni di somma ‘+’ fra colorspin e prodotto ‘∗’ per coefficiente complesso, grazie alle quali sono possibili combinazioni lineari qualsiasi di colorspin:

<colorspin expr>::=<colorspin expr> + <colorspin term> <colorspin term>::=<complex term> ∗ <colorspin fact>

Infine è stata messa a disposizione una operazione di saturazione fra i rispettivi indici di colore e di spin di due oggetti di tipo colorspin:

<complex fact>::= colorspin saturate(<colorspin fact>, <colorspin fact>)

L’utilità di questa operazione purtroppo non risulta dalle argomentazioni superficiali svolte nel capitolo 1. Basterebbe però una occhiata ai testi riportati in bibliografia (Kogut, 1979) per rendersi conto del vantaggio che altri algoritmi di inversione, ad esempio quello del conjugate-gradient, trarrebbero dall’introduzione di questa operazione.

Infine in APESE esiste un ultimo tipo, detto “spinspin”, contraddistinto da due indici spinoriali, utile in situazioni molto particolari che purtroppo, per i soliti motivi, non abbiamo avuto modo di illustrare nel precedente capitolo 1. Ad esempio è utile allorché si tenta di costruire i propagatori relativi a mesoni o barioni accoppiando in vario modo i propagatori dei singoli quark di cui, secondo il modello a quark, essi sono costituiti.

CAPITOLO 5 IL COMPILATORE PER L’APESE

5.1 DESCRIZIONE GENERALE DEL COMPILATORE

Il compilatore dell’APESE è composto di due distinte parti: il parser e le routine semantiche.

Il parser è il cuore del compilatore; si occupa del riconoscimento del linguaggio e di pilotare la conseguente fase di traduzione del linguaggio, invocando le appropriate routine semantiche.

Il parser scelto per il compilatore dell’APESE è lo ZzParser. Abbiamo già illustrato nel capitolo 3 le caratteristiche essenziali dello ZzParser, soffermandoci in particolare sul suo attributo di “dinamicità”. Tuttavia per la comprensione completa di quanto seguirà, saranno necessari ulteriori approfondimenti; soprattutto per quanto riguarda lo ZzLanguage, il linguaggio che lo ZzParser è in grado di accettare nella fase iniziale, cioè il linguaggio generato dalla grammatica iniziale della evoluzione G_{z} sulla quale sono regolate le tavole di parsing dello ZzParser al momento della sua invocazione.

Le routine semantiche sono procedure scritte in linguaggio C, linkate allo ZzParser e richiamabili dallo ZzParser stesso. Le routine semantiche hanno l’importante compito di organizzare le risorse della macchina verso la quale si sta compilando: esse si preoccupano di dividere la memoria della macchina distinguendo zone per l’allocazione di variabili semplici, array, variabili di sistema, zone di allocazione dinamica (stack); gestiscono i registri della CPU e delle FPU (floating point units), scegliendone un certo numero per l’allocazione di variabili register, altri per contenere i risultati parziali delle operazioni aritmetiche, ancora altri per contenere indirizzi di memoria a partire dai quali possiamo trovare array o zone di memoria a variabili semplici, teste di stack etc..

Ma naturalmente il compito principale delle routine semantiche resta quello di controllare il file in uscita dal compilatore, ossia il file ASSEMBLER risultato della compilazione; generalmente, ogni routine semantica aggiungerà una o più linee al file, corrispondentemente alle funzioni per essa previste: ad esempio, una routine associata alla operazione di somma sarà richiamata ogni volta nel sorgente sia trovata una istruzione richiedente una somma, e aggiungerà al file ASSEMBLER una linea ADD …, costruita a partire dagli argomenti con cui la routine è stata chiamata.

La chiamata delle routine semantiche avviene in corrispondenza a certi costrutti e istruzioni previsti per il linguaggio Zz; esistono diverse modalità di chiamata delle routine semantiche, le prenderemo in considerazione allorchè forniremo una descrizione più approfondita dello ZzLanguage.

Infine, possono essere considerati corpi integranti del compilatore i file scritti in linguaggio Zz che istruiscono il parser sulla forma di linguaggio da accettare; essi spingono cioè l’evoluzione grammaticale verso una grammatica evolvente in grado di descrivere l’APESE relativamente alle sue strutture context-free e context-sensitive. In questi file ZZ sono inoltre specificate le azioni da compiere/ routine semantiche da richiamare in corrispondenza ai costrutti dell’APESE affinché il linguaggio riceva una sua traduzione, la più ottimizzata possibile, verso l’ASSEMBLER.

Inutile dire che sarà necessario descrivere qualcosa dell’ASSEMBLER per APE100, come almeno anche i tratti essenziali di APE100 stesso; potrebbero altrimenti non essere pienamente comprese le ottimizzazioni che il compilatore si è preso cura di gestire.

5.2 LO ZzPARSER

Lo ZzParser è un parser LR dinamico e “riflessivo”. Ossia è un parser in grado di riconoscere le frasi di un linguaggio, secondo le regole di una specificata grammatica, attraverso un processo di parsing bottom-up con lettura da sinistra a destra. Ogni volta che lo ZzParser riconosce una data regola di produzione, viene eseguita l’azione associata alla regola.

La proprietà di dinamicità (ossia la capacità del parser di modificare le proprie tavole di parsing seguendo una evoluzione grammaticale) viene in modo naturale vista dallo ZzParser come la possibilità di specificare l’azione in riduzione di una regola di produzione attraverso la definizione di una nuova regola di produzione.

Col termine “riflessività” infine, ci riferiamo alla capacità dello ZzParser di eseguire azioni scritte nello stesso linguaggio sul quale in quel momento sono regolate le sue tavole di parsing, cioè il linguaggio della evoluzione grammaticale stessa.

Lo scopo dello ZzParser è quello molto generale di interfacciare un insieme di procedure con un linguaggio di “comando”. Il set di procedure (scritte in linguaggio C) disponibile nel kernel dello ZzParser è molto limitato e orientato ad un utilizzo dello ZzParser generale ma senza applicazioni pratiche. Il link di procedure C esterne al kernel consente di configurare lo ZzParser verso utilizzi specifici (nel nostro caso verso la realizzazione del compilatore del linguaggio APESE per programmi da eseguirsi sul calcolatore APE100).

L’invocazione dello ZzParser avviene scrivendo alla linea di comando:

$ zz filename

oppure, per una sessione interattiva, semplicemente:

                                           $ zz

Le successive linee in ingresso allo ZzParser (fornite dal filename o provenienti da tastiera nel caso di sessione interattiva) devono essere in linguaggio Zz, il solo linguaggio che lo ZzParser è in grado di accettare alla partenza e sul quale sono inizialmente regolate le sue tavole di parsing.

5.3 LO ZzLANGUAGE

Lo ZzLanguage è un linguaggio che consente la specifica di nuovi simboli non-terminali e di nuove regole di produzione da aggiungere alla grammatica sulla quale sono regolate (dinamicamente) le tavole di parsing dello ZzParser. Per ogni nuova regola di produzione introdotta lo ZzLanguage consente anche la specifica dell’ulteriore step evolutivo e delle azioni associate alla regola, da eseguirsi ogni volta che la regola è usata per una riduzione.

Possiamo vedere il linguaggio Zz come lo sviluppo dei concetti espressi nel capitolo 3 a proposito dell’esempio di linguaggio Z_{b}; in quella sede però, curammo piuttosto l’aspetto didascalico. Conviene perciò tenere presente quell’esempio e illustrare i punti in cui il linguaggio Zz, per necessita pratiche, se ne differenzia.

Innanzitutto vediamo quali sono gli oggetti con cui lo ZzLanguage ha a che fare. Risultano definiti alcuni tipi nei quali possiamo organizzare i dati per lo ZzParser; i più importanti di essi sono: il tipo identificatore in corrispondenza alla classe sintattica (cioè al non-terminale) ident; un dato di tipo identificatore è una sequenza di caratteri alfanumerici qualsiasi, compreso anche il simbolo ‘ ’, il cui primo carattere sia rigorosamente alfabetico. Il tipo stringa, in corrispondenza alla classe sintattica qstring, si riferisce a sequenze di caratteri qualsiasi incluse entro le doppie virgolette ‘"’; per il tipo qstring possono essere usati anche alcuni caratteri speciali come ad esempio lo spazio bianco o il carattere indicante un carriage return ‘\n’. I tipi integer (valore numerico intero) e float (valore numerico floating point) in corrispondenza alle classi sintattiche int e float; lo ZzParser distingue un dato di tipo float da uno di tipo integer per la presenza del punto decimale o perché posto nella notazione esponenziale.

Esistono poi altri tipi di dato di uso più infrequente e che non sono stati necessari nella definizione del linguaggio APESE; per essi si rimanda al documento ZzLanguage indicato in bibliografia.

Per tutti i tipi/classi sintattiche potremmo fornire le regole di produzione della grammatica G_{z} ad essi associate; credo sia facile immaginare a questo punto quali esse possano essere ad esempio per il non-terminale

ident.

La grammatica evolvente di partenza G_{z} generante il linguaggio Zz ha come simbolo non-terminale di partenza una root collegata al simbolo stat attraverso le regole di produzione:

                                  root → root stat [eol]
                                  root → stat [eol]
                                  root → root stat ;
                                  root → stat ;

Dunque su ciascuna linea in ingresso allo ZzParser può esserci il singolo statement del linguaggio Zz o anche più statement separati dal punto e virgola ‘;’. Per convenzione ogni statement proprio del linguaggio Zz comincia col carattere ‘/’; con questo si evitano eventuali conflittualità che potrebbero sorgere fra gli statement del linguaggio di programmazione che stiamo definendo attraverso lo ZzLanguage, e gli statement dello ZzLanguage stesso.

Un valido statement del linguaggio Zz è la definizione e l’assegnazione (tutto allo stesso tempo) di una variabile del linguaggio. La forma per esso assunta è (in notazione BNF):

/<var> = <valore>

/<var> = <valore> as <tipo>

ove al posto di <var> possiamo specificare un qualsiasi identificatore, anche già usato in precedenza in un altro statement di assegnazione, mentre al posto di <value> un dato dei tipi sopra elencati. Ogni variabile del linguaggio Zz è contraddistinta dal suo nome (l’evenienza di identificatore posto a sinistra dell’uguale), dal suo valore e dal suo tipo. Definendo una variabile attraverso la prima forma di assegnazione, il tipo della variabile è automaticamente settato al tipo del dato che le è stato assegnato. La seconda forma consente di forzare il tipo della variabile ad essere differente dal tipo di dato ad essa assegnato; ad esempio possiamo imporre che una variabile a cui sia stato assegnato un valore di tipo numerico o di tipo identificatore, sia in realtà di tipo stringa; in questo modo possiamo proibire sulla variabile elaborazioni numeriche che sarebbero invece possibili se la variabile fosse di tipo numerico.

Le variabili possono comparire ovunque sia lecito esprimere un dato dello stesso tipo assegnato alla variabile. Lo ZzParser sostituisce il nome della variabile con il valore che in quel momento la variabile assume.

Lo statement in assoluto più importante del linguaggio Zz è quello che consente di definire nuovi simboli non-terminali e nuove regole di produzione da aggiungere alla grammatica di partenza G_{z}. E’ detto statement di syntax extension, ed in generale assume la forma (in notazione BNF):

/<sintagma> − > <thread> { <action> }

il che significa che la grammatica G_{z} conterrà una regola di produzione (attenzione anche qui alla differenza fra i simboli → e − >):

                         stat → /sintagma − > thread { action }

Lo step evolutivo associato alla syntax extension consiste nell’aggiunta alla grammatica della regola di produzione e di tutti i nuovi simboli non-terminali specificati grazie allo statement di syntax extension stesso.

Il sintagma è un qualsiasi identificatore, sia esso definito ex-novo o predefinito nella G_{z}. Nel caso sia un identificatore già definito nella G_{z}, cioè un non-terminale della G_{z} (ad esempio stat o int), lo ZzParser in riduzione della syntax extension si limiterà semplicemente ad aggiungere alla G_{z} la nuova regola di produzione per il simbolo; questo equivarrà ad esempio a definire un nuovo valido statement oppure una nuova notazione in cui risulti possibile scrivere un valore numerico intero. Nel caso invece il sintagma sia un identificatore mai usato prima e non presente nell’insieme dei simboli non-terminali della G_{z}, lo step evolutivo in riduzione della syntax extension consisterà nell’aggiungere alla G_{z}, oltre alla nuova regola di produzione, anche il nuovo simbolo non-terminale; naturalmente il nuovo simbolo non-terminale, al momento della sua introduzione, risulterà essere un simbolo “irraggiungibile” (compare infatti solo nella regola di produzione appena definita e a sinistra del simbolo ‘→’). Affinché esso sia realmente utile, le successive linee Zz di incremento della grammatica dovranno renderlo raggiungibile, collegandolo ad esempio direttamente o indirettamente al simbolo stat.

La thread è una sequenza qualsiasi di bead. Le bead possono essere semplici o parametriche: una bead semplice è un dato di tipo qualsiasi, ossia un identificatore, una stringa inclusa fra virgolette o un valore numerico intero o floating point; può essere anche una variabile Zz, ed in tal caso il dato è rappresentato dal valore che in quel momento la variabile assume.

Le bead parametriche sono contraddistinte da un parameter type e da un parameter name, due valide evenienze di identificatore; il parameter type e il parameter name sono separati dal carattere ‘^’.

<bead param>::=<param type>ˆ <param name>

Le bead semplici specificano gli eventuali simboli terminali contenuti nella regola di produzione che stiamo introducendo. Le bead parametriche sono invece in relazione ai simboli non-terminali; il parameter type delle bead parametriche specifica il simbolo non-terminale.

Il parameter type può essere un non-terminale già introdotto o addirittura facente parte della grammatica G_{z} (ad esempio ident, qstring, int, …); oppure può essere un simbolo non-terminale definito ex-novo, da aggiungere alla grammatica insieme alla nuova regola di produzione. Naturalmente il nuovo simbolo non-terminale, al momento della sua introduzione, risulta “inattivo”; le successive syntax extension dovranno provvedere a che esso compaia a sinistra di regole di produzione terminanti, cioè le cui thread siano composte da sole bead semplici/simboli terminali o comunque contengano simboli non-terminali attivi.

Infine la opzionale action (se presente va inclusa entro le parentesi graffe), specifica le azioni che lo ZzParser deve compiere in reducing della nuova regola di produzione; le azioni possono essere invocazioni alle procedure C appartenenti al kernel dello ZzParser o ad esso linkate, o anche nuove syntax extension specificanti lo step evolutivo associato alla regola, come la teoria delle grammatiche evolventi prevede.

In generale una action è una lista di statement qualsiasi appartenenti al linguaggio fino a quel momento raggiunto; perciò sia statement strettamente appartenenti allo ZzLanguage (abbiamo già visto le definizioni di variabili Zz e le syntax extension); sia nuovi statement appartenenti al linguaggio in via di definizione, purché essi siano stati definiti in precedenza. Nuovi statement del linguaggio sono definiti grazie a syntax extension il cui sintagma (il non-terminale a sinistra del simbolo ‘− >’) sia stat. Questa proprietà dello ZzParser di accettare come valida specifica della action di una nuova syntax extension linee del linguaggio fino a quel momento raggiunto viene indicata col termine “riflessività”.

All’interno della action possono essere usate le variabili definite altrove ed esternamente alla action, come anche nuove variabili definite all’interno della action; la visibilità di queste ultime è però locale alla action e non può estendersi al di fuori di essa. Inoltre possono essere usati, con valenza di variabili, i parameter name relativi alle bead parametriche che compaiano nella thread; il nome di queste variabili sarà specificato da param name, il tipo da param type; il valore assunto sarà specificato, ogni volta che la regola viene riconosciuta, dai simboli terminali “dominati” (cfr. capitolo 3) dal non-terminale param type.

Una action inoltre può contenere uno statement della forma:

                                      /return <value>

il quale permette di associare il simbolo non-terminale che compare a sinistra della thread (il sintagma), al dato specificato da <value>; il dato potrà essere riutilizzato nel seguito del parsing quando, procedendo bottom-up, ridurremo un’altra regola di produzione nella quale ora il non-terminale compaia come bead parametrica (cioè a sinistra del simbolo ‘− >’); in quel caso il param name della bead assumerà il valore ritornatogli dallo statement /return.

Infine spendiamo qualche parola sul modo con cui attraverso il linguaggio Zz è possibile richiamare le procedure C, sia quelle del kernel dette anche KCP, sia quelle aggiunte dall’utente o UCP.

Sono considerati statement dello ZzLanguage le chiamate a procedure C non ritornanti un valore. Un esempio può essere fornito dalla KCP /print:

/print <data list> la quale consente la visualizzazione di uno o più dati (separati da virgole) di un qualsiasi tipo standard; ossia di tipo stringa, identificatore o valore numerico intero o floating point. L’identificatore di variabili precedentemente definite viene riconosciuto e tradotto nel valore che ad esso era stato assegnato.

Lo statement /print causa una chiamata e relativo passaggio di argomenti alla procedura C print(), contenuta nelle librerie standard del linguaggio e naturalmente linkata al kernel dello ZzParser.

Chiamate a procedure C avvengono anche in corrispondenza alle function del linguaggio Zz; nel caso delle function, la procedura C ritorna un valore nella forma caratteristica dei dati Zz; ossia accanto al valore deve essere anche ritornato il tipo del dato (int, float o qstring, etc.). Ad esempio la new() è una function del linguaggio Zz la quale chiama una procedura C del kernel (una KCP); il valore ritornato dalla function è un int il cui valore è incrementato ad ogni chiamata, a partire dall’iniziale 0.

Per le UCP valgono le cose dette fino ad adesso in generale per le procedure C. La sola differenza con le KCP sta nel fatto che, una volta linkate allo ZzParser, per esse va specificato nel file UA.C il nome con cui desideriamo esse siano identificate dal linguaggio Zz, così come la modalità di chiamata; ossia se devono essere considerate delle Zz function con valore di ritorno o piuttosto degli Zz statement, richiamabili nella consueta forma degli statement Zz (nome preceduto dallo slash ‘/’).

5.4 LA GENERAZIONE DEL CODICE E LE ACTION

La generazione del codice ASSEMBLER in traduzione del linguaggio di alto livello APESE è demandata ad un pacchetto di procedure C, chiamate actions, linkate al kernel dello ZzParser; ogni action si preoccupa di eseguire una azione elementare.

L’approccio alla realizzazione di un compilatore infatti, può essere tentato osservando che ogni struttura di un linguaggio di alto livello, ogni istruzione, per quanto potente e compattata, è scomponibile in una sequenza di azioni elementari, le stesse utilizzabili nelle più svariate circostanze.

La tendenza generale nella realizzazione dei compilatori è la individuazione di tali (non molte) azioni elementari. Quindi la scrittura delle procedure le quali si preoccupino che l’azione venga eseguita (esse inseriscono nel listato ASSEMBLER le linee che opportunamente, a run-time, eseguono l’azione). Infine l’organizzazione delle azioni elementari nelle complesse strutture del linguaggio di alto livello; nel caso del compilatore per l’APESE, questo compito se lo assumono i file ZZ in ingresso allo ZzParser.

Facciamo un esempio di azione elementare; allorché il parser riconosce una operazione di somma fra due variabili reali register, diciamo A e B, richiama immediatamente una procedura in linguaggio C a nome a madadd che ha quale azione elementare associata quella di inserire nel listato ASSEMBLER una linea:

                                       MADADD R A B

dove A e B sono i registri associati alle variabili da sommare ed R è il registro dove viene posto il risultato. A run-time la linea ASSEMBLER agirà proprio nel senso voluto.

La a madadd naturalmente chiederà come argomenti i parametri relativi alle due variabili da sommare (la specifica dei registri dove esse sono contenute), e restituirà come argomento di ritorno la specifica del registro R dove ha sistemato il risultato; l’argomento di ritorno può essere in seguito riutilizzato come argomento di chiamata ad un’altra action, magari come argomento di una nuova chiamata alla a madadd, nel caso il parser abbia, per fare un esempio, riconosciuto una somma a più addendi.

Non tutte le action hanno quale azione associata quella di produrre una linea ASSEMBLER. Ad esempio, in riconoscimento di una sintassi di dichiarazione di variabile statica intera il parser chiamerà la action a decl int var; la azione della a decl int var è quella di riservare per la variabile un opportuno posto in memoria, cercando di evitare condivisioni e sconfinamenti, e di preparare i puntatori alla cella di memoria grazie ai quali poter accedere alla variabile in tutte le sue future utilizzazioni. Gli argomenti di ritorno della a decl int var sono i parametri con i quali sarà possibile in seguito avere accesso alla variabile.

Come si può vedere, la a decl int var non introduce linee di codice nel file ASSEMBLER.

Il pacchetto è fondamentalmente diviso in due gruppi di action, in questo volendo rispecchiare la struttura di APE100 stesso: un gruppo per le action relative alla parte intera e di controllo del flusso del programma; l’altro per quelle relative alla parte floating point. Dovremo pensare dunque che esistono due action di somma: la a madadd per le somme fra dati floating point, la a add per le somme fra dati interi; le due action sono concettualmente distinte in conseguenza della estrema diversità hardware fra la S-CPU, il processore centrale che si occupa del flusso del programma e della elaborazione intera, e il MAD, il processore destinato alla elaborazione floating point.

Esiste inoltre un altro gruppo di action richiamato dal parser appena un momento prima dell’inizio della lettura del file sorgente APESE. Il loro scopo è l’inizializzazione del sistema; esse si preoccupano di resettare i contatori del compilatore, di inserire le linee ASSEMBLER che inizializzano le variabili del compilatore o i registri dal compilatore destinati a contenere particolari indirizzi di memoria (teste di stack, inizio di zone per l’allocazione di dati vari, etc.).

Gli argomenti con cui avviene lo scambio, mediato dal parser, delle informazioni fra una action e l’altra (ossia gli argomenti di ritorno e di chiamata delle action) contenenti le informazioni necessarie a costruire le linee ASSEMBLER, sono delle struct C essenzialmente di due tipi: il primo tipo è il gloper, una struttura in linea generale da pensarsi associata al global operand dell’ASSEMBLER (cfr. ) e che si riferisce ad argomenti riguardanti la parte intera e di controllo del flusso del programma della macchina; il gloper contiene tutti i dati necessari a costruire un global operand sul quale operare con le istruzioni ASSEMBLER le più diverse. Bisogna fare attenzione però a non utilizzare un gloper associato, ad esempio, ad una variabile intera in un contesto nel quale sarebbe logico un gloper associato ad una variabile reale.

L’altro tipo è il bundle, letteralmente “fascio” di registri, struttura studiata per adattarsi alla caratteristica hardware con cui è stato costruito il MAD, ossia il suo grande numero di registri. Il bundle rappresenta un insieme contiguo di registri del MAD, ed è quindi una struttura specifica per la sola parte floating point della macchina; è il pacchetto, unità minima, con cui la memoria e il MAD si scambiano i dati in fast-in e fast-out. Ogni dato APESE di tipo complex sarà in relazione a bundle di 2 registri; un dato di tipo spinor sarà un bundle di 4 × 2 registri (uno spinore uguale 4 componenti complesse).

Come si vede, le specifiche che avevamo stabilito necessarie per il linguaggio APESE influenzano notevolmente anche questa parte; il bundle non è niente altro che il riflesso dei tipi strutturati (su3, colorspin, etc.) verso i quali si desidera che il compilatore agisca con una certa efficienza.

Potremmo a questo punto fornire un elenco completo di tutte le action che è stato necessario scrivere per realizzare il compilatore APESE; in realtà presenteremo solo quelle che serviranno alla trattazione che intendiamo fare. Per avere un’idea del funzionamento delle action mostreremo in appendice il listato C delle fondamentali.

Deve essere chiaro comunque che una conoscenza di questa parte, ossia del funzionamento delle action, o almeno del risultato che esse producono allorchè sono invocate, non è fine a se stesso. Programmatori APESE che soffrano particolarmente nei loro programmi di problemi di efficienza, non possono limitarsi ad una conoscenza da manuale, così come l’abbiamo presentata nel capitolo 4, del linguaggio APESE. Essi trarrebbero grande vantaggio dalla conoscenza di questi argomenti, diventando abili a prevedere la forma esatta con cui il compilatore traduce il loro programma nel codice oggetto, per apportare così sul sorgente tutte le modifiche le quali si traducano in maggiore efficienza. E diventando anche abili a correggere eventuali errori (errare humanum est) che, con grave pregiudizio dell’efficienza della programmazione, avessimo commesso nella realizzazione del compilatore.

5.5 TYPE.HZ

Abbiamo ora tutti gli elementi sufficienti per iniziare un’analisi dei file ZZ nei quali sono contenute le definizioni del linguaggio. Lo faremo cominciando col mostrare come è possibile implementare la sintassi di dichiarazione di una variabile di un certo “tipo” del linguaggio APESE, ad esempio il tipo spinor.

Lo spinor è una struttura dati di tipo floating point; la gestione di variabili di tipo spinor riguarderà naturalmente la parte floating point della macchina.

Sui calcolatori APE100 l’unità minima che sovrintende alla eleborazione floating point, detta FPU, è composta di un MAD, il processore che si preoccupa di eseguire le operazioni in virgola mobile, ed una RAM ad esso associata. Essendo i calcolatori APE100 degli elaboratori paralleli, ogni singola replica di APE100 conterrà più FPU; il numero di FPU e lo schema con cui ogni FPU può scambiare dati con le altre dipende dalla configurazione utilizzata per quella specifica replica.

Tuttavia su APE100 il controllo dell’indirizzamento della RAM di tutte le FPU avviene ad opera di uno stesso processore centrale, la S-CPU. Ogni MAD infatti non è libero di operare indipendentemente dagli altri MAD; tutti sono sostretti ad eseguire lo stesso programma, lo stesso set di istruzioni, operando su locazioni della memoria identificate da una stesso indirizzo: il valore specificante l’indirizzo di memoria sarà lo stesso per tutti i MAD, ma ciascuno opererà sul dato prelevato dalla rispettiva memoria locale; inizializzando le memorie locali con valori diversi, si mettono i MAD in condizione di lavorare in parallelo ma per il calcolo di risultati diversi.

La dichiarazione di una variabile statica spinor riserva spazio su tutte le memorie locali, su tutte le FPU. Esiste una copia della variabile su tutte le FPU, ma resta all’operatore la decisione di assegnare alla variabile stessi valori su tutte le FPU o valori diversi da una FPU all’altra.

La action che si prende il compito di riservare spazio sulle memorie locali per variabili floating point (quindi non solo variabili di tipo spinor) è la a decl float var. La a decl float var richiede due argomenti: il nome, che è utile ad identificare la variabile anche nel listato ASSEMBLER, e la dimensione relativa al tipo della variabile; nel caso di una variabile spinor la dimensione è 4 × 2 = 8. L’azione eseguita dalla a decl float var è quella di riservare un numero di celle di memoria (ogni cella contiene un dato floating point a 32 bit) pari alla dimensione specificata come argomento, in questo caso 8.

La a decl float var sarà attenta a riservare per una stessa variabile celle di memoria contigue; se così non fosse, verrebbe a mancare una delle ottimizzazioni che il compilatore si è preso cura di gestire, ossia la possibilità di fare trasferimenti veloci da memoria a registri di insiemi di dati dei quali in genere nelle elaborazioni si ha bisogno globalmente.

Dunque, grazie alla a decl float var, le variabili di tipo spinor (così come quelle degli altri tipi strutturati dell’APESE) ricevono una allocazione contigua degli elementi che le compongono; ogni volta si abbia bisogno di esse nella evalutazione di espressioni nelle quali compaiano, si riducono i grandi tempi necessari all’indirizzamento dei singoli elementi da cui sono composte mediante l’indirizzamento singolo dell’intera struttura.

Il funzionamento della a decl float var è semplice; essa controlla un contatore che prende nota della posizione in memoria di tutte le variabili semplici via via dichiarate; le action di partenza inizializzano il contatore a puntare al primo indirizzo di memoria a partire dal quale possono essere allocate le variabili semplici. Ogni volta che viene dichiarata una variabile semplice, e quindi che viene chiamata la a decl float var, il contatore fornisce l’indirizzo dal quale iniziare l’allocazione della variabile; successivamente, sempre ad opera della a decl float var, il contatore viene incrementato per un valore pari alla dimensione della variabile dichiarata: le celle di memoria necessarie alla variabile sono ora esclusivamente riservate ad essa, e il contatore punta ad un nuovo indirizzo di memoria libera, pronto per l’allocazione di una nuova variabile.

La a decl float var ritorna un gloper nel quale è specificato l’indirizzo di memoria a partire dal quale è avvenuta l’allocazione, il numero di celle di memoria che sono state occupate, il modo con cui accedere alla variabile il più rapidamente possibile; in genere l’accesso alle variabili semplici avviene specificando come argomento delle istruzioni ASSEMBLER un global operand della forma:

                                          128.1

dove 1 è un registro cosiddetto DSREG, precedentemente inizializzato a puntare al primo indirizzo della zona di allocazione delle variabili semplici; 128 è l’offset specificante la posizione relativa della variabile rispetto all’indirizzo di inizio della zona riservata alle variabili semplici. Relativamente alle possibilità offerte da APE100, questo è il modo più rapido per far avvenire un accesso in memoria.

Vediamo ora come nei file ZZ avviene la specifica della sintassi con cui accettare la dichiarazione di una variabile semplice di tipo spinor, insieme a quella delle azioni da compiere in riconoscimento della sintassi:

                            /stat − > spinor identˆ nome {
                              /gloper = a decl float var(nome, 8)
                              /spinor var − > nome {
                                /return gloper

}

}

L’esempio è semplice e nello stesso tempo completo. La prima linea è una syntax extension: si informa lo ZzParser che d’ora in poi dovrà essere considerato come valido statement del linguaggio una linea iniziante con la keyword spinor seguita da una qualsiasi evenienza di identificatore. Ad esempio:

                                        spinor psi

Abbiamo aggiunto così una nuova regola di produzione alla grammatica di partenza.

Le linee all’interno delle due parentesi graffe più esterne indicano l’azione che il parser deve compiere allorché effettua una riduzione mediante la nuova regola di produzione. La prima linea costringe il parser a chiamare la function Zz a decl float var associata alla UCP omonima, e ad assegnare il valore di ritorno della function alla variabile Zz a nome gloper. La a decl float var viene chiamata specificando correttamente, quali argomenti, l’identificatore della variabile (il valore contenuto nella variabile Zz nome, parameter name della bead parametrica che compare nella thread) e la dimensione della variabile (8 indistintamente per tutte le variabili di tipo spinor). La a decl float var ritorna, all’interno della struttura C gloper, tutti i parametri necessari a descrivere una qualsiasi variabile APESE.

La seconda linea all’interno della azione è di nuovo una syntax extension. Essa specifica lo step evolutivo, caratteristico in teoria dei linguaggi evolventi, per il quale la grammatica evolva al momento della riduzione della regola.

Allorchè lo ZzParser incontra una sintassi di dichiarazione di una variabile spinor, aggiunge alla grammatica una nuova regola di produzione; la nuova regola di produzione informa lo ZzParser che l’identificatore dichiarato dopo la keyword spinor, il “nome” della variabile, può essere considerato una valida evenienza di variabile spinor; e che dunque gli deve essere consentito di comparire ovunque in un programma sia lecito compaia una variabile di tipo spinor.

Naturalmente, se abbiamo fatto leggere allo ZzParser solo queste poche linee, non esiste (non esiste ancora) un luogo in cui sia lecito compaia una variabile spinor; il nuovo simbolo non-terminale spinor var al momento in cui viene aggiunto alla grammatica (ossia la prima volta che compare in una syntax extension) è un simbolo irraggiungibile; ci preoccuperemo però in seguito di renderlo raggiungibile attraverso la specifica di nuove syntax extension le quali istruiscano il parser sugli utilizzi da fare di variabili spinor.

Anche lo step evolutivo ha un’azione associata. L’azione associata alla regola di produzione dello step evolutivo è quella molto semplice di ritornare per il non-terminale spinor var la struttura C gloper contenente le informazioni per la variabile. Il non-terminale spinor var “domina” i caratteri alfanumerici relativi all’identificatore della variabile, ma per il parser esso “vale” la struttura gloper in grado di riassumere tutti i parametri univocamente identificanti la variabile (ossia, oltre al nome, anche l’indirizzo di allocazione e la modalità di accesso ad essa).

La dichiarazione di array di spinor (o più in generale di array di un qualsiasi tipo floating point, reale, complesso, etc.) avviene ad opera di un’altra action: la a decl float array. Gli argomenti di chiamata della a decl float array sono, oltre al nome dell’array e alla dimensione dei singoli elementi dell’array (8 per array di spinor), anche la dimensione dell’array, ossia il numero di elementi di cui si vuole composto l’array.

Le linee ZZ specificanti una sintassi di dichiarazione di un array di tipo spinor insieme alle azioni da compiere in riconoscimento della sintassi sono:

                     /stat − > spinor identˆ nome [ intˆ dimensione ] {
                       /gloper = a decl float array(nome, 8, dimensione)
                       /spinor var − > nome [ integer exprˆ indice ] {
                         /gloper indicizzato = a ket(gloper, indice)
                         /return gloper indicizzato

}

}

Le differenze rispetto alla dichiarazione di variabili semplici sono nel fatto ovvio che la dichiarazione di un array presuppone la specifica della dimensione dell’array, così come l’uso dell’elemento di un array presuppone la specifica dell’indice che punti all’elemento; a risentire della diversa sintassi sono naturalmente le due linee di syntax extension.

La a decl float array pensa da sola a riservare le 8 ∗ dimensione locazioni di memoria necessarie all’array. Essa inoltre effettua l’allocazione curando che le 8 componenti reali relative a ciascuno spinore elemento dell’array giacciano su locazioni di memoria consecutive; infatti il caricamento rapido di ogni elemento dell’array sui registri del MAD avviene solo se si è avuta cura di allocare consecutivamente tutte le componenti dell’elemento. La zona di memoria utilizzata per l’allocazione di array è diversa da quella utilizzata per le variabili semplici.

La a decl float array ritorna un gloper contenente tutte le informazioni relative all’array; il gloper deve essere pensato come associato all’array visto nel suo complesso. Naturalmente conterrà il nome dell’array, l’indirizzo di memoria a partire dal quale è stato allocato, il numero di elementi dell’array e la dimensione di ciascun elemento (8 per array di spinor). Deve essere puntualizzato che il gloper ritornato dalla a decl float array non può contenere informazioni relative alle modalità di accesso all’array; queste possono essere supplite solo al momento dell’utilizzo dell’array, poiché solo allora viene indicato, attraverso la specifica dell’indice, l’elemento dell’array che deve essere utilizzato.

Esistono tuttavia in APESE operazioni per cui l’array deve essere visto come una struttura completa, un tutto indivisibile; ad esempio possiamo passare ad una subroutine quale argomento un intero array; in tal caso però il gloper ritornato dalla a decl float array contiene già le informazioni sufficienti affinché si possa proseguire correttamente nella compilazione.

Al momento dell’uso di un singolo elemento dell’array, al momento cioè nel quale sul programma sorgente viene letto il nome dell’array seguito dall’espressione intera (fra le parentesi quadre) specificante l’elemento dell’array da prendere in considerazione, allora viene chiamata la action a ket. La action a ket si preoccupa di ritornare, a partire dal gloper fornito dalla a decl float array e dalla espressione intera fornita come indice all’array, un gloper questa volta relativo al singolo elemento dell’array; il gloper ritornato è quindi immediatamente utilizzabile da action “operazionali” quali la a madadd o la a madmult.

Da tutti gli esempi finora considerati è manifesto il ruolo assunto dal parser quale semplice “smistatore” dei flussi degli argomenti delle action; il parser non è assolutamente in grado di operare ad esempio sul gloper ritornato dalla a decl float array; l’unica cosa che è in grado di fare è considerare il gloper come una scatola nera, conservarla e passarla come argomento ad altre action allorché abbia ricevuto istruzioni in questo senso.

Le procedure C sanno gestire solo array monodimensionali, seppure, per quanto abbiamo detto sopra, è chiaro controllino anche la gestione della dimensione interna relativa al tipo dichiarato. La gestione di matrici di spinori o più generalmente di array pluridimensionali di spinori (o anche di altri tipi) è a cura non delle action ma dei file ZZ. Vediamo un esempio di linee ZZ implementanti una sintassi di dichiarazione di array bidimensionali di spinori insieme alle azioni ad essa connesse:

            /stat − > spinor identˆ nome [ intˆ dim1 , intˆ dim2 ] {
              /dimensione = dim1 ∗ dim2
              /gloper = a decl float array(nome, 8, dimensione)
              /spinor var − > nome [ integer exprˆ ind1 , integer exprˆ ind2 ] {
                /gloper dim2 = a decl int imm(dim2)
                /temp = a mult(ind1, gloper dim2)
                /indice = a add(temp, ind2)
                /gloper indicizzato = a ket(gloper, indice)
                /return gloper indicizzato

}

}

Come si vede il parser passa alla a decl float array la dimensione totale dell’array ottenuta moltiplicando le dimensioni parziali. Questo è possibile perché in linguaggio Zz sono definite sui tipi int e float le operazioni aritmetiche tradizionali ∗, /, +, −, così come sul tipo qstring è definita l’operazione binaria &, il cui risultato, sempre di tipo qstring, è la stringa ottenuta collegando le stringhe operando.

Dunque il parser, istruito dalle linee ZZ, trasforma un array bidimensionale a dimensioni dim1 e dim2 in un array monodimensionale a dimensione specificata dalla variabile Zz di tipo int dimensione; il valore di dimensione è il prodotto di dim1 e dim2. Il parser lascia poi che siano le action a occuparsi delle altre azioni da compiersi.

Al momento dell’utilizzo nel programma di un elemento dell’array, il parser interviene di nuovo per tradurre la specifica bidimensionale dell’elemento dell’array in una specifica monodimensionale, accettabile per la action a ket. Per fare questo chiama le action che introducano nel listato ASSEMBLER le linee relative al calcolo dell’indice assoluto dell’elemento dell’array; ossia una a mult che inserisce una moltiplicazione (MULT) fra il global operand relativo all’espressione intera specificante il primo indice dell’elemento e il global operand relativo all’intero immediato dim2 specificato al momento della dichiarazione dell’array. La a mult dovrà essere chiamata fornendo quali argomenti il gloper associato all’espressione intera (a sua volta ottenuto da un’altra action chiamata all’interno della azione relativa ad una syntax extension, definita altrove, per il non-terminale integer expr, e ritornato attraverso lo statement /return posto all’interno della stessa azione), e il gloper associato all’intero dim2 generato appositamente dalla action a decl int imm. Al risultato ottenuto con la MULT va poi sommata l’espressione intera specificata come secondo indice dell’elemento (il secondo indice è quello che “varia più lentamente”); il che si ottiene chiamando, con gli opportuni gloper di argomento, la action a add affinché inserisca una linea ADD nel compilato ASSEMBLER. Infine il gloper risultato ultimo della serie di operazioni (a cui nel listato ASSEMBLER ovviamente corrisponde un global operand per la conservazione temporanea del risultato) viene passato alla a ket affinché provveda alle altre necessità, fra le quali ad esempio quella di inserire nel listato ASSEMBLER una nuova MULT fra il global operand così ottenuto e l’intero immediato 8, relativo alla dimensione interna di un array di tipo spinor.

E’ sempre fondamentale nella realizzazione dei compilatori l’esatta cognizione delle operazioni che devono essere eseguite a compilation-time e quelle che non possono essere eseguite che a run-time; ad esempio la moltiplicazione fra le due dimensioni dell’array bidimensionale deve essere eseguita a compilation-time (il compilatore deve conoscere immediatamente quanto spazio riservare in memoria per l’array); invece le operazioni per ottenere l’indice assoluto dell’elemento dell’array non possono essere eseguite che a run-time: gli indici puntanti all’elemento potrebbero essere stati specificati mediante variabili del programma, il cui valore in generale non può essere conosciuto a compilation-time.

Noi potremmo fare uso della proprietà di “riflessività” dello ZzParser, già illustrata in precedenza, per scrivere in modo migliore e più chiaro le linee Zz relative alla sintassi per gli array bidimensionali. Ammesso che avessimo definito altrove la sintassi, e le azioni ad essa connesse, affinchè il parser sappia riconoscere una espressione APESE di tipo intero, noi possiamo sostituire le linee:

                            /gloper dim2 = a decl int imm(dim2)
                            /temp = a mult(ind1, gloper dim2)
                            /indice = a add(temp, ind2)

attraverso la più immediata:

                           /indice = ind1 ∗ dim2 + ind2 as gloper

Il risultato sul listato ASSEMBLER in uscita è esattamente equivalente. In sostanza all’interno delle graffe dell’azione di una syntax extension, ci siamo avvalsi non solo delle sintassi appartenenti al linguaggio Zz, ma anche delle nuove sintassi che sono state definite nel corso del parsing.

E’ importante sottolineare che lo ZzParser si comporta in modo del tutto differente quando legge la linea:

                           /indice = ind1 ∗ dim2 + ind2 as gloper

rispetto a quando invece legge la linea:

                                 /dimensione = dim1 ∗ dim2

Infatti nel primo caso l’operatore ‘∗’ compare fra due termini considerati di classe sintattica integer fact, ossia espressioni (o variabili) del tipo APESE intero, il cui valore è noto solo a run-time; il parser ridurrà la frase mediante una regola di produzione (definita altrove, ma prima comunque che possa essere letta la frase):

                      /integer term − > integer term ∗ integer fact

e l’azione che eseguirà in corrispondenza alla riduzione sarà quella di richiamare la action a mult, inserendo così nel listato ASSEMBLER la linea di MULT. Nel secondo caso invece dim1 e dim2 sono di classe sintattica int (la classe sintattica predefinita in Zz relativa a valori interi direttamente specificati), e il parser ridurrà mediante la regola di produzione (già presente nella grammatica G_{z} di partenza):

                            /int term − > int term ∗ int fact

la cui azione consisterà nel chiamare la KCP che fornisca immediatamente, come argomento di ritorno, il risultato della moltiplicazione.

Indirettamente abbiamo anche mostrato un esempio di overloading dell’operatore ‘∗’; il comportamento del parser di fronte al simbolo ‘∗’ dipende assolutamente dal contesto nel quale esso compare. Più avanti vedremo altri e più importanti esempi di overloading dell’operatore ‘∗’.

Se noi facessimo leggere allo ZzParser i tre spezzoni di listati Zz sopra mostrati, indubbiamente saremmo in grado di compilare la dichiarazione di qualsiasi variabile semplice di tipo spinor, così come di qualsiasi array mono o bidimensionale di spinori. Siamo però ancora lontani dall’obiettivo che ci siamo imposti per la dichiarazione APESE di variabili di tipo spinor: infatti in APESE sono possibili array di qualsiasi dimensionalità, come pure sono possibili liste di dichiarazioni di variabili di stesso tipo sulla medesima linea di programma.

Con le syntax extension che abbiamo fornito, per ogni singola dichiarazione di variabile dobbiamo cambiare linea, esprimendo di nuovo e ogni volta la keyword spinor. Il rimedio a questo problema può essere trovato facendo leggere al parser, una volta per tutte, le syntax extension:

                 /stat − > spinor spinor decl listˆ $
                 /spinor decl list − > spinor decl listˆ $ , spinor declˆ $
                 /spinor decl list − > spinor declˆ $

(in linguaggio Zz le bead parametriche possono avere il simbolo ‘$’ al posto dell’identificatore specificante il param name; in tal caso è inteso che non si abbia alcuna necessità di utilizzare nella azione il param value associato alla bead, e ovviamente al parser ne risulta una procedura più snella).

Sostituiremo poi negli spezzoni Zz le thread di tutte le syntax extension della forma:

                             /stat − > spinor identˆ nome . . .

con la thread:

                             /spinor decl − > identˆ nome . . .

lasciando immutato il resto.

Otteniamo così il risultato di consentire più dichiarazioni su una stessa linea di programma; addirittura sono possibili sulla stessa linea dichiarazioni di variabili semplici e di array.

Potremmo altresì mostrare, ma con maggiore difficoltà, come si è ottenuto di poter dichiarare in APESE array di dimensione qualsiasi; o potrebbero essere mostrati tutti i miglioramenti necessari a raggiungere l’obiettivo dell’APESE agevole che abbiamo descritto nel capitolo 4.

Di fatto possiamo candidamente dichiarare senza produrre meraviglia o delusione nel lettore, che le linee Zz che abbiamo prodotto quali esemplificazioni di definizione sintattica della dichiarazione del tipo spinor, non sono affatto quelle contenute nel risultato stabilito come finale per le librerie Zz di definizione dell’APESE. Le librerie contengono qualcosa di simile cercando inoltre di essere più generali.

La libreria Zz che definisce la sintassi di dichiarazione delle variabili di un qualsiasi tipo dell’APESE, denominata TYPE.HZ, definisce una istruzione da non considerarsi come facente parte del set relativo all’APESE, né del set per il linguaggio Zz; più propriamente è l’istruzione di un linguaggio intermedio fra Zz e l’APESE, introdotta con lo scopo di arrivare ad una definizione più agevole dell’APESE. E’ un riflesso di quella che chiamammo proprietà di riflessività dello ZzParser, grazie alla quale ci è consentito di utilizzare per la definizione di nuove sintassi il linguaggio fino a quel momento raggiunto.

L’istruzione definita in TYPE.HZ è la typedef, che, invocata ad esempio nella forma:

                                    typedef spinor(8)

guida l’evoluzione grammaticale verso l’aggiunta delle regole di produzione relative alla dichiarazione di variabili spinor (più o meno le stesse regole che troviamo scritte negli esempi di questo paragrafo), più alcune altre relative alle applicazioni del tipo spinor possedute in comune con gli altri tipi APESE. Ad esempio la sintassi di assegnazione di un valore ad una variabile di tipo spinor è certamente simile a quella stabilita per gli altri tipi (<var> = <expr>).

Grazie alla typedef evitiamo di riscrivere ogni volta, in corrispondenza ad ogni tipo APESE, linee Zz pressappoco identiche; eliminiamo certamente il rischio di errori di battitura.

Di seguito alla keyword typedef dobbiamo specificare il nome del tipo da definire (esempio, colorspin, su3, etc.) e la dimensione associata al tipo (nell’esempio dei colorspin la dimensione è 3 × 4 × 2 = 24).

Il parser dopo aver letto la typedef avrà aggiunto tutte le regole di produzione necessarie a trattare il nuovo tipo. Naturalmente non potranno essere aggiunte con la typedef le regole di produzione specificanti l’algebra del tipo poiché l’algebra è peculiare al tipo; l’algebra andrà specificata con regole di produzione a parte.

In particolare, la typedef introduce automaticamente tutte le linee specificanti le sintassi di dichiarazione di variabili semplici ed array mono e pluri-dimensionali; sintassi di dichiarazione di variabili register; sintassi per il passaggio alle sottoprocedure di variabili ed espressioni nelle modalità per puntatore e per valore; poche altre cose di utilizzo meno frequente.

L’istruzione typedef si applica solo a tipi relativi a dati floating-point; le definizioni relative al tipo integer (in APESE non sono previsti tipi strutturati a elementi interi poiché essi non si producono in alcuna ottimizzazione su APE100) non possono avvenire tramite la typedef, e devono essere esplicitamente fornite a parte.

Per quanto riguarda la sintassi di dichiarazione relativa al tipo integer, possiamo ricalcare le linee Zz fornite negli esempi; al posto delle a decl float var e a decl float array invocheremo però le action a decl int var e a decl int array relative alla gestione dell’allocazione della memoria globale della macchina.

5.6 STD.HZ

La libreria STD.HZ contiene le definizioni dell’APESE standard, ossia dell’APESE privo delle strutture QCD; essa non può essere letta dallo ZzParser prima che gli siano state presentate le linee di TYPE.HZ, dato che in STD.HZ è usata l’istruzione typedef.

La libreria STD.HZ comincia con la definizione della sintassi di dichiarazione del tipo integer; come abbiamo detto nel paragrafo precedente, per il tipo integer non possiamo fare ricorso all’istruzione typedef. Le linee Zz relative alle definizioni per il tipo integer devono essere tutte riportate per esteso.

Quindi si procede con la definizione della aritmetica sul tipo intero. Questa parte provvede a che il parser sia in grado di riconoscere espressioni di tipo intero comunque complicate, eseguendone una strutturazione compatibile con la priorità specificata per gli operatori. Abbiamo già visto nei capitoli dedicati all’esposizione della teoria dei linguaggi formali come è possibile stabilire le regole di produzione per una grammatica in grado di imporre le corrette relazioni strutturali fra le diverse parti di una espressione (fattori, termini o espressioni).

E’ semplice perciò comprendere che le linee:

/integer fact − > integer varˆ gloper { /return gloper }
/integer fact − > intˆ num {
  /gloper = a decl int imm(num)
  /return gloper

}

/integer term − > integer factˆ gloper { /return gloper }
/integer term − > integer termˆ gl1 ∗ integer factˆ gl2 {
  /gl3 = a mult(gl1, gl2)
  /return gl3

}

/integer term − > integer termˆ gl1 / integer factˆ gl2 {
  /gl3 = a div(gl1, gl2)
  /return gl3

}

/integer expr − > integer termˆ gl { /return gl }
/integer expr − > integer exprˆ gl1 + integer termˆ gl2 {
  /gl3 = a add(gl1, gl2)
  /return gl3

}

/integer expr − > integer exprˆ gl1 − integer termˆ gl2 {
  /gl3 = a sub(gl1, gl2)
  /return gl3

}

/integer fact − > ( integer exprˆ gl ) { /return gl }

producono proprio il risultato voluto. Con le prime due syntax extension in particolare si specifica che, quali fattori di una espressione intera, possono comparire variabili intere o valori interi immediati.

Il non-terminale integer expr è collegato al simbolo di partenza della grammatica attraverso la regola di produzione:

    /stat − > integer varˆ glv = integer exprˆ gle {
      /a move(glv, gle)

}

Dopo aver definito la sintassi per il tipo integer, si passa ai tipi real e complex. Le definizioni generali per i due tipi avvengono tramite le linee:

                   typedef real(1)
                   typedef complex(2)

Si passa poi a stabilire l’aritmetica per il tipo real. Questa parte non presenta particolare interesse poiché ricalca da vicino l’analoga già fornita per il tipo integer; l’unica differenza è che dovremo richiamare le action floating-point a madadd, a madmult, etc., al posto di quelle intere a add, a mult.

Per il tipo complex troviamo invece interessanti novità. Intanto la definizione della sintassi sulla “estrazione” di dati real da dati complex, come è stata descritta nal capitolo 4:

                  /complex disp − > re { /return 0 }
                  /complex disp − > im { /return 1 }
                  /real var − > complex varˆ v . complex dispˆ d {
                    /gl = a decl int imm(d)
                    /ret = a dyn subgloper(v, d, 1)
                    /return ret

}

                  /real reg var − > complex reg varˆ v . complex dispˆ d {
                    /ret = a extract bundle(v, d, 1)
                    /return ret

}

Vengono usate due diverse action: per le variabili statiche la a dyn subgloper ritorna un gloper di dimensione 1 (il valore specificato dal terzo argomento) a partire dal gloper specificato come primo argomento; il secondo argomento specifica la posizione relativa del gloper ritornato rispetto a quello da cui si estrae: è la posizione 0 nel caso si desideri la parte reale, la posizione 1 nel caso la parte immaginaria.

La a extract bundle viene usata per estrazioni di parti reali o immaginarie da variabili complesse register; estrae un bundle di dimensione 1 (il terzo argomento) dal bundle fornito come primo argomento e sulla base del displacement fornito come secondo argomento.

Si passa quindi alla definizione dell’algebra sul tipo complex; vediamo come viene definita la moltiplicazione fra due dati di tipo complex:

                   /complex term − > complex termˆ a ∗ complex factˆ b {
                     /ret = (a.re ∗ b.re − a.im ∗ b.im, a.re ∗ b.im + a.im ∗ b.re)
                     /return ret

}

Nella quale si sfrutta il fatto che è già stata definita la sintassi per l’estrazione di dati real da complex, di moltiplicazione fra dati real, e di specifica di dati complex da dati real.

Mostriamo anche la definizione della sintassi relativa alla operazione di coniugazione complessa:

                          /complex fact − > complex factˆ gl ˜ {
                            /ret = (gl.re, −gl.im)
                            /return ret

}

In STD.HZ è contenuta anche la definizione delle sintassi relative al controllo del flusso del programma; ossia la definizione degli statement if, where, for, etc., insieme alla definizione delle espressioni booleane e degli operatori booleani. Quindi anche la definizione delle sintassi riguardanti il trasferimento del flusso del programma fra subroutine e programma principale e l’input/output.

5.7 QCD.HZ

La libreria QCD definisce i tipi e gli operatori che rendono l’APESE un ambiente ideale per lo sviluppo di simulazioni sul calcolatore della teoria di campo QCD formulata sul reticolo.

L’analisi del listato della libreria QCD svela i particolari e soprattutto le ottimizzazioni che l’APESE prende in considerazione per consentire un utilizzo efficiente della macchina.

Essa contiene le definizioni dei tipi spinor, su3, colorspin, spinspin, insieme a tutti gli operatori che su di essi agiscono. Della libreria descriveremo le parti significative, tralasciando i contorni o i particolari che non meritano attenzione.

La sintassi relativa a dichiarazione di variabili statiche o register del tipo spinor viene introdotta attraverso il solito statement:

                                    typedef spinor(8)

Si passa poi alla definizione della sintassi di estrazione degli elementi complessi costituenti il tipo.

                   /complex var − > spinor varˆ v . [ integer exprˆ i ] {
                     /ret = a dyn subgloper(v, i, 2)
                     /return ret

}

                   /complex reg var − > spinor reg varˆ v . [ intˆ i ] {
                     /ret = a extract bundle(v, i, 2)
                     /return ret

}

Solo ora possiamo capire perché per variabili spinor register è possibile effettuare estrazioni specificando già a compilation-time la posizione dell’elemento all’interno della struttura (la classe sintattica int presuppone la specifica di un intero immediato), mentre per varibili statiche la specifica può essere rimandata a runtime (il valore di un integer expr in generale è noto solo a run-time). Tutto dipende dal fatto che la a dyn subgloper ha come argomento di displacement un gloper, e il gloper è in relazione a variabili o a risultati parziali di espressioni; la a dyn subgloper esegue un sottoindirizzamento dinamico delle strutture, analogo a quello usato per gli array. La a extract bundle vuole invece come argomento di displacement un intero immediato; non si può infatti eseguire un indirizzamento di tipo vettoriale su strutture allocate sui registri.

Non presentano particolare interesse le linee Zz definenti la elementare algebra prevista per il tipo (somme, prodotti per scalari); questa parte ricalca l’analoga già mostrata a proposito dei tipi elementari dell’APESE (integer, real, complex).

Sono degne di particolare attenzione invece le linee che definiscono l’operazione di applicazione di una matrice di Dirac ad uno spinore; da esse dipendono senz’altro (per quanto sottolineato nel capitolo 1) i tempi impiegati dagli algoritmi della teoria nel conseguimento dei risultati.

L’operazione viene definita molto semplicemente da una syntax extension a thread:

                   spinor fact − > dirac matrixˆ m ∗ spinor factˆ s { . . .

Prima di mostrare l’azione associata a questa thread è necessario fornire spiegazioni su un altro dei tipi in uso in linguaggio Zz: il tipo lista. Una lista è un insieme, grande quanto si voglia, composto di dati Zz dei tipi più semplici; lo ZzParser riconosce come validi dati di tipo lista, insiemi di dati di tipo più semplice separati da virgole e racchiusi entro parentesi quadre. Un esempio di lista può essere:

                                 [4, 3.4, "salve mondo", psi]

In ZzLanguage possono essere definite variabili di tipo lista:

                               /a = [4, 3.4, "salve mondo", psi]

Per accedere ai singoli elementi di una variabile di tipo lista, è sufficiente far seguire al nome della variabile e ad un punto ‘.’, il numero d’ordine dell’elemento all’interno della lista; le linee seguenti:

                                        /print a.0 ; /print a.2 ; /print a.1 ; /print a.3

producono sullo standard output, nell’ordine, la stampa di 4, salve mondo, 3.4 e infine psi; qualora psi fosse una variabile precedentemente inizializzata con uno statement /psi = …, viene stampato non psi, ma il valore assegnato alla variabile.

Vediamo ora come è stata definita nella libreria QCD la classe sintattica dirac matrix, mostrando i simboli terminali del linguaggio che essa domina e i dati ritornati. In corrispondenza ad ogni matrice predefinita del linguaggio c’è una linea Zz del tipo:

            dirac matrix − > gamma1 {
              /return [3, complex i, 2, complex i, 1, complex meno i, 0, complex meno i]

}

Ricordiamo che il simbolo gamma1 in APESE è in corrispondenza alla matrice:

γ^{1} = ( \begin{matrix}0 & 0 & 0 & +i \\ 0 & 0 & +i & 0 \\ 0 & −i & 0 & 0 \\ −i & 0 & 0 & 0 \end{matrix} )

La lista assegnata come valore di ritorno al simbolo gamma1 è una specifica degli elementi nonnulli della matrice e della loro posizione; punto fermo nella definizione della classe sintattica dirac matrix è la limitazione che le matrici di Dirac del linguaggio siano costituite da solo 4 elementi nonnulli su un totale di 16, ognuno disposto su una differente riga della matrice.

La lista è composta di quattro coppie di valori; nell’ordine, le coppie sono associate alle quattro righe della matrice. Il primo elemento di ogni coppia è un intero che specifica la colonna entro la quale si trova l’elemento nonnullo; il secondo elemento specifica, attraverso un set di costanti predefinite del linguaggio (complex uno, complex meno uno, etc.), il valore assunto dall’elemento nonnullo. Nell’esempio della gamma1, l’elemento nonnullo della prima riga è il numero i (complex i) posto in terza colonna.

La lista riassume tutti i dati con i quali è possibile ricostruire la matrice. Vediamo ora come vengono utilizzati questi dati dall’azione della syntax extension cui la lista viene ritornata; ossia la syntax extension che definisce l’operazione di applicazione di una matrice di Dirac ad uno spinore, della quale già abbiamo mostrato la thread:

                    /spinor fact − > dirac matrixˆ m ∗ spinor factˆ s {
                      /c0 = m.0; /c1 = m.2; /c2 = m.4; /c3 = m.6
                      /e0 = m.1; /e1 = m.3; /e2 = m.5; /e3 = m.7
                      spinor temporary ret
                      ret.[0] = s.[c0] ∗ e0
                      ret.[1] = s.[c1] ∗ e1
                      ret.[2] = s.[c2] ∗ e2
                      ret.[3] = s.[c3] ∗ e3
                      /return ret

}

Osserviamo dapprima come avviene l’estrazione dei dati della lista contenuta nel parameter name m ritornata dalla syntax extension per la classe sintattica dirac matrix, e come essi vengano appoggiati nelle variabili c0, c1, etc., e0, etc.; le ci specificano la colonna dove trovare l’elemento nonnullo della riga i, le ei l’elemento nonnullo stesso.

L’applicazione di una matrice di Dirac ad uno spinore, piuttosto che causare la saturazione dell’indice di colonna della matrice con l’indice dello spinore, viene risolta dal compilatore nella esecuzione delle sole operazioni che non comportino un prodotto per zero.

Nel caso il compilatore avesse risolto l’applicazione di una matrice di Dirac ad uno spinore vedendola come la saturazione dell’indice di colonna della matrice con l’indice dello spinore, l’azione della syntax extension sarebbe stata:

                ret.[0] = s.[0] ∗ 0.0 + s.[1] ∗ 0.0 + s.[2] ∗ 0.0 + s.[3] ∗ complex i
                ret.[1] = s.[0] ∗ 0.0 + s.[1] ∗ 0.0 + s.[2] ∗ complex i + s.[3] ∗ 0.0
                ret.[2] = s.[0] ∗ 0.0 + s.[1] ∗ complex meno i + s.[2] ∗ 0.0 + s.[3] ∗ 0.0
                ret.[3] = s.[0] ∗ complex meno i + s.[1] ∗ 0.0 + s.[2] ∗ 0.0 + s.[3] ∗ 0.0

naturalmente molto meno efficientemente.

Il compilatore prende in considerazione l’ulteriore ottimizzazione che il codice può ricevere nel caso si eseguano moltiplicazioni per costanti particolari (1.0, −1.0, i, −i). Tale ottimizzazione è però gestita non dal parser ma dalle action C richiamate dal parser.

Il risultato della moltiplicazione fra un numero complesso x + iy e ad esempio i può essere calcolato evitando l’esecuzione di operazioni aritmetiche, verso operazioni di MOVE; il risultato −y + ix è ottenuto dal compilatore spostando la parte reale del moltiplicando nella parte immaginaria del prodotto, e portando la parte immaginaria del moltiplicando cambiata di segno nella parte reale del prodotto.

In corrispondenza ai simboli complex uno, complex i, etc., o (1.0,0.0), (0.0,1.0) della classe sintattica complex const, il parser ha associati dei bundle “speciali”; nel senso che nella struttura C rappresentante questi bundle viene settato un particolare flag il quale renda conto del loro comportamento “speciale”. Allorché la action a madmult, ad esempio, viene richiamata porgendogli come argomento uno qualsiasi di questi bundle speciali, l’azione della a madmult sarà diversa da quella consueta: invece di inserire nel listato ASSEMBLER istruzioni di MADMULT, si provvederà piuttosto a inserire le appropriate MADMOVE in grado di fornire il risultato. Nel caso sopra visto di moltiplicazione di una variabile z = x + iy per i, il risultato ottimale di compilazione sarà:

                                   MADMOVE ris.re − z.im
                                   MADMOVE ris.im z.re

(in ris abbiamo messo il risultato della moltiplicazione) senza esecuzione di alcuna

MADMULT o MADADD.

Per concludere questa sezione sulle matrici di Dirac, mostriamo senza commento le linee Zz che definiscono l’istruzione #diracdef grazie alla quale è possibile al programmatore definire le sue “personali” matrici di Dirac. Notiamo che egli potrebbe farlo, con identici risultati, anche inserendo direttamente nei propri programmi linee di linguaggio Zz simili a quelle contenute nelle librerie standard:

            /dirac matrix − > gamma1 {
              /return [3, complex i, 2, complex i, 1, complex meno i, 0, complex meno i]

} ma naturalmente la definizione tramite lo statement #diracdef è più elegante.

             /stat − > #diracdef identˆ dirac name . . .
             dirac rowˆ r0 EOL dirac rowˆ r1 EOL dirac rowˆ r2 EOL dirac rowˆ r3 {
               /dirac matrix − > dirac name {
                 /return [r0.0, r0.1, r1.0, r1.1, r2.0, r2.1, r3.0, r3.1]

} }

             /dirac row − > dirac iˆ a dirac zˆ $ dirac zˆ $ dirac zˆ $ {
               /return [0, a]

}

             /dirac row − > dirac zˆ $ dirac iˆ a dirac zˆ $ dirac zˆ $ {
               /return [1, a]

}

             /dirac row − > dirac zˆ $ dirac zˆ $ dirac iˆ a dirac zˆ $ {
               /return [2, a]

}

             /dirac row − > dirac zˆ $ dirac zˆ $ dirac zˆ $ dirac iˆ a {
               /return [3, a]

}

             /dirac z − > 0
             /dirac i − > 1 { /return complex uno }
             /dirac i − > i { /return complex i }
             /dirac i − > −1 { /return complex meno uno }
             /dirac i − > −i { /return complex meno i }

La libreria QCD passa poi alla definizione del tipo su3. La sintassi relativa alla dichiarazioni di variabili su3, statiche o register, viene introdotta al solito tramite la linea:

                                     typedef su3(18)

Per il tipo su3 si sta assumendo una dimensione 18 (3 × 3 × 2) relativa alla possibilità di ritenere tutte le 9 componenti complesse dell’elemento del gruppo SU(3) posto nella sua rappresentazione fondamentale. Naturalmente il controllo del fatto che dati dichiarati su3 siano effettivamente in relazione ad elementi del gruppo SU(3) non è eseguito dal calcolatore ma lasciato al programmatore; il compilatore mette a disposizione una operazione normalize(su3 object) che consente di calcolare, a partire dalle 9 componenti complesse qualsiasi (quelle di un dato dichiarato come su3), l’elemento del gruppo SU(3) che meglio potrebbe essere specificato da esse; la normalize può essere utile quando, dopo un numero imprecisato a discrezione dell’operatore di operazioni su dati di tipo su3, si sospetti che i dati stessi, a causa dell’imprecisione fisica dei calcoli su eleboratore, non rapresentino più efficacemente elementi del gruppo SU(3).

Avremmo potuto definire un tipo su3 lasciando che esso avesse la dimensionalità minima per essere in relazione a elementi del gruppo SU(3); in realtà ci si è resi conto che nei programmi spesso si sentiva la necessità di accedere direttamente ai dati relativi agli elementi del gruppo posti nella loro rappresentazione fondamentale di matrici 3 × 3. Il calcolo degli elementi delle matrici 3 × 3 a partire dalla rappresentazione “minima”, avrebbe comportato una serie lunga di operazioni, e il risultato sarebbe stato un rallentamento nella esecuzione dei programmi.

Quello che d’altra parte si è fatto è stato di utilizzare una rappresentazione intermedia fra quella minima e quella estesa: si è definito un nuovo tipo, detto su3thin, con la caratteristica di conservare le sole prime due righe della matrice della rappresentazione fondamentale di SU(3); la dimensionalità ridotta delle su3thin (12) si presta a ottimizzazioni riguardo il consumo della memoria dell’elaboratore.

Per il tipo su3thin sono stati previsti molti meno impieghi rispetto ai più tradizionali tipi; ad esempio per l’su3thin sono possibili esclusivamente dichiarazioni di variabili statiche; questo è dovuto al fatto che l’utilizzo di su3thin deve avvenire al solo scopo di risparmiare memoria. La definizione dell’su3thin non può avvenire così tramite il typedef; si provvede invece a inserire nella libreria le sole linee Zz riguardanti le limitate applicazioni del tipo.

Per l’aritmetica sul tipo su3thin si ha come punto di riferimento quella del tipo maggiore su3; allorché si invoca una operazione sui dati associati ad una variabile di tipo su3thin, il compilatore automaticamente esegue una trasformazione verso il tipo su3, ricostruendo l’ultima riga della matrice sulla base delle proprietà possedute da una matrice unitaria:

/su3 fact − > su3thin varˆ a {
su3 temporary ret
/fastin(ret, a)

ret.[2, 0] = ret.[0, 1]˜ ∗ ret.[1, 2]˜ − ret.[0, 2]˜ ∗ ret.[1, 1]˜ ret.[2, 1] = ret.[0, 2]˜ ∗ ret.[1, 0]˜ − ret.[0, 0]˜ ∗ ret.[1, 2]˜ ret.[2, 2] = ret.[0, 0]˜ ∗ ret.[1, 1]˜ − ret.[0, 1]˜ ∗ ret.[1, 0]˜

/return ret

}

Per il tipo su3 sono definite le operazioni di coniugazione hermitiana:

/su3 fact − > su3 factˆ a ˜ {
su3 temporary ret

ret.[0, 0] = a.[0, 0] ret.[0, 1] = a.[1, 0] ret.[0, 2] = a.[2, 0] ret.[1, 0] = a.[0, 1] ret.[1, 1] = a.[1, 1] ret.[1, 2] = a.[2, 1] ret.[2, 0] = a.[0, 2] ret.[2, 1] = a.[1, 2] ret.[2, 2] = a.[2, 2]

/return ret

} come anche l’operazione del gruppo (il prodotto righe per colonne fra le matrici 3 × 3):

/su3 fact − > su3 termˆ a ∗ su3 factˆ b {
su3 temporary ret

ret.[0, 0] = a.[0, 0] ∗ b.[0, 0] + a.[0, 1] ∗ b.[1, 0] + a.[0, 2] ∗ b.[2, 0] ret.[0, 1] = a.[0, 0] ∗ b.[0, 1] + a.[0, 1] ∗ b.[1, 1] + a.[0, 2] ∗ b.[2, 1] ret.[0, 2] = a.[0, 0] ∗ b.[0, 2] + a.[0, 1] ∗ b.[1, 2] + a.[0, 2] ∗ b.[2, 2] ret.[1, 0] = a.[1, 0] ∗ b.[0, 0] + a.[1, 1] ∗ b.[1, 0] + a.[1, 2] ∗ b.[2, 0] ret.[1, 1] = a.[1, 0] ∗ b.[0, 1] + a.[1, 1] ∗ b.[1, 1] + a.[1, 2] ∗ b.[2, 1] ret.[1, 2] = a.[1, 0] ∗ b.[0, 2] + a.[1, 1] ∗ b.[1, 2] + a.[1, 2] ∗ b.[2, 2] ret.[2, 0] = a.[2, 0] ∗ b.[0, 0] + a.[2, 1] ∗ b.[1, 0] + a.[2, 2] ∗ b.[2, 0] ret.[2, 1] = a.[2, 0] ∗ b.[0, 1] + a.[2, 1] ∗ b.[1, 1] + a.[2, 2] ∗ b.[2, 1] ret.[2, 2] = a.[2, 0] ∗ b.[0, 2] + a.[2, 1] ∗ b.[1, 2] + a.[2, 2] ∗ b.[2, 2]

/return ret

}

Si deve insistere sul fatto che i due fattori a e b della operazione, sono già sui registri al momento della riduzione della regola; o perché vi sono già stati caricati dalla memoria, o perché sono il risultato di precedenti moltiplicazioni.

Infine viene definita la traccia:

         /complex fact − > tr su3 termˆ a {
           complex temporary ret

ret = a.[0, 0] + a.[1, 1] + a.[2, 2]

           /return ret

}

Degne di nota sono anche le linee di applicazione di una matrice di SU(3) all’indice di colore di un colorspin; anche in questo caso si insiste sull’ottimizzazione derivante dal caricare globalmente i due tipi strutturati (su3 e colorspin) sui registri e dal posticipare le operazioni aritmetiche.

/colorspin term − > su3 termˆ s ∗ colorspin termˆ c {
colorspin temporary ret

ret.[0, 0] = s.[0, 0] ∗ c.[0, 0] + s.[0, 1] ∗ c.[1, 0] + s.[0, 2] ∗ c.[2, 0] ret.[0, 1] = s.[0, 0] ∗ c.[0, 1] + s.[0, 1] ∗ c.[1, 1] + s.[0, 2] ∗ c.[2, 1] ret.[0, 2] = s.[0, 0] ∗ c.[0, 2] + s.[0, 1] ∗ c.[1, 2] + s.[0, 2] ∗ c.[2, 2] ret.[0, 3] = s.[0, 0] ∗ c.[0, 3] + s.[0, 1] ∗ c.[1, 3] + s.[0, 2] ∗ c.[2, 3] ret.[1, 0] = s.[1, 0] ∗ c.[0, 0] + s.[1, 1] ∗ c.[1, 0] + s.[1, 2] ∗ c.[2, 0] ret.[1, 1] = s.[1, 0] ∗ c.[0, 1] + s.[1, 1] ∗ c.[1, 1] + s.[1, 2] ∗ c.[2, 1] ret.[1, 2] = s.[1, 0] ∗ c.[0, 2] + s.[1, 1] ∗ c.[1, 2] + s.[1, 2] ∗ c.[2, 2] ret.[1, 3] = s.[1, 0] ∗ c.[0, 3] + s.[1, 1] ∗ c.[1, 3] + s.[1, 2] ∗ c.[2, 3] ret.[2, 0] = s.[2, 0] ∗ c.[0, 0] + s.[2, 1] ∗ c.[1, 0] + s.[2, 2] ∗ c.[2, 0] ret.[2, 1] = s.[2, 0] ∗ c.[0, 1] + s.[2, 1] ∗ c.[1, 1] + s.[2, 2] ∗ c.[2, 1] ret.[2, 2] = s.[2, 0] ∗ c.[0, 2] + s.[2, 1] ∗ c.[1, 2] + s.[2, 2] ∗ c.[2, 2] ret.[2, 3] = s.[2, 0] ∗ c.[0, 3] + s.[2, 1] ∗ c.[1, 3] + s.[2, 2] ∗ c.[2, 3]

/return ret

}

Infine, l’applicazione di una matrice di Dirac ad un dato di tipo colorspin è vista nei termini della già definita operazione di applicazione di una matrice di Dirac alle singole componenti spinoriali estraibili dal colorspin allorché viene fissato l’indice di colore.

colorspin fact − > dirac matrixˆ m ∗ colorspin factˆ c {
/c0 = m.0; /c1 = m.2; /c2 = m.4; /c3 = m.6
/e0 = m.1; /e1 = m.3; /e2 = m.5; /e3 = m.7
colorspin temporary ret
ret.[0, 0] = c.[0, c0] ∗ e0
ret.[0, 1] = c.[0, c1] ∗ e1
ret.[0, 2] = c.[0, c2] ∗ e2
ret.[0, 3] = c.[0, c3] ∗ e3
ret.[1, 0] = c.[1, c0] ∗ e0
ret.[1, 1] = c.[1, c1] ∗ e1
ret.[1, 2] = c.[1, c2] ∗ e2
ret.[1, 3] = c.[1, c3] ∗ e3
ret.[2, 0] = c.[2, c0] ∗ e0
ret.[2, 1] = c.[2, c1] ∗ e1
ret.[2, 2] = c.[2, c2] ∗ e2
ret.[2, 3] = c.[2, c3] ∗ e3
/return ret

}

CONCLUSIONI

Da due anni funziona su macchine della serie VAX-VMS e workstation UNIX un compilatore in grado di accettare il linguaggio APESE descritto nel capitolo 4.

Grazie a questo compilatore sono state fatte girare le prime applicazioni scientifiche su macchine della serie APE100, alcune delle quali hanno prodotto risultati scientifici interessanti. Complessivamente, superata una prima fase dedicata alla correzione degli errori, il compilatore si è ben comportato riuscendo a conseguire gli obiettivi di efficienza previsti. L’unico problema che sia stato realmente avvertito è consistito nei grandi tempi di attesa necessari alla compilazione di programmi anche di media lunghezza; la principale ragione di questo problema è da ricercarsi nel fatto che un parser dinamico (quello su cui è basato il compilatore per APESE), a causa delle più ampie potenzialità che mette a disposizione rispetto ad un parser statico, richiede certamente alla macchina che lo ospita una maggiore potenza di calcolo. Una soluzione a questo problema pare essere stata trovata nel ricorso alle workstation UNIX dell’ultima generazione sulle quali il compilatore APESE sembrerebbe girare proponendo tempi di compilazione accettabili.

E’ imminente l’uscita di una nuova versione del compilatore APESE, la 3.0, maggiormente curata verso una ulteriore minimizzazione dei tempi di compilazione e nella quale sono stati corretti e riveduti vizi e difetti della versione precedente, soprattutto per quanto riguarda il problema della visibilità delle variabili, a soluzione del quale è stata necessaria una nuova versione dello ZzParser.

Il linguaggio APESE è risultato un buon ambiente di sviluppo sia per quanto riguarda la programmazione interessante il campo della teoria QCD su reticolo, sia per quanto riguarda, opportunamente riconfigurato, altri settori di ricerca scientifica.

In questo paragrafo conclusivo vorrei dedicare qualche parola ad indicare le prospettive future che con APESE si sono dischiuse e rispetto alle quali l’APESE è appena un precursore.

L’APESE fondamentalmente indica nei linguaggi evolventi un interessante strumento da proporsi nella teoria della programmazione e non soltanto di ambito scientifico; utile non solo alla risoluzione di alcuni fra i tipici problemi che affliggono la teoria della programmazione, quale è il parsing delle strutture contextsensitive del linguaggio, ma anche in direzione di ulteriori sviluppi. La definizione di nuove strutture dati indirizzate al preciso ambito scientifico nel quale si svolge la programmazione, la definizione di nuovi operatori agenti su queste strutture e l’overloading degli operatori già esistenti, possono essere considerate delle semplici anticipazioni delle possibilità offerte da una riconfigurazione dinamica del linguaggio che solamente i parser dinamici mettono a disposizione.

Per quanto riguarda poi il parser dinamico ZzParser gli obiettivi sono ancora più ambiziosi: il linguaggio Zz intende addirittura proporsi come il linguaggio dei linguaggi, un linguaggio in grado di definire tutti gli altri linguaggi e di portarne a realizzazione i compilatori. In questo senso lo ZzParser potrebbe sostituirsi allo standard UNIX yacc, rispetto al quale possiede l’indubbio vantaggio derivantegli dal suo attributo di dinamicità; l’unico ostacolo a questo processo potrebbe essere costituito dalla già constatata richiesta da parte del ZzParser di migliori performance la quale sembrerebbe escludere un utilizzo di Zz su macchine non in possesso di particolari requisiti di potenza.

Il problema della definizione e della portabilità dei linguaggi di programmazione da una macchina all’altra trova in Zz una agevole soluzione: per ogni diversa distinta macchina sono realizzate le action (attualmente nella forma di routine in linguaggio C), ciascuna in relazione ad una singola azione elementare di compilazione; generalmente i linguaggi di programmazione usano sintassi anche molto diverse per riferirsi a niente altro che le stesse azioni elementari. Quindi per ogni nuovo (o vecchio) linguaggio di programmazione viene scritta, una volta per tutte, la rispettiva definizione in linee di linguaggio Zz; l’header HZ del linguaggio, vera carta d’identità anzi passaporto del linguaggio, può essere dunque portato su una qualsiasi macchina nella quale sia stato istallato uno ZzParser e per la quale siano state scritte le corrispondenti action.

Grazie a questa procedura l’APESE stesso, primo linguaggio definito attraverso Zz, può essere agevolmente portato su una qualsiasi altra macchina, parallela o sequenziale. La definizione del linguaggio, immodificata, resta quella contenuta negli header HZ, mentre l’unico ostacolo è rappresentato dalla riscrittura delle action C specifiche per la nuova macchina.

Naturalmente l’efficienza e le ottimizzazioni dell’APESE sono perse su macchine non in grado di supportarle; ad esempio, la definizione di tipi strutturati e di operatori fra tipi strutturati perde parte del proprio valore su macchine con processori a set di registri limitato; tuttavia l’eleganza formale della sintassi definita per l’APESE può essere di stimolo per un trasporto di APESE anche su macchine meno potenti di APE100.

Infine vorrei segnalare all’attenzione i primi approcci verso riconfigurazioni del linguaggio APESE che mettessero in uso la proprietà di evolvenza del linguaggio seguendo le indicazioni fornite da APESE stesso; in particolare i risultati che si sono avuti nel campo della fluidodinamica grazie alla definizione di nuove strutture linguistiche; e inoltre i risultati ottenuti in applicazioni per la simulazione di reti neuronali.

APPENDICE A

A.1 LIMITE AL CONTINUO DELLA AZIONE DI WILSON

Richiamiamo l’espressione che abbiamo visto per l’azione gauge-invariante di una teoria di pura gauge sul reticolo:

S = \frac{1}{2g^{2}} ∑_{n,μ,ν} tr[U_{μ}(x_{n})U_{ν}(x_{n+μ})U_{μ}^{−1}(x_{n+ν})U_{ν}^{−1}(x_{n})] + h.c.  (A.1.1)

ove:

U_{μ}(x_{n}) = e^{igaA_{μ}(x_{n})}  (A.1.2)

La S in funzione dei campi A_{μ}(x_{n}) è perciò:

S = \frac{1}{2g^{2}} ∑_{n,μ,ν} tr [e^{igaA_{μ}(x_{n})}e^{igaA_{ν}(x_{n+μ})}e^{−igaA_{μ}(x_{n+ν})}e^{−igaA_{ν}(x_{n})}] + h.c.  (A.1.3)

Ora noi possiamo sviluppare le espressioni del tipo A_{μ}(x_{n+ν}) in un intorno di a = 0:

A_{μ}(x_{n+ν}) = A_{μ}(x_{n}) + a∂_{ν}A_{μ}(x_{n}) + o(a)

E sostituendo nella S, otteniamo:

S = \frac{1}{2g^{2}} ∑_{n,μ,ν} tr[e^{igaA_{μ}(x_{n})}e^{iga[A_{ν}(x_{n})+a∂_{μ}A_{ν}(x_{n})]}

e^{−iga[A_{μ}(x_{n})+a∂_{ν}A_{μ}(x_{n})]}e^{−igaA_{ν}(x_{n})}] + h.c.  (A.1.4)

Il prodotto di due esponenziali è l’esponenziale della somma solo nel caso i due esponenti commutino; altrimenti dovremo avvalerci della identità operatoriale:

e^{ϵA}e^{ϵB} = e^{ϵ(A+B)+ \frac{1}{2} ϵ^{2}[A,B]+o(ϵ^{2})}  (A.1.5)

Per la S dunque abbiamo:

S = \frac{1}{2g^{2}} ∑_{n,μ,ν} tr[e^{iga[A_{μ}(x_{n})A_{ν}(x_{n})+a∂_{μ}A_{ν}(x_{n})+ \frac{1}{2} iga[A_{μ}(x_{n}),A_{ν}(x_{n})]+o(a^{2})]}·
e^{−iga[A_{μ}(x_{n})A_{ν}(x_{n})+a∂_{ν}A_{μ}(x_{n})− \frac{1}{2} iga[A_{μ}(x_{n}),A_{ν}(x_{n})]+o(a^{2})]}] + h.c.  (A.1.6)
= \frac{1}{2g^{2}} ∑_{n,μ,ν} tr[e^{iga^{2}[∂_{μ}A_{ν}(x_{n})−∂_{ν}A_{μ}(x_{n})+ig[A_{μ}(x_{n}),A_{ν}(x_{n})]]+o(a^{2})}] + h.c.  (A.1.7)

Nell’ultima espressione può essere riconosciuto il tensore di campo F_{μν}, fornito giustappunto dalla:

F_{μν}(x_{n}) = ∂_{μ}A_{ν}(x_{n}) + ∂_{ν}A_{μ}(x_{n}) + ig[A_{μ}(x_{n}), A_{ν}(x_{n})]  (A.1.8)

L’azione assume dunque la semplice forma:

S = \frac{1}{2g^{2}} ∑_{n,μ,ν} tr [e^{iga^{2}F_{μν}(x_{n})+o(a^{2})}] + h.c.  (A.1.9)

Sviluppiamo ora l’esponenziale in serie:

S = \frac{1}{2g^{2}} ∑_{n,μ,ν} tr [1 + iga^{2}F_{μν}(x_{n}) − \frac{1}{2}g^{2}a^{4}F_{μν}(x_{n})F^{μν}(x_{n}) + o(a^{4})] + h.c.  (A.1.10)

Poiché la traccia dei generatori dell’algebra è nulla, abbiamo che:

tr[F_{μν}] = F_{μν}^{a} tr[T^{a}] = 0  (A.1.11)

Tenendo conto di questo risultato ed esplicitando i termini che finora abbiamo indicato con ‘h.c.’, otteniamo per S:

S = \frac{1}{2g^{2}} ∑_{n,μ,ν} 2 tr[1] − g^{2}a^{4} tr[F_{μν}(x_{n})F^{μν}(x_{n})] + o(a^{4})  (A.1.12)

Al limite per a → 0 dobbiamo fare la sostituzione:

∑_{n,μ,ν} → ∫ \frac{d^{4}x}{a^{4}} ∑_{μ,ν}

Sicché finalmente otteniamo:

S = −\frac{1}{2} ∫ d^{4}x tr[F_{μν}(x_{n})F^{μν}(x_{n})] + \frac{1}{g^{2}} ∫ \frac{d^{4}x}{a^{4}} ∑_{μν} tr[1]  (A.1.13)

che a meno dell’ultimo termine è proprio la azione di Yang-Mills. In realtà quindi, al posto dell’azione (A.1.1), dovremo considerare l’azione:

S = \frac{1}{2g^{2}} ∑_{n,μ,ν} tr [U_{μ}(x_{n})U_{ν}(x_{n+μ})U_{μ}^{−1}(x_{n+ν})U_{ν}^{−1}(x_{n}) − 1] + h.c.  (A.1.14)

che ha come limite al continuo esattamente la azione di Yang-Mills:

S_{YM} = −\frac{1}{2} ∫ d^{4}x tr[F_{μν}(x_{n})F^{μν}(x_{n})]  (A.1.15)

A.2 ALGORITMO DI METROPOLIS

Sia P_{N}(U) la probabilità di trovare la configurazione dei campi di gauge U dopo N passi elementari dell’algoritmo di Metropolis. Dobbiamo dimostrare che per N tendente a infinito la P_{N}(U) tenda alla distribuzione di Boltzmann exp[−S(U)] (Binder, 1979).

Consideriamo la relazione fra il passo N-esimo e il passo (N + 1)-esimo dell’algoritmo di Metropolis:

P_{N+1}(U) = ∑_{U′} W (U′ → U)P_{N}(U′) + [1 − ∑_{U′} W (U → U′)] P_{N}(U)  (A.2.1)

W (U′ → U) è la probabilità che il sistema nella configurazione U′ al passo N, passi alla configurazione U; di conseguenza, 1 − ∑_{U′} W (U → U′) è la probabilità che il sistema, già nella configurazione U al passo N, permanga in tale configurazione.

P_{N+1}(U) = P_{N}(U) + ∑_{U′}[W (U′ → U)P_{N}(U′) − W (U → U′)P_{N}(U)]  (A.2.2)

In una situazione di stazionarietà, ossia se per ogni U e U′ fosse:

W (U′ → U)P_{N}(U′) = W (U → U′)P_{N}(U)  (A.2.3)

avremmo:

P_{N+1}(U) = P_{N}(U)

Nel caso invece per qualche U e U′ fosse:

W (U′ → U)P_{N}(U′) > W (U → U′)P_{N}(U)

allora avremmo:

\begin{aligned}P_{N+1}(U) & > P_{N}(U) \\ P_{N+1}(U′) & < P_{N}(U′) \end{aligned}

Sicché ripetute applicazioni del metodo conducono il sistema nella situazione di stazionarietà; alla fine, dunque, per ogni U e U′ vale la:

\frac{P_{∞}(U)}{P_{∞}(U′)} = \frac{W (U′ → U)}{W (U → U′)}  (A.2.4)

Ora ricordando che l’algoritmo di Metropolis pone:

W (U → U′) = \{ \begin{matrix}1 & se S(U) > S(U′) \\ e^{−[S(U′)−S(U)]} & se S(U) < S(U′) \end{matrix}  (A.2.5)

Sostituendo nella (A.2.4), otteniamo:

\frac{P_{∞}(U)}{P_{∞}(U′)} = e^{−[S(U)−S(U′)]}

Il risultato finale è perciò:

P_{∞}(U) = ce^{−S(U)}

come dovevamo dimostrare.

APPENDICE B

B.1 DIMOSTRAZIONE DELLA L(T_{CF}) ⊃ L(T_{REG})

Per dimostrare la L(T_{CF}) ⊃ L(T_{REG}) possiamo semplicemente dimostrare che esiste almeno un linguaggio generabile da una grammatica context-free che non possa essere generato attraverso una grammatica regolare.

Prenderemo il linguaggio L_{3}:

L_{3} = { a^{n}ba^{n}|n ≥ 1 }

per cui nel capitolo 2 fornimmo una grammatica context-free dalla quale poteva essere generato.

Supponiamo che per L_{3} esista una grammatica regolare che lo generi; come tutte le grammatiche, essa avrà un insieme finito di simboli non-terminali: poniamo sia q il numero di simboli non-terminali della grammatica.

Vogliamo generare attraverso questa grammatica una frase a^{n}ba^{n} con n > q che, per definizione, appartiene ad L_{3}. Poiché per le grammatiche regolari il processo di produzione di una frase avviene attraverso successive frasi che incrementano la loro lunghezza ad ogni passo di 1, per generare la parte a^{n} a sinistra di b saranno necessari n passi, corrispondentemente a n linee di derivazione. Necessariamente dunque, su alcune di queste linee di derivazione troveremo ripetuto almeno un simbolo non-terminale della grammatica. Indizio evidente che la grammatica contiene regole di produzione ricorsive; ma dunque, se per generare la a^{n} a sinistra di b abbiamo usato un certo numero di volte, ricorsivamente, uno stesso set di regole di produzione, nulla ci impedisce, allorché passiamo a generare la frase a^{n} a destra di b, di usare la tecnica della recursione per un numero di volte diverso. Il risultato è che il linguaggio comprenderebbe le frasi a^{n}ba^{n}, ma purtroppo anche le frasi a^{n}ba^{m} con n ≠ m; e dunque una grammatica regolare non è in grado di generare L_{3}. L_{3} appartiene a L(T_{CF}) ma non a L(T_{REG}).

Osserviamo d’altra parte che per il linguaggio:

L′_{3} = { a^{n}ba^{n}|1 ≤ n ≤ N }

invece esiste una grammatica regolare. Infatti in questo caso possiamo evitare la tecnica della ricorsività semplicemente scegliendo q > N.

APPENDICE C

C.1 ROUTINE SEMANTICHE

Saranno presentate in questa sezione, a scopo puramente esemplificativo, alcune fra le più significative routine semantiche utilizzate dal compilatore APESE per la generazione del codice. Come abbiamo già spiegato, esse hanno la forma di funzioni scritte in linguaggio C linkate al ZzParser e richiamate dal ZzLanguage nella forma di UCP.

Gli argomenti per lo scambio dei dati fra il ZzParser e le funzioni sono in numero di 3: un intero argc specificante il numero di argomenti passato dal programma chiamante; un vettore di argomenti in chiamata argv a dimensione argc, contenente una serie di puntatori a strutture di tipo s content; infine l’argomento di ritorno della funzione ret, contenente il puntatore ad una struttura s content. La struttura C s content è il tipo con cui vengono scambiati argomenti tra parser e routine semantiche; ha due campi: un campo ->tag, nel quale viene specificato il tipo Zz del dato (int, ident, qstring, etc.); un campo ->value che contiene il valore assunto dal dato. L’interpretazione del valore dipende naturalmente dal tipo Zz ad esso associato, ma viene lasciata comunque grande libertà nell’associare valori e tipi non corrispondenti: il puntatore ad una struttura gloper viene ritornato ad esempio come INT, ma l’interpretazione a livello di linguaggio Zz del valore del puntatore secondo la classe sintattica int non avrebbe alcun senso.

Per prime verranno presentate le action per la dichiarazione di variabili e vettori floating point: la a decl float var e la a decl float array. Sono richiamate specificando quale primo argomento il nome della variabile o array (una stringa o puntatore a carattere, in accordo con la concezione tipica del linguaggio C); il secondo argomento è l’intero specificante la dimensione del tipo floating point (ad esempio 2 nel caso la variabile sia complessa, 8 nel caso sia di tipo spinor, etc.); la a decl float array richiede un terzo argomento specificante la dimensione globale (vista come unidimensionale) dell’array. Entrambe ritornano una struttura gloper che riassume le informazioni relative alla variabile semplice o all’array dichiarato.

/* ACTION PER LA DICHIARAZIONE DI VARIABILI SEMPLICI FLOATING POINT */

a ḋecl˙ float v̇ar(argc,argv,ret)

ui argc ;

struct s ċontent *ret,*argv ;

–

struct gloper *new ; char string[40] ;

new = (struct gloper *) calloc (1,sizeof(struct gloper)) ;

new-¿check = GLOPER ;

new-¿name = (char*) argv[0].value ;

new-¿type = ’f’ ;

new-¿size = argv[1].value ; /* IL SECONDO ARG. E’ LA SIZE DELLA VARIABILE */ new-¿dim = 1 ; new-¿ipost = IMM; if(argc¿2) – new-¿address = argv[2].value; ˝ else – new-¿address = mad.next v̇ar ȧddress ; mad.next v̇ar ȧddress += new-¿size ; if(mad.next v̇ar ȧddress¿MAD V̇AR ṀAX ȦDDRESS)

– ėrr ṗrintf(”A ḊECL ḞLOAT V̇AR oltre i limiti di memoria”) ; return(0) ; ˝

˝ new-¿sign = ’ ’ ; new-¿pre = new-¿address - MAD V̇AR L̇OW ȦDDRESS ; new-¿prename = new-¿name ; new-¿post = MAD V̇AR ḊSREG ; new-¿postname = scpu.dsreg u̇sage[MAD V̇AR ḊSREG] ; if (asm˙ flag) – sprintf(string,”““equ %s 0x%x”,new-¿name,new-¿pre) ; ṗreproc ḟprintf(string) ; sprintf(string,”““equ %s˙ 0x%x”,new-¿name,new-¿address) ; ṗreproc ḟprintf(string) ; ˝ ret-¿tag = TAG İNT ; ret-¿value = (long) new ; return(1) ; ˝

/* ACTION PER LA DICHIARAZIONE DI ARRAYS FLOATING POINT */

a ḋecl˙ float ȧrray(argc,argv,ret) ui argc ; struct s ċontent *ret,*argv ; – struct gloper *new ; char string[80] ;

new = (struct gloper *) calloc (1,sizeof(struct gloper)) ; new-¿check = VIRTUAL ĠLOPER ; new-¿name = (char*) argv[0].value ; new-¿type = ’f’ ; new-¿size = argv[1].value ; /* IL SECONDO ARG. E’ LA SIZE DEI SINGOLI ELEM.*/ new-¿dim = argv[2].value ; /* IL TERZO E’ LA DIMENSIONE DELL’ARRAY */ new-¿address = mad.next ȧrray ȧddress ; new-¿ipost = IMM; mad.next ȧrray ȧddress += new-¿size * new-¿dim ; if(mad.next ȧrray ȧddress¿MAD ȦRRAY ṀAX ȦDDRESS)

– ėrr ṗrintf(”A ḊECL ḞLOAT ȦRRAY oltre i limiti di memoria”) ; return(0) ; ˝

if (asm˙ flag) – sprintf(string,”““equ %s˙ 0x%x”,new-¿name,new-¿address) ; ṗreproc ḟprintf(string) ; ˝ ret-¿tag = TAG İNT ; ret-¿value = (long) new ; return(1) ;

˝

La action che segue viene richiamata per eseguire l’indicizzazione degli array; ha bisogno di due argomenti: la struttura gloper relativa all’array da indicizzare, più precisamente la struttura gloper ritornata da una precedente chiamata dichiarativa alla a decl float array o alla a decl int array; la struttura gloper relativa alla espressione intera indicizzante l’array. Ritorna la struttura gloper associata al preciso elemento dell’array indicizzato.

/* ACTION PER L’INDICIZZAZIONE DEGLI ARRAY */

a k̇et(argc,argv,ret) int argc; struct s ċontent *ret,*argv; – struct gloper *new,*base,*indice ; base = (struct gloper *) argv[0].value ; /* IL PRIMO ARG. E’ IL VETTORE */ indice = (struct gloper *) argv[1].value ; /* IL SECONDO E’ L’INDICE */ if(base-¿check!=VIRTUAL ĠLOPER && base-¿check!=VIRTUAL ȦRRAY ȦRG ĠLOPER

&& base-¿check!=LATTICE V̇IRTUAL ĠLOPER && indice-¿check!=GLOPER ) – ġloper ėrr ṗrintf(”A K̇ET chiamata con strano gloper”,indice) ; return(0) ; ˝ new= ġet ȧrray ḋsreg(base) ; if(base-¿size==1) if((indice-¿ipost==IMM) && (indice-¿ipre==0)) – ċopy ḃ ṫo i̇(new,indice); ġloper ċfree(indice); ret-¿tag = TAG İNT; ret-¿value = (long) new ; return(1); ˝ else – struct gloper *tmp ṙeg ġloper ; tmp ṙeg ġloper=˙ find ḃin ȯp ṫmp ṙeg(base,indice) ; if(tmp ṙeg ġloper==0)

– ėrr ṗrintf(”A K̇ET registri temporanei terminati”); return(0);˝ ȯgg ḟprintf(MOVE,tmp ṙeg ġloper,indice) ; ċopy ḃ ṫo i̇(new,tmp ṙeg ġloper); ret-¿tag = TAG İNT; ret-¿value = (long) new ; return(1) ; ˝ else – if( base-¿check!=LATTICE V̇IRTUAL ĠLOPER ) – ui argc ; struct gloper *tmp ṙeg ġloper, *size ; size= ḋecl i̇nt i̇mm(base-¿size) ; tmp ṙeg ġloper=˙ find ḃin ȯp ṫmp ṙeg(base,indice) ; if(tmp ṙeg ġloper==0) return(0) ; ȯggg ḟprintf(MULT,tmp ṙeg ġloper,indice,size) ; ċopy ḃ ṫo i̇(new,tmp ṙeg ġloper) ; ret-¿tag = TAG İNT; ret-¿value = (long) new ; return(1) ; ˝ else – ui argc ; int tmp ; struct gloper *tmp ṙeg ġloper, *tmp ṙeg ġloper1, *size, *masq1, *masq ; size= ḋecl i̇nt i̇mm(base-¿size) ; masq= ḋecl i̇nt i̇mm(0x00FFFFFF) ; masq1= ḋecl i̇nt i̇mm(0xFF000000) ; tmp ṙeg ġloper1= ġet ṫmp ṙeg() ; if(tmp ṙeg ġloper1==0) return(0) ; ȯggg ḟprintf(OR,tmp ṙeg ġloper1,indice,masq1) ; tmp ṙeg ġloper=˙ find ḃin ȯp ṫmp ṙeg(base,indice) ; if(tmp ṙeg ġloper==0) return(0) ; ȯggg ḟprintf(AND,tmp ṙeg ġloper,indice,masq) ; ȯggg ḟprintf(MULT,tmp ṙeg ġloper,tmp ṙeg ġloper,size) ; ȯggg ḟprintf(OR,tmp ṙeg ġloper,tmp ṙeg ġloper,tmp ṙeg ġloper1) ; if((tmp= q̇uale ṫmp ṙeg(tmp ṙeg ġloper1))!=-1) ḟree ṫmp ṙeg(tmp) ; ċopy ḃ ṫo i̇(new,tmp ṙeg ġloper) ; ret-¿tag = TAG İNT; ret-¿value = (long) new ; return(1) ; ˝ ˝ ˝

Infine alcune action relative a operazioni aritmetiche o di “move” per dati floating point; a differenza delle action precedentemente mostrate, queste aggiungono sempre linee di codice al listato ASSEMBLER prodotto della compilazione. E’ interessante scoprire in esse il modo con cui vengono prese in considerazione le ottimizzazioni riguardanti moltiplicazioni o addizioni per 0.0 o 1.0.

/* ACTION PER IL MOVE DI DATI SUL MAD */

/* Viene presa in considerazione l’ottimizzazione riguardante i dati complessi 1.0 , -1.0 , i , -i facendo riferimento ai registri 0 e 1 hardware fixed a 0.0 e 1.0 */

a ḃundlemove(argc,argv,ret) ui argc ; struct s ċontent *ret,*argv ; – struct s ċontent cdummy; struct bundle *target,*source; int i,n,ok; target = (struct bundle*) argv[0].value; source = (struct bundle*) argv[1].value; ok= ȧllocate ḃundle(target); if(!ok) return 0; n = target-¿size; if(source-¿size!=n) – zzerrmsg(”madbundlemove. different bundle sizes (%d,%d)”,n,source-¿size); return 0; ˝ if(source-¿reg ṫype==SPECTRALMADREG) –

if(source-¿name==COMPLEXZERONAME)

– ȯbibi ḟprintf(MADMOVE,target,0,&real żero,0) ; ȯbibi ḟprintf(MADMOVE,target,1,&real żero,0) ;

˝ else–

if(source-¿name==COMPLEXUNONAME)

–

ȯbibi ḟprintf(MADMOVE,target,0,&real u̇no,0) ; ȯbibi ḟprintf(MADMOVE,target,1,&real żero,0) ; ˝ else– if(source-¿name==COMPLEXINAME) – ȯbibi ḟprintf(MADMOVE,target,0,&real żero,0) ; ȯbibi ḟprintf(MADMOVE,target,1,&real u̇no,0) ; ˝ else– if(source-¿name==COMPLEXMENOUNONAME) – ȯbibi ḟprintf(MADMOVE,target,0,&real ṁeno u̇no,0) ; ȯbibi ḟprintf(MADMOVE,target,1,&real żero,0) ; ˝ else– if(source-¿name==COMPLEXMENOINAME) – ȯbibi ḟprintf(MADMOVE,target,0,&real żero,0) ; ȯbibi ḟprintf(MADMOVE,target,1,&real ṁeno u̇no,0) ; ˝ ˝ ˝ ˝ ˝

˝ else – for(i=0;i¡n;i++) ȯbibi ḟprintf(MADMOVE,target,i,source,i); return(1) ; ˝

˝

/* ACTION PER LA MOLTIPLICAZIONE DI DATI FLOATING POINT */

/* Viene ottimizzata la moltiplicazione per 0.0 (come risultato viene passato il registro hardware fixed a 0.0, senza l’esecuzione di alcuna operazione) e la moltiplicazione per 1.0 (come risultato viene passato il moltiplicando). */

a ṁadmult(argc,argv,ret) ui argc ; struct s ċontent *ret,*argv ; – int ok; struct s ċontent csize; struct bundle *bundle1, *bundle2 ;

bundle1=(struct bundle *) argv[0].value ; bundle2=(struct bundle *) argv[1].value ; if(bundle1-¿check!=BUNDLE) ḃundle ėrr ṗrintf(”MADMULT chiamata con strano bundle”,bundle1) ; if(bundle1-¿madreg==NOMADREG) ḃundle ėrr ṗrintf(”MADMULT chiamata con bundle non allocato”,bundle1) ; if(bundle2-¿check!=BUNDLE) ḃundle ėrr ṗrintf(”MADMULT chiamata con strano bundle”,bundle2) ; if(bundle2-¿madreg==NOMADREG) ḃundle ėrr ṗrintf(”MADMULT chiamata con bundle non allocato”,bundle2) ;

if(bundle1-¿reg ṫype==PHYSMADREG) switch(bundle1-¿madreg) – case ZERO : –ret-¿tag = TAG İNT; ret-¿value = (long) &real żero; return(1);˝ case UNO : –ret-¿tag = TAG İNT; ret-¿value = (long) bundle2; return(1) ; ˝ case MENOUNO : – csize.tag=TAG İNT; csize.value=1; ok=a ċreate ḃundle(1,&csize,ret); if(!ok) return 0; ȯbisbi ḟprintf(MADMOVE,ret-¿value,0,’-’,bundle2,0); return 1; ˝ default : break;

˝ if(bundle2-¿reg ṫype==PHYSMADREG) switch(bundle2-¿madreg) – case ZERO : –ret-¿tag = TAG İNT; ret-¿value = (long) &real żero; return(1);˝ case UNO : –ret-¿tag = TAG İNT; ret-¿value = (long) bundle1; return(1) ; ˝ case MENOUNO : – csize.tag=TAG İNT; csize.value=1; ok=a ċreate ḃundle(1,&csize,ret); if(!ok) return 0; ȯbisbi ḟprintf(MADMOVE,ret-¿value,0,’-’,bundle1,0); return 1; ˝ default : break;

˝ csize.tag=TAG İNT; csize.value=1; ok=a ċreate ḃundle(1,&csize,ret); if(!ok) return 0; ȯbibibi ḟprintf(MADMULT,ret-¿value,0,bundle1,0,bundle2,0); return 1; ˝

/* ACTION PER L’ADDIZIONE DI DATI FLOATING POINT */

/* Viene ottimizzata la addizione per 0.0 (come risultato viene passato l’addendo). */

a ṁadadd(argc,argv,ret) ui argc ; struct s ċontent *ret,*argv ; – int ok; struct s ċontent csize; struct bundle *bundle1, *bundle2 ;

bundle1=(struct bundle *) argv[0].value ; bundle2=(struct bundle *) argv[1].value ; if(bundle1-¿check!=BUNDLE) ḃundle ėrr ṗrintf(”MADADD chiamata con strano bundle”,bundle1) ; if(bundle1-¿madreg==NOMADREG) ḃundle ėrr ṗrintf(”MADADD chiamata con bundle non allocato”,bundle1) ; if(bundle2-¿check!=BUNDLE) ḃundle ėrr ṗrintf(”MADADD chiamata con strano bundle”,bundle2) ; if(bundle2-¿madreg==NOMADREG) ḃundle ėrr ṗrintf(”MADADD chiamata con bundle non allocato”,bundle2) ;

if(bundle1-¿reg ṫype==PHYSMADREG) switch(bundle1-¿madreg) – case ZERO : –ret-¿tag = TAG İNT; ret-¿value = (long) bundle2; return(1) ; ˝ default : break;

˝ if(bundle2-¿reg ṫype==PHYSMADREG) switch(bundle2-¿madreg) – case ZERO : –ret-¿tag = TAG İNT; ret-¿value = (long) bundle1; return(1) ; ˝ default : break;

˝ csize.tag=TAG İNT; csize.value=1; ok=a ċreate ḃundle(1,&csize,ret); if(!ok) return 0; ȯbibibi ḟprintf(MADADD,ret-¿value,0,argv[0].value,0,argv[1].value,0); return 1;

˝

C.2 ESEMPI DI LISTATI Zz

Mostreremo ora alcune parti significative dei listati Zz relativi alle librerie HZ del linguaggio APESE.

Il primo esempio riportato si riferisce al listato completo della libreria TYPE.HZ, la libreria che definisce le caratteristiche generali del linguaggio APESE. Può essere senz’altro riconosciuta la definizione dell’istruzione typedef anche se qui la troviamo confusa all’interno di tutta una serie di complicazioni, omesse nella descrizione didattica fornita nel capitolo 5, le quali non possono non essere presenti in una definizione reale del linguaggio. Ad esempio una complicazione risulta nelle linee che si prendono cura di un eventuale successivo debugging del programma in compilazione.

!! !! LIBRERIA TYPE.HZ !! constant i̇nt -¿ intˆ $ :pass

recdef-¿record :return 1 recdef-¿:return 0

!! !! DEFINIZIONE DELL’ISTRUZIONE typedef !! stat -¿ typedef identˆ type ”(” constant i̇ntˆ element ṡize ”)” recdefˆ recd– /constant = type& ċonstant /var = type& v̇ar /array = type& ȧrray /var ṙeg = type& v̇ar ṙeg /lvar = type& l̇var /lvar ṙeg = type& l̇var ṙeg /expr = type& ėxpr /term = type& ṫerm /fact = type& ḟact /decl l̇ist = type& ḋecl l̇ist /decl i̇tem = type& ḋecl i̇tem /decl ṙeg l̇ist = type& ḋecl ṙeg l̇ist /decl ṙeg i̇tem = type& ḋecl ṙeg i̇tem /decl ṫemp l̇ist = type& ḋecl ṫemp l̇ist /decl ṫemp i̇tem = type& ḋecl ṫemp i̇tem /record ḋecl l̇ist = record˙ &type& ḋecl l̇ist /record ḋecl i̇tem = record˙ &type& ḋecl i̇tem /boo = boo˙ &element ṡize

/fix lvar;/fix lvar ṙeg /fix var;/fix var ṙeg /fix array;/fix constant /fix expr;/fix element ṡize

num ė -¿ ṡize ȯf type : return element ṡize write ṡmb ṫripletta type

/dati ṡu ṫipo=”[”&type&”,[],”&element ṡize&”,“”fpu ṁemory“”]”

/if recd==0 – write ṡmb ė ȧrg ṅot ṙecord type write ṡmb ”append ”&dati ṡu ṫipo&” to elenco ṫipi”

˝ /if recd==1 –write ṡmb ė ȧrg ṙecord˝

stat -¿ type decl l̇istˆ $ decl l̇ist -¿ decl i̇temˆ $ decl l̇ist -¿ decl l̇istˆ $ ”,” decl i̇temˆ $

boo -¿ : create ḃundle (element ṡize)

decl i̇tem -¿ identˆ id – /addr = declare ṙeal v̇ar(id,element ṡize) as int var -¿ id : return addr as var write ṡmb v̇ar type,id,addr /dati=”[”&id&”,“”1“”,”&type&”,”&element ṡize&...

”,“”fpu memory“”,”&fis ȧddr addr&”]”

write ṡmb ”append ”&dati&” to elenco v̇ar”

˝

decl i̇tem -¿ identˆ id ”[” decl i̇ndexˆ index ”]” –

/index1=”index˙ ”&index

/size = ṡizeof index1 /addr = declare ṙeal ȧrray (id,element ṡize,size) as int var -¿ id ”[” index1ˆ disp ”]” : ket(addr,disp) as var var -¿ id ”[””#”integer ėxprˆ disp”]” : ket(addr,disp) as var

array -¿ id : return addr as array

num ė -¿ ḋim ȯf id :return size num ė -¿ ṫyp ȯf id :return type write ṡmb ȧrray type,id,addr,index1 /dati=”[”&id&”,“””&index&”“”,”&type&”,”&element ṡize&...

”,“”fpu memory“”,”&fis ȧddr addr&”]”

write ṡmb ”append ”&dati&” to elenco v̇ar”

˝

stat -¿ type register decl ṙeg l̇istˆ $ decl ṙeg l̇ist -¿ decl ṙeg i̇temˆ $ decl ṙeg l̇ist -¿ decl ṙeg l̇istˆ $ ”,” decl ṙeg i̇temˆ $

decl ṙeg i̇tem -¿ identˆ id – /addr = declare ṙeal ṙeg(id,element ṡize) var ṙeg -¿ id : return addr as var ṙeg ˝

record ḋecl -¿ type record ḋecl l̇istˆ $ record ḋecl l̇ist -¿ record ḋecl i̇temˆ $ record ḋecl l̇ist -¿ record ḋecl l̇istˆ $ ”,” record ḋecl i̇temˆ $

record ḋecl i̇tem -¿ identˆ name – /disp v̇alue = local ȧddress

/tmp = local ȧddress + element ṡize num ė -¿ local ȧddress : return tmp /imm ḋisp ṅame = current ṙecord & ˙ & type & ˙ & imm ḋisp /disp ṅame = current ṙecord & ˙ & type & ˙ & disp imm ḋisp ṅame -¿ name : return disp v̇alue disp ṅame -¿ imm ḋisp ṅameˆ $ : decl i̇nt i̇mm /big = current ṙecord & v̇ar /big ṙeg = big & ṙeg var -¿ bigˆ v ”.” disp ṅameˆ d : ... dyn ṡubgloper(v,d,element ṡize) as var var ṙeg -¿ big ṙegˆ v ”.” imm ḋisp ṅameˆ d : ... extract ḃundle(v,d,element ṡize) as var ṙeg write ṡmb ḋisp big,var,name,disp v̇alue append name to campi ṙecord append [name,type,””&element ṡize] to dati ċampi ˝

stat -¿ type temporary decl ṫemp l̇istˆ $ decl ṫemp l̇ist -¿ decl ṫemp i̇temˆ $ decl ṫemp l̇ist -¿ decl ṫemp l̇istˆ $ ”,” decl ṫemp i̇temˆ $ decl ṫemp i̇tem -¿ l̇ v̇alueˆ aaa booˆ bundle :z ṗaram ȧssign(aaa,bundle,var ṙeg)

decl ȧrg i̇tem -¿ type identˆ id – /size = 1 /addr = $decl˙ float ȧrray ȧrg(id,element ṡize,size) var -¿ id : get ḋsreg(addr) as var ˝

decl ȧrg i̇tem -¿ type identˆ id ”[” decl i̇ndexˆ index ”]” –

/index1=”index˙ ”&index

/size = ṡizeof index1 /addr = $decl˙ float ȧrray ȧrg(id,element ṡize,size) var -¿ id ”[” index1ˆ disp ”]” : ket(addr,disp) as var

array -¿ id : return addr as array

˝

decl ȧrg i̇tem -¿ ”value” ”of” type identˆ id – /addr = $decl l̇ocal˙ float v̇ar(id,element ṡize) var -¿ id : return addr as var ˝

/call v̇ = call˙ &type&v /call ṗ = call˙ &type&p /decl v̇ = decl˙ &type&v /decl ṗ = decl˙ &type&p

prot i̇tem -¿ type ”value” – /return type&v ˝

prot i̇tem -¿ type – /return type&p ˝

call ṗ -¿ varˆ arg : save ḃy ȧddress ȯn ṡstack call ṗ -¿ arrayˆ arg : save ḃy ȧddress ȯn ṡstack call v̇ -¿ varˆ arg : save ȯn ḟstack decl ṗ -¿ type identˆ id – /size = 1 /addr = $decl˙ float ȧrray ȧrg(id,element ṡize,size) var -¿ id : get ḋsreg(addr) as var ˝

decl ṗ -¿ type identˆ id ”[” decl i̇ndexˆ index ”]” – /index1=”index˙ ”&index /size = ṡizeof index1

/addr = $decl˙ float ȧrray ȧrg(id,element ṡize,size) var -¿ id ”[” index1ˆ disp ”]” : ket(addr,disp) as var

array -¿ id : return addr as array

˝

decl v̇ -¿ ”value” ”of” type identˆ id – /size = 1 /addr = $decl l̇ocal˙ float v̇ar(id,element ṡize) var -¿ id : return addr as var ˝

float v̇ar -¿ varˆ $ : pass float ȧrray -¿ arrayˆ $ : pass

stat -¿ rreturn exprˆ aaa : z ṙeturn(aaa,var ṙeg)

stat -¿ var ṙegˆ $ ”=” exprˆ $ : bundlemove stat -¿ varˆ $ ”=” exprˆ $ : madtomem expr -¿ termˆ $ : pass as var ṙeg term -¿ factˆ $ : pass as var ṙeg fact -¿ ”(” exprˆ $ ”)” : pass as var ṙeg fact -¿ varˆ $ : loadreg as var ṙeg fact -¿ var ṙegˆ $ : pass as var ṙeg num ė -¿ ṡizeof type : return element ṡize ˝

!! !! DEFINIZIONI GENERALI SULL’INDICIZZAZIONE DEGLI ARRAY !! (a numero di indici qualsiasi) !! decl ṡingle i̇ndex -¿ constant i̇ntˆ a – /name = index˙ &a name -¿ integer ėxprˆ index : pass as integer v̇ar

write ṡmb i̇ndex name,a

num ė -¿ ṡizeof name : return a

/return a ˝

decl i̇ndex -¿ decl ṡingle i̇ndexˆ a :pass

decl i̇ndex -¿ decl i̇ndexˆ a ”,” decl ṡingle i̇ndexˆ b – /name = ”index˙ ”&a&˙ &b

/name0= ”index˙ ”&a /name1= ”index˙ ”&b /asize = ṡizeof name0 /totsize = b* asize /fix b name -¿ name0ˆ first ”,” integer ėxprˆ second –

rreturn first*b + second ˝ write ṡmb i̇ndex name, name0, name1 num ė -¿ ṡizeof name : return totsize /return a&˙ &b

˝

stat -¿ rreturn integer ėxprˆ a : z ṙeturn(a,integer v̇ar)

!! !! DEFINIZIONE DEI RECORD !! record ḋ -¿ identˆ name – string ė -¿ current ṙecord : return name /campi ṙecord:=[] /dati ċampi:=[] num ė -¿ local ȧddress : return 0 /return name ˝

record l̇ -¿ eol record l̇ -¿ record l̇ˆ $ record ḋeclˆ a eol

stat -¿ record record ḋˆ recordname record l̇ˆ $ end record– /tmp = local ȧddress typedef recordname (tmp) record write ṡmb ṫipi ṙecord l̇ist tmp ˝

Vediamo ora la definizione completa della sintassi relativa al tipo più semplice contenuto nella libreria QCD: il tipo spinor.

!! !! SPINOR !!

!! decl

typedef spinor (8)

!! displacement

spinor ḋisp -¿ ”[” integer ėxprˆ d ”]” – rreturn d * 2 ˝

complex v̇ar -¿ spinor v̇arˆ v ”.” spinor ḋispˆ i : ...

return dyn ṡubgloper(v,i,2) as complex v̇ar spinor ṙeg ḋisp -¿ ”[” intˆ d ”]” –/return d*2˝ complex v̇ar ṙeg -¿ spinor v̇ar ṙegˆ v ”.” spinor ṙeg ḋispˆ i : ...

return extract ḃundle(v,i,2) as complex v̇ar ṙeg

!! arit

/next rule prec UMINUS spinor ḟact -¿ ”-” spinor ḟactˆ a –

spinor temporary rret rret.[0] = - a.[0] rret.[1] = - a.[1] rret.[2] = - a.[2] rret.[3] = - a.[3]

rreturn rret ˝

spinor ḟact -¿ spinor ḟactˆ a ”˜ ” – spinor temporary rret1 rret1.[0] = a.[0]˜ rret1.[1] = a.[1]˜ rret1.[2] = a.[2]˜ rret1.[3] = a.[3]˜

rreturn rret1 ˝

spinor ėxpr -¿ spinor ėxprˆ a ”+” spinor ṫermˆ b – spinor temporary rret2 rret2.[0] = a.[0] + b.[0] rret2.[1] = a.[1] + b.[1] rret2.[2] = a.[2] + b.[2] rret2.[3] = a.[3] + b.[3]

rreturn rret2 ˝

spinor ėxpr -¿ spinor ėxprˆ a ”-” spinor ṫermˆ b – spinor temporary rret3 rret3.[0] = a.[0] - b.[0] rret3.[1] = a.[1] - b.[1] rret3.[2] = a.[2] - b.[2] rret3.[3] = a.[3] - b.[3]

rreturn rret3 ˝

spinor ḟact -¿ dirac ṁatrixˆ m ”*” spinor ḟactˆ s – spinor temporary rret4 /c0=m.0;/c1=m.2;/c2=m.4;/c3=m.6 /e0=m.1;/e1=m.3;/e2=m.5;/e3=m.7 rret4.[0] = s.[c0] * e0 rret4.[1] = s.[c1] * e1 rret4.[2] = s.[c2] * e2 rret4.[3] = s.[c3] * e3 rreturn rret4

˝

spinor ḟact -¿ ”spin żero” – spinor temporary rret5 rret5.[0] = complex żero rret5.[1] = complex żero rret5.[2] = complex żero rret5.[3] = complex żero rreturn rret5 ˝

clear i̇tem -¿ spinor v̇arˆ s – s = spin żero ˝ clear i̇tem -¿ spinor v̇ar ṙegˆ s – s = spin żero ˝

!! !! spinor -¿ spinor * complex !! spinor -¿ spinor / complex !! spinor -¿ complex * spinor !!

spinor ṫerm -¿ spinor ṫermˆ s ”*” complex ḟactˆ c – spinor temporary tret tret.[0] = s.[0] * c tret.[1] = s.[1] * c tret.[2] = s.[2] * c tret.[3] = s.[3] * c

rreturn tret ˝

spinor ṫerm -¿ spinor ṫermˆ s ”/” complex ḟactˆ c – spinor temporary tret2 tret2.[0] = s.[0] / c tret2.[1] = s.[1] / c tret2.[2] = s.[2] / c tret2.[3] = s.[3] / c

rreturn tret2 ˝

spinor ṫerm -¿ complex ṫermˆ c ”*” spinor ḟactˆ s – spinor temporary tret1 tret1.[0] = s.[0] * c tret1.[1] = s.[1] * c tret1.[2] = s.[2] * c tret1.[3] = s.[3] * c

rreturn tret1 ˝

!! !! complex -¿ spinor * spinor !! complex ṫerm -¿ spinor ṫermˆ a ”*” spinor ḟactˆ b –

complex temporary ret

ret = a.[0] * b.[0] + a.[1] * b.[1] + a.[2] * b.[2] + a.[3] * b.[3] rreturn ret ˝

!! !! MATRICI DI DIRAC !! modulo un segno inessenziale Cgamma5== gamma31 !! Cgamma1== gamma35 !!

dirac ṁatrix -¿ ”gamma0” – /return [0,complex u̇no,1,complex u̇no,2,complex ṁeno u̇no,3,complex ṁeno u̇no]˝ dirac ṁatrix -¿ ”gamma1” – /return [3,complex i̇,2,complex i̇,1,complex ṁeno i̇,0,complex ṁeno i̇]˝ dirac ṁatrix -¿ ”gamma2” – /return [3,complex u̇no,2,complex ṁeno u̇no,1,complex ṁeno u̇no,0,complex u̇no]˝ dirac ṁatrix -¿ ”gamma3” – /return [2,complex i̇,3,complex ṁeno i̇,0,complex ṁeno i̇,1,complex i̇]˝ dirac ṁatrix -¿ ”gamma5” – /return [2,complex u̇no,3,complex u̇no,0,complex u̇no,1,complex u̇no]˝ dirac ṁatrix -¿ ”gamma05” – /return [2,complex u̇no,3,complex u̇no,0,complex ṁeno u̇no,1,complex ṁeno u̇no]˝ dirac ṁatrix -¿ ”gamma15” – /return [1,complex i̇,0,complex i̇,3,complex ṁeno i̇,2,complex ṁeno i̇]˝ dirac ṁatrix -¿ ”gamma25” – /return [1,complex u̇no,0,complex ṁeno u̇no,3,complex ṁeno u̇no,2,complex u̇no]˝ dirac ṁatrix -¿ ”gamma35” – /return [0,complex i̇,1,complex ṁeno i̇,2,complex ṁeno i̇,3,complex i̇]˝ dirac ṁatrix -¿ ”gamma23” – /return [1,complex i̇,0,complex i̇,3,complex i̇,2,complex i̇]˝ dirac ṁatrix -¿ ”gamma31” – /return [1,complex u̇no,0,complex ṁeno u̇no,3,complex u̇no,2,complex ṁeno u̇no]˝ dirac ṁatrix -¿ ”gamma12” – /return [0,complex i̇,1,complex ṁeno i̇,2,complex i̇,3,complex ṁeno i̇]˝ dirac ṁatrix -¿ ”gamma01” – /return [3,complex i̇,2,complex i̇,1,complex i̇,0,complex i̇]˝ dirac ṁatrix -¿ ”gamma02” – /return [3,complex u̇no,2,complex ṁeno u̇no,1,complex u̇no,0,complex ṁeno u̇no]˝ dirac ṁatrix -¿ ”gamma03” – /return [2,complex i̇,3,complex ṁeno i̇,0,complex i̇,1,complex ṁeno i̇]˝ dirac ṁatrix -¿ ”gammaI” – /return [0,complex u̇no,1,complex u̇no,2,complex u̇no,3,complex u̇no]˝

stat -¿ ”#diracdef” identˆ dirac ṅame dirac ṙowˆ r0 dirac ṙowˆ r1 ...

dirac ṙowˆ r2 dirac ṙowˆ r3 –

dirac ṁatrix -¿ dirac ṅame – /return [r0.0,r0.1,r1.0,r1.1,r2.0,r2.1,r3.0,r3.1] ˝

˝ dirac ṙow -¿ dirac i̇ˆ a dirac żˆ $ dirac żˆ $ dirac żˆ $ – /return [0,a] ˝ dirac ṙow -¿ dirac żˆ $ dirac i̇ˆ a dirac żˆ $ dirac żˆ $ – /return [1,a] ˝ dirac ṙow -¿ dirac żˆ $ dirac żˆ $ dirac i̇ˆ a dirac żˆ $ – /return [2,a] ˝ dirac ṙow -¿ dirac żˆ $ dirac żˆ $ dirac żˆ $ dirac i̇ˆ a – /return [3,a] ˝ dirac ż -¿ ”0” dirac ż -¿ ”““” EOL ”0” dirac i̇ -¿ ”1” – /return complex u̇no ˝ dirac i̇ -¿ ”i” – /return complex i̇ ˝ dirac i̇ -¿ ”-” ”1” – /return complex ṁeno u̇no ˝ dirac i̇ -¿ ”-” ”i” – /return complex ṁeno i̇ ˝ dirac i̇ -¿ ”““” EOL ”1” – /return complex u̇no ˝ dirac i̇ -¿ ”““” EOL ”i” – /return complex i̇ ˝ dirac i̇ -¿ ”““” EOL ”-” ”1” – /return complex ṁeno u̇no ˝ dirac i̇ -¿ ”““” EOL ”-” ”i” – /return complex ṁeno i̇ ˝

Interessanti sono soprattutto le linee relative alla definizione degli operatori matrici di Dirac. Per curiosità vediamo anche come risultano le analoghe linee relative però al tipo colorspin.

!! !! dirac * colorspin !!

color ṡpin ḟact -¿ dirac ṁatrixˆ m ”*” color ṡpin ḟactˆ vcs – color ṡpin temporary rut /c0=m.0;/c1=m.2;/c2=m.4;/c3=m.6 /e0=m.1;/e1=m.3;/e2=m.5;/e3=m.7 rut.[0,0] = vcs.[0,c0] * e0 rut.[0,1] = vcs.[0,c1] * e1 rut.[0,2] = vcs.[0,c2] * e2 rut.[0,3] = vcs.[0,c3] * e3 rut.[1,0] = vcs.[1,c0] * e0 rut.[1,1] = vcs.[1,c1] * e1 rut.[1,2] = vcs.[1,c2] * e2 rut.[1,3] = vcs.[1,c3] * e3 rut.[2,0] = vcs.[2,c0] * e0 rut.[2,1] = vcs.[2,c1] * e1 rut.[2,2] = vcs.[2,c2] * e2 rut.[2,3] = vcs.[2,c3] * e3 rreturn rut ˝

Infine mostriamo le linee relative alla saturazione fra due tipi complessi, il tipo su3 con il tipo colorspin.

!! !! color ṡpin -¿ su3 * color ṡpin !!

color ṡpin ṫerm -¿ su3 ṫermˆ vsu3 ”*” color ṡpin ḟactˆ vcs – color ṡpin temporary ret ret.[0,0] = vsu3.[0,0] * vcs.[0,0] + vsu3.[0,1] * vcs.[1,0] + ...

vsu3.[0,2] * vcs.[2,0] ret.[1,0] = vsu3.[1,0] * vcs.[0,0] + vsu3.[1,1] * vcs.[1,0] + ...

vsu3.[1,2] * vcs.[2,0] ret.[2,0] = vsu3.[2,0] * vcs.[0,0] + vsu3.[2,1] * vcs.[1,0] + ...

vsu3.[2,2] * vcs.[2,0]

ret.[0,1] = vsu3.[0,0] * vcs.[0,1] + vsu3.[0,1] * vcs.[1,1] + ...

vsu3.[0,2] * vcs.[2,1] ret.[1,1] = vsu3.[1,0] * vcs.[0,1] + vsu3.[1,1] * vcs.[1,1] + ...

vsu3.[1,2] * vcs.[2,1] ret.[2,1] = vsu3.[2,0] * vcs.[0,1] + vsu3.[2,1] * vcs.[1,1] + ...

vsu3.[2,2] * vcs.[2,1]

ret.[0,2] = vsu3.[0,0] * vcs.[0,2] + vsu3.[0,1] * vcs.[1,2] + ...

vsu3.[0,2] * vcs.[2,2] ret.[1,2] = vsu3.[1,0] * vcs.[0,2] + vsu3.[1,1] * vcs.[1,2] + ...

vsu3.[1,2] * vcs.[2,2] ret.[2,2] = vsu3.[2,0] * vcs.[0,2] + vsu3.[2,1] * vcs.[1,2] + ...

vsu3.[2,2] * vcs.[2,2]

ret.[0,3] = vsu3.[0,0] * vcs.[0,3] + vsu3.[0,1] * vcs.[1,3] + ...

vsu3.[0,2] * vcs.[2,3] ret.[1,3] = vsu3.[1,0] * vcs.[0,3] + vsu3.[1,1] * vcs.[1,3] + ...

vsu3.[1,2] * vcs.[2,3] ret.[2,3] = vsu3.[2,0] * vcs.[0,3] + vsu3.[2,1] * vcs.[1,3] + ...

vsu3.[2,2] * vcs.[2,3] rreturn ret ˝

BIBLIOGRAFIA

[1] C. Itzykson, J.B. Zuber: Quantum Field Theory, McGraw-Hill, New York 1980.

[2] K.J. Wilson: Quarks: from paradox to myth, New Phenomena in Subnuclear Physics, ed. A. Zichichi, Erice 1975, 13.

[3] J. Kogut, Rev. Mod. Phys 51 (1979), 659.

[4] J. Kogut: A review of lattice gauge theory, Les Houches, 1982.

[5] F. Wegner, J. Math. Phys. 12 (1971), 2259.

[6] K.G. Wilson, Phys. Rev. D 14 (1974), 2455.

[7] A.M. Polyacov, Phys. Lett. B 59 (1975)a, 79.

[8] A.M. Polyacov, Phys. Lett. B 59 (1975)b, 82.

[9] Karsten, Smith, Nucl. Phys B 183 (1981), 103.

[10] J. Kogut, L. Susskind, Phys. Rev. D 11 (1975), 395.

[11] C.N. Yang, R.L. Mills, Phys. Rev. 96 (1954), 1605.

[12] T. Banks, J. Kogut, L. Susskind, Phys. Rev. D 13 (1976), 1043.

[13] H.B. Nielsen, M. Ninomiya, Phys. Lett. B 105, (1981), 219.

[14] S.D.Drell, M. Weinstein, S. Yankielowicz, Phys. Rev. D 14 (1976), 487.

[15] H. Hamber, G. Parisi, Phys. Rev. Lett. 47 (1981), 1792.

[16] L. Maiani, G. Martinelli, Phys. Lett. B 178 (1986),256.

[17] Ape collab., Nucl. Phys B 317 (1989), 509.

[18] Ape collab., Nucl. Phys B (Proc. Suppl.) 17 (1990), 431.

[19] Ape collab., Proceedings of Lattice Conference A (1992).

[20] N. Chomsky, G. A. Miller: Introduction to the formal analysis of natural languages, Handbook of mathematical psychology Vol. ii, 1963, 269.

[21] M. A. Harrison: Introduction to formal languages theory, Addison-Wesley, 1978.

[22] A. Salomaa: Formal languages, Academic Press, 1973.

[23] N. Chomsky: Formal properties of grammars, Handbook of mathematical psychology Vol. ii, 1963, 323.

[24] M.P. Schützenberger,N. Chomsky: The algebraic theory of context-free languages, Computer programming and formal systems, Amsterdam 1963, 118.

[25] Y. Bar-Hillel, H. Perles, E. Shamir: On formal properties of simple phrase structure grammars, Zeitschrift für phonetik, sprachwissenschaft und kommunikationsforschung 14, 1961, 143.

[26] Gross, Lentin: Introduction to formal grammars, Springer-Verlag, Berlin 1970.

[27] N. Chomsky: On certain formal properties of grammars, Information and control 2 (a), 1959, 137.

[28] J. Katz, J. Fodor: The structure of a semantic theory, Readings in the philosophy of language, Prentice-Hall, New York 1963.

[29] C. E. Shannon, W. Weaver: The mathematical theory of communication, University of Illinois press, Urbana 1949.

[30] N. Chomsky: The algebraic theory of context-free languages, Studies in logic and the foundations of mathematics, 1963, 118.

[31] S. Ginsburg: The mathematical theory of context-free languages, McGraw-Hill, New York 1966.

[32] J. E. Hopcroft, J. D. Ullman: Formal languages and their relation to automata, Addison-Wesley, 1969.

[33] S. Ginsburg, H.G. Rice: Two families of languages related to ALGOL, J. assoc. computing machinery 10, 1962, 350.

[34] S. Cabasino, P.S. Paolucci, G.M. Todesco: Dynamic parsers and evolving grammars , SIGPLAN Notices, Vol. 27 No. 11, (1992).

[35] S. Cabasino, P.S. Paolucci, G.M. Todesco: ZzLanguage, Ape100 group documentation, “La Sapienza” Univ. Phys. Dep., Roma 1991.

[36] B. Burshteyn: On the modification of the formal grammar at parse time, SIGPLAN Notices, Vol. 25 No. 5, (1990), 117.

[37] H. Christiansen: A survey of adaptable grammars, SIGPLAN Notices, Vol. 25 No. 11, (1990), 35.

[38] B. Burshteyn: Generation and recognition of formal languages by modifiable grammars, SIGPLAN Notices, Vol. 25 No. 12, (1990), 45.

[39] J. Heering, P. Kint, J. Rekers: Incremental generation of parser, SIGPLAN Notices, Vol. 24 No. 7, (1989), 179.

[40] J.P. Tremblay, P.G. Sorenson: The theory and practice of Compiler Writing, McGraw-Hill, New York 1985.

INDICE

INTRODUZIONE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .1 1 LA TEORIA DI GAUGE QCD SU RETICOLO . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .3

1.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 3 1.2 Teorie di campo sul reticolo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4 1.3 Teorie di pura gauge sul reticolo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6 1.4 Formulazione funzionale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .9 1.5 Algoritmo di Metropolis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.6 L’algoritmo di Metropolis sul calcolatore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 10 1.7 Campi fermionici sul reticolo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 12 1.8 Species dubling - Fermioni di Wilson . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 13 1.9 QCD: teoria al continuo e formulazione funzionale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 14 1.10 L’algoritmo di inversione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .17 1.11 Il calcolo degli osservabili . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 20

2 TEORIA DEI LINGUAGGI FORMALI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 2.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 2.2 Vocabolario . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .22 2.3 Definizione di linguaggio formale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 22 2.4 Rappresentazione dei linguaggi formali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 23 2.5 Grammatica e sintassi dei linguaggi naturali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .24 2.6 Grammatiche formali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 26 2.7 Classificazione delle grammatiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 30 2.8 Grammatiche context-free . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .32 2.9 Semantica . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 33 2.10 Alberi sintattici e parsing . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 34 2.11 Grammatiche ambigue . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 38 2.12 Grammatiche ridotte . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 40 2.13 Azioni semantiche - Linguaggi di programmazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .43

3 LINGUAGGI EVOLVENTI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 3.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 3.2 Grammatiche evolventi ed evoluzioni grammaticali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 46 3.3 Definizione di linguaggio evolvente . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49 3.4 Proprieta’ delle grammatiche evolventi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51 3.5 Parser dinamici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56 3.6 Formulazioni alternative . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 56

4 IL LINGUAGGIO APESE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 4.1 Introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 60 4.2 L’APESE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .61 4.3 Notazioni - Formalismo di Backus-Naur . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 61 4.4 STD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .63 4.5 Dichiarazione di costanti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 4.6 Dichiarazione di variabili e array statici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 63 4.7 Dichiarazione di variabili register . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .64 4.8 Dichiarazione di variabili physical register . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 65 4.9 Operatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .65 4.10 Condizioni logiche ed espressioni booleane . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .66 4.11 Istruzioni di controllo del flusso globale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 68 4.12 Istruzioni di controllo del flusso locale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .68 4.13 SUB . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .69 4.14 Dichiarazione di subroutine e function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69 4.15 Invocazione di subroutine e function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 4.16 Definizione di subroutine e function . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 70 4.17 QCD . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 4.18 Tipo su3 - Dichiarazione di variabili e array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71 4.19 Operatori agenti sul tipo su3 . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .72 4.20 Tipo spinor . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .75 4.21 Tipi colorspin e spinspin . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 78

5 IL COMPILATORE PER L’APESE . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 5.1 Descrizione generale del compilatore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81 5.2 Lo ZzParser . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .81 5.3 Lo ZzLanguage . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 82 5.4 La generazione del codice e le action . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 85 5.5 TYPE.HZ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 86 5.6 STD.HZ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92 5.7 QCD.HZ . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 95

CONCLUSIONI . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 APPENDICE A . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105

A.1 Limite al continuo della azione di Wilson . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105 A.2 Algoritmo di Metropolis . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .106

APPENDICE B . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108

B.1 Dimostrazione della L(T_{CF}) ⊃ L(T_{REG}) . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .108 APPENDICE C . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

C.1 Routine semantiche . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109 C.2 Esempi di listati Zz . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .116

BIBLIOGRAFIA . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126