LINGUAGGI - COMPILATORI - INTERPRETI Per poter risolvere un dato problema utilizzando un computer è necessario che questo venga guidato da una serie di istruzioni che specificano, passo dopo passo, la sequenza di azioni che devono essere compiute. Esiste il problema di comunicare al computer gli algoritmi tramite i quali operare. Questo problema è risolto definendo un linguaggio che sia comprensibile sia all utente che al computer e nel quale vengano espressi gli algoritmi. LINGUAGGI Un linguaggio, naturale od artificiale, è un sistema di segni, di natura qualsiasi, creato per poter comunicare delle conoscenze. Per linguaggio artificiale si intende un linguaggio creato dall uomo, secondo precise regole convenzionali, per particolari finalità. I linguaggi naturali ed artificiali presentano una differenza fondamentale : l univocità del significato che in essi assumono le parole, le frasi e le altre costruzioni linguistiche. Infatti mentre il significato di una parola o di una frase in un linguaggio naturale varia anche sensibilmente, secondo il contesto del discorso, nei linguaggi artificiali ed in particolare in quelli di programmazione ogni parola o frase ha un significato assolutamente univoco. I linguaggi naturali ed artificiali presentano però un analogia : qualunque frase oltre ad essere costruita in modo corretto deve avere un significato. Come nel caso dei linguaggi naturali, anche per lo studio dei linguaggi di programmazione, occorre conoscere l insieme dei simboli che questi riconoscono come propri cioè l Alfabeto. Occorrerà anche stabilire come comporre tra loro i simboli dell alfabeto definendo sia le azioni che possono essere descritte dal linguaggio che le regole che sono necessarie per richiedere l esecuzione di tali azioni. E necessario allora affrontare lo studio dei linguaggi di programmazione secondo 2 direttrici precise : 1) lo studio delle regole che specificano le costruzioni valide (SINTASSI) 2) lo studio delle interpretazioni e del significato da dare ad ogni elemento del linguaggio (SEMANTICA) Un ALFABETO è un insieme finito di simboli; si definisce stringa una successione finita di simboli. Un LINGUAGGIO, da questo punto di vista, è un sottoinsieme, opportunamente definito, ricavato dall insieme di tutte le stringhe che possono essere generate a partire dall alfabeto. Ogni elemento di un linguaggio sarà detto PAROLA del linguaggio. Esisteranno sia stringhe che appartengono al linguaggio che stringhe che non vi appartengono. La SINTASSI del linguaggio è composta da una serie di regole per poter stabilire se una stringa appartiene o meno al Linguaggio. Invece la GRAMMATICA è l insieme delle regole che permettono di generare le stringhe di un linguaggio. Pertanto un generico linguaggio di programmazione deve possedere un alfabeto mediante il quale, tramite delle regole grammaticali, si costruiscono delle stringhe corrette; tramite la sintassi si può poi decidere quali stringhe grammaticalmente corrette appartengono al linguaggio ed infine, per mezzo dell analisi semantica si può assegnare un ben definito significato ad ogni stringa appartenente al linguaggio. Storia del linguaggi Fino alla fine degli anni 50 la programmazione era sostanzialmente un opera di minuziosa traduzione dei singoli blocchi di un algoritmo in una sequenza di cifre binarie, ottali, hex secondo un codice, LM, rispondente alle caratteristiche specifiche dell elaboratore che doveva eseguire il programma. Linguaggio macchina Con il termine programma in LM facciamo riferimento ad un algoritmo codificato in una forma direttamente interpretabile dalla CPU e residente in MC. Il LM è quindi una codifica binaria con cui richiedere alla CPU l esecuzione delle operazioni previste dai circuiti logici dell ALU, che dipende dalle particolarità tecniche hardware e perciò varia in base al tipo di computer. Ogni elaboratore è caratterizzato da un unico linguaggio macchina, stabilito all atto della sua costruzione. Ogni circuito logico realizza una determinata operazione sui dati binari. In fase di costruzione di un elaboratore elettronico viene determinata perciò la gamma e il tipo di operazioni eseguibili dalla macchina. Ad ogni singola operazione viene associato un codice binario stabilendo una corrispondenza biunivoca tra le combinazioni di gruppi di bit di lunghezza prefissata e i vari circuiti o i tipi di operazioni. La codifica avviene per mezzo di una lista di informazioni a lunghezza fissa dette istruzioni che occupano delle locazioni di memoria adiacenti in una stessa area, indicata come area di programma. In un altra zona, detta area dati, vengono posti i valori su cui opera il programma. Un elaboratore dispone di diverse classi di istruzioni con diversi tipi di formati. Con il termine formato si intendono le parti che compongono le istruzioni stesse e cioè : Codice operativo Indirizzo op1 Indirizzo op2 Indirizzo risul. 1
La lunghezza dei vari campi ( espressa in numero di bits ) varia da elaboratore ad elaboratore; significativa risulta essere quella del codice operativo dal cui valore si può risalire al massimo repertorio di istruzioni possibili n -> 2 n Per le classi di istruzioni vedere programma di III Esistevano delle difficoltà ad operare in LM : 1. enorme disparità tra tempo di programmazione e tempo di esecuzione 2. i programmi erano molto sensibili agli errori di difficile localizzazione 3. i programma non erano documentabili 4. lunghi tempi di apprendimento 5. difficoltà in quanto tutto ( variabili ed istruzioni ) dovevano essere scritte in termini di cifre binarie 6. difficoltà di prova di un programma 7. necessità di scomporre l algoritmo in passi elementari 8. nessuna portabilità : il programma in LM doveva essere ogni volta adattato alle caratteristiche del computer utilizzato, costringendo il programmatore alla consocenza dettagliata delle caratteristiche tecniche della macchina Fu presto evidente che i lunghi controlli necessari,per stilare correttamente un programma in LM erano in realtà dei processi meccanici e come tali potevano essere affidati ad un altro programma. In fatti il lavoro di assegnazione di indirizzi alle istruzioni ed ai dati è molto semplice e ripetitivo. Il primo stadio di sviluppo fu costituito dai cosiddetti Linguaggi Simbolici a Basso Livello i quali consentivano di specificare i dati e le istruzioni mediante dei simboli cioè dei codici mnemonici che sostituivano i codici operativi, gli indirizzi ed in dati espressi in forma binaria. Linguaggi Assembler I linguaggi assemblativi sono linguaggi simbolici a basso livello. Simbolici in quanto fanno uso di simboli : codici mnemonici al posto dei codici operativi (es. ADD al posto di 0010) e nomi di variabili al posto degli indirizzi di memoria (es. AREA al posto di 0ABE0). A basso livello in quanto esiste sempre una corrispondenza biunivoca con le istruzioni in LM. Ciò permette al programmatore di sfruttare al meglio le prestazioni hardware, offrendogli quindi la massima possibilità di ottimizzazione dei programmi in termini di tempo di esecuzione e occupazione di memoria. I programmi per contro sono rigidamente dipendenti dalle caratteristiche della macchina e non portabili su un altro sistema. I vantaggi che si ottengono nell utilizzare un linguaggio assemblativo, al posto di quello macchina, sono : la possibilità di usare dei simboli al posto dei codici operativi e degli indirizzi facilità nella scrittura e nella lettura dei programmi semplificazione nella prova e correzione di un programma Per contro i punti 7. e 8. degli svantaggi del LM risultano ancora persistenti. Infatti ogni computer ha un suo assembler ( nessuna portabilità ) e l algoritmo deve essere scomposto in passi elementari (corrispondenza uno a uno con LM). Descriviamo ora le caratteristiche comuni a tutti i linguaggi assemblativi in termini di formato : Label Codice operativo Operando Commento dove : label : è utilizzata per l identificazione dell istruzione Codice operativo : indica il tipo di operazione che deve essere svolta Operando : indica il valore oppure l indirizzo del dato che viene utilizzato nell operazione in forma simbolica Commento : facoltativo, descrive le funzioni dell istruzione Classi di istruzioni per l assembler : dichiarative logico-aritmetiche controllo controllo programma 2
input/output Dichiarative : usate per la descrizione di costanti, variabili e per la definizione di aree di memoria. In questo caso la label rappresenta il nome associato al dato. Il codice operativo specifica il tipo di operazione che si intende fare. Il campo operandi indicherà il valore associato oppure la grandezza dell area. Logico-aritmetiche, Input/output, controllo = LM Controllo programma : danno disposizioni sul modo in cui deve essere eseguito il programma. Vengono chiamate anche pseudoistruzioni perché all atto della traduzione non generano alcun codice oggetto. I programma scritti in forma simbolica devono però essere tradotti in una forma comprensibile alla macchina; questa funzione di traduzione è svolta da alcuni programmi che prendono il nome di Assemblatori. Essi generano un istruzione in codice macchina per ogni istruzione simbolica e inoltre hanno il compito di gestire gli indirizzi dei dati e delle istruzioni, assegnando ai diversi nomi simbolici definiti nel programma delle posizioni assolute in MC. La codifica viene fornita al computer attraverso un qualsiasi dispositivo di input e assegnata come dato di input al programma assemblatore, che produce come output un equivalente programma in LM o in un linguaggio intermedio. In generale, dato che nel campo operandi di un istruzione possono apparire simboli non ancora definiti, sono necessari 2 passi ( o scansioni) di elaborazione da parte dell assemblatore. La prima scansione determina l indirizzo di ogni simbolo, mentre la seconda genera le istruzioni in codice oggetto e gli appropriati indirizzi. E il caso delle forward reference, ovvero dei salti in avanti ad etichette non ancora definite. L assemblatore per funzionare utilizzerà una tabella dei codice macchina, una tabella delle pseudoistruzioni, e creerà una tavola dei simboli e il codice macchina. Ovviamente fra i compiti dell assemblatore vi sarà anche quello di effettuare un analisi lessicale e sintattica e segnalare opportuni errori ( in genere una lista prodotta su un tabulato di stampa) che non permetterà la traduzione in LM. E anche possibile avere tutto un gruppo di istruzioni in corrispondenza di una sola istruzione definita nel linguaggio simbolico; in tal caso si parla di macroistruzioni e di macroassemblatori. IL codice viene espanso e sostituito ogni qual volta nel sorgente viene richiamata la macro istruzione ( non è pertanto equivalente al concetto di sottoprogramma nei linguaggi evoluti). L assembler in conclusione semplifica notevolmente il lavoro al programmatore, consentendogli di lavorare in una forma più semplice e veloce e contemporaneamente preservando quelle caratteristiche di ottimizzazione delle risorse della macchina già presenti nel LM: Tuttavia persistono 2 problemi fondamentali ovvero la non portabilità e la necessità di scomporre l algoritmo in passi elementari. E per rispondere a queste esigenze che sono sorti i linguaggi evoluti o di terza generazione. I linguaggi evoluti si propongono, seppure in modo parziale, di risolvere i 2 problemi rimasti in sospeso. Sono infatti linguaggi in cui ogni frase assume un significato LOGICO-OPERATIVO, ovvero non c è più corrispondenza biunivoca con una singola istruzione in LM, ma ogni istruzione in L.E. ha un significato più ampio es. repeat..until). Inoltre il nucleo dei linguaggi evoluti risulta portabile. Problemi continuano a sussistere per quanto concerne le istruzioni di I/O (in quanto strettamente legate al Sistema Operativo) e le istruzioni aggiuntive appartenenti a particolari librerie e caratteristiche esclusivamente di particolari versioni del linguaggio. I vantaggi derivanti dall uso dei linguaggi evoluti possono essere così riassunti : tempo di apprendimento minore documentazione più accurata maggiore portabilità facilità di prova e correzione Ovviamente è necessario riportare le istruzioni del linguaggio evoluto in una forma comprensibile alla macchina. Tale funzione è svolta da quei programmi, appartenenti sempre alla categoria dei traduttori, che vengono indicati coi nomi Compilatori ed Interpreti. Un Compilatore esegue la traduzione di un programma scritto in L.E. ( source) in un programma scritto in codice macchina (object). Vengono analizzate tutte le istruzioni del source sulle quali viene effettuato un controllo lessicale, sintattico e semantico. Se non sono presenti errori, il compilatore effettuerà la traduzione in codice oggetto. La fase di compilazione risulta completamente separata da quella di esecuzione. Il codice oggetto prodotto verrà memorizzato e sarà a disposizione per successive esecuzioni. L interprete invece analizza ogni istruzione del source, effettuando controlli lessicali, sintattici e semantici e, prima del controllo dell istruzione successiva, viene eseguita l istruzione corrente. 3
L interprete presenta un nucleo principale che esegue la scansione del programma sorgente, frase per frase, e una "collezione di sottoprogrammi ognuno predisposto all esecuzione di un certo tipo di istruzione del linguaggio. Poiché nell interpretazione l eventuale errore riscontrato viene segnalato immediatamente, il programmatore può mettere a punto il proprio programma operando in modo conversazionale. Per tale ragione l interprete è sicuramente preferibile in fase di debugging e di stesura del programma. Tuttavia la natura del processo, che prende in considerazione una sola istruzione per volta, non si adata facilmente al caso di richiami a sottoprogrammi, strutture dati complesse, ecc. Riassumendo : la differenza principale tra i due processi risiede nel fatto che una volta che il programma in L.E. è stato compilato, questa operazione di traduzione non deve più essere fatta ogni volta che si desidera l esecuzione del programma. Al contrario, quando si esegue il processo di interpretazione, non rimane traccia delle singole righe tradotte dopo l esecuzione e quindi ogni volta che è richiesta l esecuzione del programma interpretato deve essere speso un tempo ulteriore per una nuova verifica sintattica e la conseguente interpretazione. SE, per contro, consideriamo il lavoro di ricerca dell errore, si può notare che compilando, anche per semplici errori di sintassi, deve essere eseguita una nuova compilazione completa del programma prima di poterne vedere l esecuzione effettiva. Da questo punto di vista il processo di interpretazione consente una maggiore elasticità, riducendo i tempi di debugging; infatti è possibile interrompere l esecuzione con opportuni comandi ed eseguire in modo interattivo la verifica sia della sintassi che del contenuto delle variabili. Il procedimento seguito dall interprete porta ad una maggiore lentezza nell esecuzione dei programmi rispetto a quelli compilati : per esempio le istruzioni contenute in un ciclo vengono analizzate e tradotte ogni volta che il ciclo viene ripetuto. E da notare inoltre che un programma in linguaggio interpretato, anche dopo alcune prove di esecuzione, non dà garanzia di correttezza sintattica in quanto vengono controllate soltanto le istruzioni effettivamente eseguite FASI DELLA COMPILAZIONE Il processo di compilazione consiste in una serie di passi successivi che in genere appaiono come un unico passo. Ognuno di essi produce un risultato intermedio che costituisce l input per il passo successivo. Le fasi logicamente distinte in cui può suddividersi un processo di compilazione possono essere così riassunte : 1. ANALISI LESSICALE (SCANNER): In questa fase vengono riconosciuti gli elementi base (o atomi) di un linguaggio. Può essere incontrato un elemento non assimilabile a nessun tipo previsto da parte dell analizzatore automatico (es. nome variabile costruito irregolarmente). IL programma viene analizzato secondo la seguente sequenza di operazioni : scansione sequenziale di tutte le istruzioni riconoscimento e separazione gli uni dagli altri degli elementi base del programma classificazione degli atomi come identificatori, costanti, ecc. Questa terza operazione nasce da un esigenza ben precisa : gli elementi che compaiono nelle frasi sono disomogenei tra loro e per poterli trattare in modo automatico devono essere resi facilmente riconoscibili. Questo scopo viene raggiunto attribuendo a ciascuno di essi un codice che ne definisce il tipo. Inoltre ogni elemento deve essere riconosciuto per distinguerlo da un altro dello stesso tipo ; viene pertanto attribuito un numero distintivo per ogni elemento. Alla fine della fase di analisi lessicale si formano una o più tabelle di descrittori degli elementi del linguaggio. Le parole chiavi del linguaggio sono già contenute in una tabella interna. 2. ANALISI SINTATTICA (PARSER) : in questa fase vengono riconosciute le strutture sintattiche elementari e segnalati eventuali errori. 3. ANALISI SEMANTICA : in questa fase viene interpretato il significato associato ad ogni struttura che può portare direttamente alla produzione del codice oggetto oppure ad una rappresentazione intermedia. 4. GENERAZIONE DEL CODICE : in questa fase viene prodotto il codice oggetto. Le prime tre fasi dipendono dal linguaggio mentre la quarta dipende dall elaboratore ospite e non gode sicuramente della caratteristica di portabilità. Svantaggi nell utilizzo dei linguaggi evoluti Uno degli svantaggi più sentiti è l inefficienza dei programmi prodotti. Il compilatore non è in grado di effettuare valutazioni, quali il livello d uso di una variabile o se essa, non essendo più necessaria, può essere eliminata. Inoltre non può far uso di esperienze precedenti per semplificare in modo intelligente i codici da eseguire. Il programma oggetto risultante è pertanto più lento e più lungo del necessario. Questo 4
è uno dei motivi per cui, in particolari ambienti dove il tempo di esecuzione di un programma è un elemento significativo, l assembler è ancora utilizzato. Molti compilatori prevedono l utilizzo di una successiva fase di ottimizzazione, proprio per risolvere almeno parzialmente questo problema. PARAMETRI DI STIMA DI UN LINGUAGGIO EVOLUTO portabilità versatilità ( ricchezza di strutture dati e di controllo ) leggibilità velocità di traduzione modularità ottimizzazione Classificazione Linguaggi FORTRAN (53-54) : indirizzato a problemi scientifici ALGOL (58) : è sorto come un linguaggio universale per la produzione di algoritmi COBOL (59) : finalizzato espressamente alle applicazioni commerciali e gestionali RPG (62) : è stato sviluppato in ambiente IBM allo scopo di facilitare la redazione di tabulati e tabelle BASIC ( 63-64) : linguaggio algoritmico di carattere generale, non indirizzato ad alcuna specifica applicazione, di facile apprendimento ed utilizzo PASCAL (68) : linguaggio di carattere generale che consente un alto livello di strutturazione sia degli algoritmi che dei dati ed offre la possibilità di definire tipo di dati diversi da quelli standard ( ambiente didattico ) C (74) : per la produzione di software di base in ambiente Unix ADA (79) : sorto per fra fornte alla necessità di un linguaggio ed uno standard di documentazione univoco Sono tutti linguaggi della 3 generazione : imperativi. Linguaggi della 4 generazione : Linguaggi per DB : nati per manipolare Basi di Dati (DBIV, ambiente Oracle, Paradox, ecc) Linguaggi della 5 generazione : LISP (58) : indirizzato alla manipolazione di espressioni simboliche e di dati in genere strutturati a liste dinamiche ( intelligenza artificiale e robotica) PROLOG (72) : ispirato alla logica formale : utilizzato soprattutto in ambienti di intelligenza artificiale Linguaggi Object oriented : Visual Basic, Visual C, Delphi, ecc Esempio Prolog : padre(giorgio,mario). Padre(giorgio,mario) Sono fatti fratello (a,b) : - padre(c,a) ^ padre (c,b) regola Si esegue scrivendo :? - fratello(mario,luca) La risposta all esecuzione sarà si o no. Per eseguire usa i fatti e le regole. 5