Sunteți pe pagina 1din 52

LIMBAJE FORMALE SI TRANSLATOARE 2017

CURS
LIMBAJE FORMALE ȘI TRANSLATOARE

1. PROBLEME GENERALE

1.1. Introducere

În cadrul acestui curs se vor prezenta urmatoarele aspecte:


 Limbaje de programare imperative:
o caracteristicile sintactice și semantice;
o modurile de specificare a sintaxei și semanticii;
o ariile problematice și ambiguitățile;
o puterea și utilitatea diferitelor caracteristici ale unui limbaj.
 Translatoare pentru limbajele de programare:
o diferitele clase de translatoare (asambloare, compilatoare, interpretoare); implementarea
translatoarelor.
 Generatoare de compilatoare:
o unelte disponibile pentru automatizarea construcției translatoarelor pentru limbajele de
programare.

Pentru implementarea studiilor de caz se va utiliza limbajul de programare C/C++ și în anumite părți
limbajul de asamblare I80X86.
De asemenea, algoritmii sunt prezentați în pseudo-cod, putând fi implementați apoi în orice limbaj de
programare.
În etapa de implementare, după analiza problemei și stabilirea algoritmului, acesta trebuie tradus
(implementat) într-un limbaj de programare.

Scrierea (editarea) programului sursă


Programele sursă sunt fişiere text care conţin instrucţiuni (cu sintactică și semantică proprii
limbajului utilizat).
Programul (fişierul) sursă este creat cu ajutorul unui editor de texte şi va fi salvat pe disc
(programele sursă C primesc, de obicei, extensia *.c, iar cele C++, extensia *.cpp).
Pentru a putea fi executat, programul sursă trebuie compilat şi linkeditat.

Compilarea
Procesul de compilare este realizat cu ajutorul compilatorului, care translatează codul sursă în cod
obiect (cod maşină), pentru ca programul să poată fi înţeles de calculator.
În cazul limbajului C, în prima fază a compilării este invocat preprocesorul. Acesta recunoaşte şi
analizează mai întâi o serie de instrucţiuni speciale, numite directive procesor. Verifică apoi codul sursă
pentru a constata dacă acesta respectă sintaxa12 şi semantica limbajului. Dacă există erori, acestea sunt
semnalate utilizatorului.
Utilizatorul trebuie să corecteze erorile (modificând programul sursă). Abia apoi codul sursă este
translatat în cod de asamblare, iar în final, în cod maşină, binar, propriu calculatorului. Acest cod binar

1
https://ro.wikipedia.org/wiki/Sintaxa_limbajului_C
2 https://ro.wikipedia.org/wiki/C_(limbaj_de_programare)

1
LIMBAJE FORMALE SI TRANSLATOARE 2017

este numit cod obiect şi de obicei este memorat într-un alt fişier, numit fişier obiect. Fişierul obiect va
avea, de obicei, acelaşi nume cu fişierul sursă şi extensia *.obj.

Linkeditarea
După ce programul sursă a fost translatat în program obiect, el este va fi supus operaţiei de
linkeditare. Scopul fazei de linkeditare este acela de a obţine o formă finală a programului, în vederea
execuţiei acestuia. Linkeditorul “leagă” modulele obiect, rezolvă referinţele către funcţiile externe și
rutinele din biblioteci și produce cod executabil, memorat într-un alt fișier, numit fişier executabil (acelaşi
nume, extensia .exe)

Execuţia
Lansarea în execuţie constă în încărcarea programului executabil în memorie şi startarea execuţiei
sale.

Cod sursă (Preprocesor) Cod obiect Linkeditor Cod


Compilator executabil

Figura 1.1. Etapele necesare obţinerii fişierului executabil

Observaţii:
Mediile de programare integrate (BORLANDC, TURBOC) înglobează editorul, compilatorul,
linkeditorul şi depanatorul (utilizat în situaţiile în care apar erori la execuţie);
Dacă nu se utilizează un mediu integrat, programatorul va apela în mod explicit (în linie de
comandă) un editor de texte, compilatorul, linkeditorul. Lansarea în execuţie se va face tot din linie de
comandă.

1.2. Programe sistem și translatoare


Utilizatorii sistemelor de calcul moderne se pot împărți în două categorii.
Prima categorie la care ne referim este cea a celor care nu-și dezvoltă niciodată propriile programe,
ci doar utilizează aplicațiile dezvoltate de alții.
Cealaltă categorie este a celor care sunt preocupați atât de dezvoltarea programelor cât și de
utilizarea lor ulterioară. Această ultimă categorie utilizează limbaje de nivel înalt pentru implementarea
algoritmilor, editoare interactive pentru introducerea și modificarea programelor și interfețe grafice pentru
controlul execuției acestora.
Programatorii care folosesc aceste unelte au o altă viziune asupra computerelor decât cei care nu
văd decat hardware-ul computerului, deoarece utilizarea compilatoarelor, editoarelor și a sistemelor de
operare – o clasă de unelte cunoscută în general sub numele de programe sistem – ia de pe umerii omului
povara dezvoltarii sistemelor la nivelul mașinii. Dar nu trebuie să se creeze falsa impresie că aceste unelte
îndepărteaza toate posibilitățile de eroare.
În istoria dezvoltarii software-ului, mare parte din programe au fost dezvoltate în limbaj mașina –
și o parte dintre acestea, unde este necesar, sunt și acuma. Cei care au experiența scrierii programelor în
limbaj mașină, ca o colecție de cifre binare sau hexazecimale, știu să aprecieze existența limbajelor de
nivel înalt, indiferent cât de complexe și complicate ar putea să fie caracteristicile acestora.

2
LIMBAJE FORMALE SI TRANSLATOARE 2017

Oricum, pentru ca limbajele de nivel înalt să fie utilizabile, trebuie să existe unelte care să
convertească programele scrise în aceste limbaje în șiruri de biți pe care îi înțelege mașina. Pe la
începuturile limbajelor de programare s-a observat că dacă se impun constrângeri asupra sintaxei unui
limbaj de nivel înalt, procesul de translație devine unul care poate fi automatizat.
Aceasta a condus la dezvoltarea de translatoare și compilatoare – programe care acceptă (ca
date) o reprezentare textuală a unui algoritm exprimat într-un limbaj sursă, și care produce (ca ieșire
primară) o reprezentare a aceluiași algoritm într-un alt limbaj, limbajul obiect sau țintă.
Incepătorii adesea nu fac distincție între faza de compilare și cea de execuție în cadrul dezvoltării
și utilizării programelor scrise în limbaje de nivel înalt. Aceasta este explicabilă prin faptul că translația
(compilarea) este adesea invizibilă, sau invocată cu o tastă funcțională specială din cadrul unui mediu de
dezvoltare integrat (IDE), care are și alte taste funcționale rapide. Mai mult, începătorii învață
programarea fără a se face în mod clar această distincție, adesea instrucțiunile limbajului fiind explicate în
modul: ”când un computer execută o instructiune read acesta citește date de la intrare într-o variabilă”.
Această explicație ascunde câteva operații de nivel jos. Implicațiile manipularii fișierelor, conversiei
caracterelor și alocării spațiului de stocare sunt ignorate, deși acestea sunt necesare pentru a programa
calculatorul să inteleaga cuvantul read. Oricine a încercat să programeze operații de I/O în limbaj de
asamblare știe că multe dintre acestea nu sunt prea simplu de implementat.
Un translator, fiind el însuși un program, trebuie să fie scris într-un limbaj de programare, cunoscut
ca limbajul gazdă sau de implementare. La ora actuală, rar se întâlnesc translatoare dezvoltate în limbaj
mașină de la zero. Evident primele translatoare au trebuit sa fie scrise în acest mod și la începerea
dezvoltării unui translator pentru orice sistem nou trebuie revenit la limbajul mașină și la arhitectura
mașinii pentru acel sistem. Chiar și așa, translatoarele pentru mașinile noi sunt dezvoltate totuși în limbaje
de nivel înalt, utilizând adesea tehnicile de cross-compilare3 și bootstrapping4, care vor fi prezentate mai
în detaliu în capitolele următoare.
Primele translatoare importante scrise se pare ca au fost compilatoarele Fortran dezvoltate de
echipa lui Backus la IBM în 1950, deși existau atunci și uneltele pentru dezvoltarea codului mașină.
Primul compilator Fortran a fost un software creat de un programator 18 ani. Este interesant de observat
ca una dintre primele preocupari a fost să dezvolte un sistem care să poată produce cod obiect, a cărui
eficiență de execuție să se poată compara cu cea a unui expert uman. Un proces de translație automat
poate rareori să produca cod la fel de optim ca cel scris de un programator bun în limbaj mașină, și din
aceste motive până în zilele noastre componente importante din cadrul sistemelor sunt adesea scrise în
limbaj mașină, pentru a economisi timp și spațiu.
Programele translator nu sunt niciodată complet portabile (deși anumite părți ale lor pot fi) și de
obicei depind de alte programe sistem pe care le are la dispoziție utilizatorul. În particular, operațiile de
I/O și manipularea fișierelor pe sistemele moderne sunt controlate de sistemul de operare. Acesta este un
program sau o suita de programe și rutine a caror rol este să controleze execuția altor programe pentru a
realiza cel mai bine alocarea resurselor, cum ar fi imprimante, plottere, disk-uri, utilizând adesea tehnici
sofisticate cum ar fi procesarea paralelă, multiprogramarea și altele.

3 A cross compiler is a compiler capable of creating executable code for a platform other than the one on which the compiler is
running. For example, a compiler that runs on a Windows 7 PC but generates code that runs on Android smartphone is a cross
compiler. https://en.wikipedia.org/wiki/Cross_compiler
4 In general parlance, bootstrapping usually refers to a self-starting process that is supposed to proceed without external input.
In computer technology the term (usually shortened to booting) usually refers to the process of loading the basic software into the
memory of a computer after power-on or general reset, especially the operating system which will then take care of loading other
software as needed.
The term appears to have originated in the early 19th century United States (particularly in the phrase "pull oneself over a fence
by one's bootstraps"), to mean an absurdly impossible action. https://en.wikipedia.org/wiki/Bootstrapping

3
LIMBAJE FORMALE SI TRANSLATOARE 2017

Mulți ani dezvoltarea sistemelor de operare a necesitat utilizarea limbajelor de programare care au
rămas mai aproape de nivelul codului mașina, decât a limbajelor potrivite pentru programe știintifice sau
comerciale. Mai recent s-au dezvoltat mai multe limbaj de nivel înalt de succes, având ca scop principal
utilizare în proiectarea de sisteme de operare și de control în timp-real. Cel mai bun exemplu de asemenea
limbaj este limbajul C, dezvoltat inițial pentru implementarea sistemului de operare UNIX și acum utilizat
pe scară largă în toate domeniile.

1.3. Relația dintre limbajele de nivel înalt și translatoare


Proiectarea și implementarea translatoarelor este un subiect care poate fi abordat din multe
unghiuri de abordare. Același lucru este valabil și pentru proiectarea limbajelor de programare.
Limbajele de programare sunt în general clasificate în 2 categorii:
 limbaje de nivel înalt (cum sunt Pascal, Fortran, C/C++ etc)
și
 limbaj de nivel jos (cum este limbajul de asamblare).
Limbajele de nivel înalt pot fi la rândul lor clasificate în limbaje:
o imperative (cum sunt cele menționate mai sus),
o funcționale (cum sunt Lisp, Scheme etc)
o logice (cum este Prolog).
Limbajele de nivel înalt se spune că au câteva avantaje față de cele de nivel jos, cum ar fi:
 Lizibilitate: Un limbaj de nivel înalt bun va permite scrierea programelor în așa fel încât să semene
cu o descrie apropiată de limba engleză a algoritmului implementat. Dacă se scrie cu grijă,
programarea se poate realiza în așa fel încât să fie bine documentat, o proprietate foarte de dorit
când se ia în considerare că multe programe care au fost scrise odată este posibil să fie studiate mai
târziu de multe alți programatori;
 Portabilitate: Limbajele de nivel înalt, fiind în esenșă independente de mașina pe care se
implementează, au pretenția că pot fi utilizate pentru a dezvolta software portabil. Acesta este
software care poate, în principiu (și uneori chiar și în practică), să ruleze nemodificat pe mai multe
mașini diferite – doar cu condiția să se recompileze codul sursă atunci când se transferă de pe o
mașină pe alta. Pentru a realiza independența față de mașină, limbajele de nivel înalt pot impiedica
accesul la caracteristicile de nivel jos și uneori sunt evitate de programatorii care trebuie să
dezvolte sisteme de nivel jos dependente de mașină. Oricum, unele limbaje, cum este și limbajul
C, au fost proiectate astfel încât să permită accesul la aceste caracteristici din cadrul construcțiilor
de nivel înalt.
 Structura și orientarea spre obiecte: Este universal acceptat că miscarea programării structurate
din anii 1960 și orientarea către programarea orientată spre obiecte din anii 1990 au condus la
îmbunătățiri majore în calitate și lizibilitatea codului. Limbajele de nivel înalt pot fi proiectate
astfel încât să incurajeze și chiar să îmbunătățească aceste paradigme ale programării .
 Generalitate: Majoritatea limbajelor de nivel înalt permit scrierea unei mari varietăți de programe,
nemaifiind nevoie ca programatorul să devina expert în multe limbaje diferite.
 Dimensiune redusă: Programele scrise în limbaje de nivel înalt sunt adesea considerate mai scurte
(în raport cu numărul liniile de instrucțiune) decât echivalentele lor în limbaje de nivel jos.

4
LIMBAJE FORMALE SI TRANSLATOARE 2017

 Verificarea erorilor: Fiind om, este normal ca programatorul să faca multe greșeli în procesul de
dezvoltare a unui program. Multe limbaje de nivel înalt – sau cel puțin implementarile lor – pot, și
adesea realizează, verificarea unui număr mare de erori atât în timpul compilarii cat și în timpul
execuției.

Aceste avantaje câteodata par supraapreciate și nerealiste.


De exemplu, lizibilitatea este de obicei legată de stilul programatorului cu experiență, și unii
incepatori sunt deziluzionați când descoperă cât de nenatural este de fapt un limbaj de nivel înalt.
De asemenea, generalitatea multor limbaje este doar în anumite arii destul de restrânse și
programatorii pot întâlni arii care sunt slab implementate (cum este manipularea șirurilor în Pascal-ul
standard). Explicația se regăsește în stransă legatură dintre dezvoltarea limbajelor de nivel înalt și a
translatoarelor acestora. Dacă examinăm limbajele de succes, se pot găsi numeroase exemple de
compromisuri, dictate de necesitatea de a implementa idei ale limbajului în mașini cu arhitecturi care nu
permit compromisuri sau chiar nepotrivite limbajului. De asemenea, compromisul este dictat și de
particularitțăile interfeței cu sistemele de operare de pe mașini. În sfarsit, unele caracteristici utile ale
limbajelor se dovedesc a fi fie imposibil de dificil de implementat, fie prea scumpe în termeni ai resurselor
mașinii necesare. S-ar putea sa nu fie imediat vizibil ca proiectarea limbajului Pascal (sau a câtorva dintre
succesorii săi, cum sunt Modula-2 și Oberon) a fost constrânsă parțial și de dorința de a-l face ușor de
compilat. Este un merit al proiectantului ca, în ciuda limitarilor introduse de aceasta constrangere,
Pascal-ul a devenit atât de popular, modelul pentru atât de multe alte limbaje și extensii, și a încurajat
dezvoltarea de compilatoare super-rapide, asa cum sunt cele din sistemele Borland Turbo Pascal și Delphi.
Proiectarea unui limbaj de programare necesită multe aptitudini și logică. Este evident ca limbajul
creat de cineva nu este util doar pentru a exprima ideile acelei persoane. Deoarece limbajul este utilizat și
pentru a formula și dezvolta idei, cunoștintele despre limbaj determină modul în care și, chiar, ce anume
poate gândi cineva. În cazul limbajelor de programare, au existat multe controverse. De exemplu, în
limbaje ca Fortran – pentru mult timp limbajul comunității științifice – algoritmii recursivi erau dificil de
utilizat (nu imposibil, doar dificil), având ca rezultat că multor programatori specializați în Fortran li se
pare recursivitatea ciudată și dificilă, chiar ceva ce trebuie evitat cu orice preț. Este adevarat că algoritmii
recursivi sunt uneori ineficienți și ca unele compilatoare pentru limbajele care permit recursivitatea pot
accentua acest lucru; pe de alta parte este adevarat și că unii algoritmi sunt explicati mai simplu în mod
recursiv decât într-un mod care depinde de repetări explicite (cele mai bune exemple sunt probabil cele
asociate cu manipularea arborilor).
Există două abordari diferite ale modului de proiectare a limbajelor de programare. Prima, impusă de
școala Wirth, spune ca limbajele trebuie să fie mici și ușor de înțeles și trebuie încercat să se înțeleagă ce
caracteristici importante ar putea fi omise fără a lua limbajului posibilitatea dezvoltării de sisteme.
Cealaltă abordare, îndrăgită de proiectanții de limbaje care doresc să mulțumească pe toată lumea, este
pentru un limbaj plin cu toate caracteristicile posibile și potențial utile. Școala Wirth a produs limbajele
Pascal, Modula-2 și Oberon, toate având un efect enorm asupra modului de gândire al programatorilor.
Cealaltâ abordare a produs Ada, C și C++, care sunt mult mai dificil de stăpânit complet și extrem de
complicate pentru a realiza o implementare corectă, dar care au un succes extraordinar pe piață.

Alte aspecte ale proiectarii limbajelor care contribuie la succesul acestora sunt:
 Ortogonalitate: Limbajele bune tind sa aiba un numar mic de caracteristici bine gandite care pot fi
combinate intr-un mod logic pentru a furniza blocuri de construire mai puternice. În mod ideal
aceste caracteristici nu trebuie sa influenteze intre ele și nu trebuie sa fie inconfurate de o gramada
de inconsistente, cazuri exceptionale și restrictii arbitrare.

5
LIMBAJE FORMALE SI TRANSLATOARE 2017

Majoritatea limbajelor au anumite bug-uri – de exemplu, în Pascal-ul original al lui Wirth o funcție
putea returna doar o valoare scalară, și nu una de orice tip structurat. Multe extensii la limbaje bine
puse la punct se dovedesc foarte valoroase datorită rectificarii unor asemenea neajunsuri.
 Notații familiare: Majoritate computerelor sunt binare. Oamenii, pentru că au zece degete cu care
să numere și să socoteasca, pot răsufla ușurați pentru că limbajele de nivel înalt de obicei
realizează calcule în aritmetica zecimală și utilizează notatii matematice standard pentru operații.
Când sunt propuse limbaje noi, acestea adesea iau forma unor limbaj derivate sau dialecte ale celor
bine puse la punct, astfel încât programatorii să fie tentați să migreze spre limbajele noi și să se
simtă totuși stăpâni pe acestea – acesta a fost calea strabatută în dezvoltarea limbajului C++ din C,
a limbajului Java din C++ etc.

Pe lângă aspectele de mai sus, un limbaj de nivel înalt modern trebuie să indeplineasca urmatoarele
criterii suplimentare:
 Definit clar: Trebuie să fie descris clar, atât pentru utilizator cât și pentru cel care scrie
compilatorul
 Translatat rapid: Trebuie să permită translatarea rapidă, astfel încât să nu fie depășit timpul de
dezvoltare a programului atunci când se utilizeaza limbajul.
 Modularitate: Este de dorit ca programele să poata fi dezvoltate în limbajul respectiv ca o colecție
de module compilate separat, cu mecanisme corespunzatoare pentru a asigura legătura între aceste
module.
 Eficiență: Trebuie să permită generarea de cod obiect eficient.
 Disponibilitatea: Trebuie să fie posibilă asigurarea de translatoare pentru toate mașinile importante
și pentru toate sistemele de operare importante.

Importanța unei descrieri sau specificări clare a limbajului nu poate fi mai accentuată. Aceasta trebuie
să se aplice, mai întâi, asupra sintaxei limbajului 0 adică, trebuie sa specifice în mod exact ce forma poate
lua un program sursa.
Trebuie să se aplice, apoi, asupra semanticii statice a limbajului – de exemplu, trebuie să fie clarificat
ce constrângeri trebuie să satisfacă entitatile de diferite tipuri utilizate, sau scopul pe care îl au diferiții
identificatori pe parcursul textului programului. În sfârșit, specificarea trebuie să se aplice și semanticii
dinamice a programelor care satisfac regulile sintactice și semantice statice – adică, trebuie să poată
prezice efectul pe care orice program scris în acel limbaj îl va avea când este executat.
Descrierea limbajelor de programare este extrem de dificil de realizat exact, în special dacă se încearcă
realizarea în limba vorbita, de exemplu engleza. Exista o tendință crescătoare de a utiliza formalismul,
care va fi prezentat mai tarziu. Metodele formale au avantajul preciziei, deoarece utilizează notații
matematice bine definite. Un dezavantaj al acestora este faptul ca nu sunt foarte concise. De exemplu,
descrierea informala a limbajului Modula-2 a fost de circa 35 de pagini, în timp ce descrierea formala
realizata de o comisie ISO are peste 700 de pagini.
Specificatiile formale au în plus avantajul ca, în principiu, și din ce în ce mai mult în practică, pot fi
utilizate pentru a ajuta la automatizarea implementarii translatoarelor pentru limbaj. Intradevar, tot mai rar
se gasesc compilatoare moderne care sa fie implementate fara ajutorul generatoarelor de compilatoare.
Acestea sunt programe care preiau ca marime de intrare o descriere formala a sintaxei și semanticii unui
limbaj de programare și produc la iesire parti mari dintr-un compilator pentru acel limbaj.
În capitolele urmatoare se vor prezenta modul de utilizare a unor asemenea generatoare de
compilatoare, dar și modul de realizare a unui compilator pornind de la zero.

6
LIMBAJE FORMALE SI TRANSLATOARE 2017

2. CLASIFICAREA ȘI STRUCTURA TRANSLATOARELOR

Un translator poate fi definit ca o functie a carui domeniu este un limbaj sursa și codomeniu este un limbaj
destinatie sau limbaj obiect.

Instructiuni in Instructiuni in
 Translator 
lim bajul sursa lim bajul destinatie

Cei care au putina experiente în translatoare stiu ca rareori se considera ca facand parte din functia
translatorului sa execute algoritmul implementat în sursa, ci doar sa modifice reprezentarea acestuia dintr-
o forma în alta. De fapt, în dezvoltarea translatoarelor sunt implicate cel putin trei limbaje: limbajul sursa
care va fi translatat, limbajul obiect sau destinatie care va fi generat și limbajul gazda care este utilizat
pentru implementarea translatorului. Daca translatia are loc în mai multe etape, ar putea exista și alte
limbaje intermediare. Majoritatea dintre acestea – limbajul gazda și chiar limbajele obiect – raman de
obicei invizibile utilizatorului limbajului sursa.

2.1. Diagrame T
O notatie utila pentru descrierea unui program de calculator, în particular un translator, utilizeaza
diagramele T. În fig.2.1. se prezinta cateva exemple.

Fig.2.1. Diagrame T: a) un program general; b) un translator general; c) un compilator Turbo Pascal


pentru sistemul MS-DOS

7
LIMBAJE FORMALE SI TRANSLATOARE 2017

În continuare în cadrul acestor diagrame vom folosi notatia „cod-M” pentru cod mașina. Translatia este
reprezentata printr-o sageata pe o mașina, avand programul sursa și programul obiect în partea stanga și
repsectiv dreapta, asa cum se vede în fig.2.2.

Fig.2.2. Un compilator Turbo Pascal pe o mașina 80X86

Putem privi aceasta combinatie ca fiind o mașina abstracta (denumita uneori și mașina virtuala), a carei
scop este sa converteasca programe sursa Turbo Pascal în echivalentul lor în cod mașina 80X86.
Diagramele T au fost introduse pentru prima data de Bratman în 1961 și imbunatatite ulterior de Earley și
Sturgis în 1970, iar de atunci sunt utilizate foarte des.

2.2. Clasificarea translatoarelor


Exista mai multe clase diferite de translatoare, dupa cum urmeaza:
 Asambloare: acest termen este de obicei asociat acelor translatoare care realizeaza maparea
instructiunilor unui limbaj de nivel jos în codul mașina care poate fi executat direct. Instructiunile
limbajului sursa de obicei sunt mapate una cate una cu instructiunile de la nivelul mașina.
 Macro-asamblor: este de asemenea asociat cu acele translatoare care mapeaza instructiunile unui
limbaj de nivel jos în cod mașina și este o variatie a asamblorului. Majoritatea instructiunilor
limbajului sursa sunt mapate una cate una în echivalentele în limbajul destinatie, dar unele
instructiuni macro se mapeaza intr-o secventa de instructiuni în cod mașina – furnizand efectiv o
facilitate de inlocuire text și astfel extinzand limbajul de asamblare pentru utilizator. (Nu trebuie
confundata cu utilizarea procedurilor sau a altor subprograme pentru extinderea limbajelor de nivel
înalt, deoarece metoda de implementarea este de obicei foarte diferita).
 Compilator: este asociat cu acele translatoare care mapeaza instructiunile unui limbaj de nivel
înalt în cod mașina care poate fi direct executat. Instructiunile limbajului sursa se mapeaza de
obicei în mai multe instructiuni în cod mașina.
 Pre-procesor: este asociat de obicei cu acele translatoare care mapeaza un superset al unui limbaj
de nivel înalt în limbajul de nivel înalt initial, sau care realizeaza substitutii simple de text inaintea
realizarii translatiei. Cel mai cunoscut pre-procesor este probabil cel care formeaza o parte
integranta a implementarilor limbajului C și care asigura multe dintre caracteristicile care
contribuie la impresia ca C este unicul limbaj intradevar portabil.
Translator de nivel înalt: este adesea asociat cu acele translatoare care mapeaza un limbaj de nivel înalt
intr-un alt limbaj de nivel înalt – de obicei unul pentru care exista deja compilatoare sofisticate pentru mai
multe tipuri de masini. Asemenea translatoare sunt în particular utile ca elemente componente ale unui
sisteme de compilare în doua etape, sau în asistarea tehnicilor bootstraping care vor fi prezentate în
curand.
 Decompilator și dezasamblor: se refera la translatoarele care incearca sa preia codul obiect la
nivel jos și sa genereze codul sursa la un nivel înalt. În timp ce aceasta se poate realiza destul de

8
LIMBAJE FORMALE SI TRANSLATOARE 2017

bine pentru producerea de cod la nivel de asamblare, este mult mai dificil atunci cand se incearca
refacerea codului sursa scris original intr-un limbaj de nivel înalt, cum ar fi Pascal sau C.

Multe translatoare genereaza cod pentru mașinile lor gazda. Acestea se numesc translatoare rezidente.
Altele, denumite cross-translatoare, genereaza cod pentru alte masini decat cea gazda. Cross-
translatoarele sunt adesea utilizate impreuna cu microcomputere, în special în sistemele embedded, care
pot fi ele insele prea mici pentru a permite operarea satisfacatoare a translatoarelor rezidente. Deșigur,
cross-translatia introduce probleme suplimentare legate de transferarea codului obiect de la mașina donor
la mașina care executa programul tradus, și pot conduce la intarzieri în dezvoltarea programelor și
depanare grea.
Iesirea unor translatoare este cod mașina, lasat incarcat la o locatie fixata intr-o mașina gata pentru
executie imediata. Alte translatoare, denumite translatoare incarca-și-lanseaza (load-and-go) pot chiar sa
lanseze executia acestui cod. Oricum, un mare numar de translatoare nu produc cod mașina de adresa
fixata. Acestea produc ceva asemanator și anume o forma semicompilata, simbolic binara sau
realocabila. O utilizare frecventa a acestora este în dezvoltarea de biblioteci de rutine speciale, care pot fi
formate din rutine scrise în mai multe limbaje sursa. Rutinele compilate în acest mod sunt legate printr-un
program denumit linker, care poate fi privit ca fiind cel care furnizeaza ultima etapa a unui translator
multi-etape. Limbajele care incurajeaza compilarea separata a partilor de program, cum este și limbajul
C++, depind în mod critic de existenta unor asemenea linker-e. Aceste sisteme sunt vitale pentru
dezvoltarea de proiecte software foarte mari. În schimb pentru programele mici pot parea o piedica,
deoarece trebuie avut grija de mai multe fisiere și se pierde timp pentru legarea tuturor componentelor
impreuna.
Se pot utiliza diagremele T pentru a arata interdependenta dintre translatoare, programele de incarcare etc.
De exemplu, sistemul FST Modula-2 utilizeaza un compilator și un linker, asa cum se prezinta în fig.2.3.

Fig.2.3. Compilarea și linkarea programelor Modula-2 pe un sistem FST

2.3. Fazele translatiei


Translatoarele sunt programe foarte complexe și este normal sa consideram ca procesul de translatie nu
are loc intr-un singur pas. În general acest proces este impartit intr-o serie de faze.

Exista o faza analitica, în care programul sursa este analizat pentru a determina daca satisface cerintele
sintactice și semantice impuse de limbaj. Aceasta este urmata de o faza sintetica în care este generat
codul obiect corespunzator în limbajul destinatie. Componentele translatorului care realizeaza aceste doua
faze majore se spune ca sunt cuprinse în front end și în back end al compilatorului. Partea front end este

9
LIMBAJE FORMALE SI TRANSLATOARE 2017

independenta de mașina destinatie, în timp ce partea back end depinde foarte mult de mașina destinatie. În
cadrul acestei structuri se pot recunoaste componente sau faze mai mici, ca în fig.2.4.

Fig.2.4. Structura și fazele unui compilator

Procesorul de caractere (Character handler) este sectiunea care comunica cu lumea exterioara, prin
sistemul de operare, pentru a citi caracterele care formeaza textul sursa. Deoarece seturile de caractere și
manipularea fisierelor difera de la un sistem la altul, aceasta faza este adesea dependenta de mașina sau de
sistemul de operare.
Analizorul lexical sau scanner-ul reprezinta sectiunea care grupeaza caracterele din textul sursa în
grupuri care formeaza din punct de vedere logic token-urile limbajului – simboluri cum sunt
identificatorii, sirurile, constantele numerice, cuvintele cheie (de ex.: while, if), operatorii (de ex. <=) etc.
Unele dintre aceste simboluri sunt reprezentate foarte simplu la iesirea scannerului, altele trebuie sa fie
asociate cu diverse proprietati cum ar fi numele sau valoarea lor.
Analiza lexicala este uneori usoara iar alteori este grea. De exemplu, instructiunea C++:

WHILE (A>3*B) A=A-1

Se decodifica usor în token-urile:

WHILE keyword
(
A identifier name A
> operator comparison
3 constant literal value 3
* operator multiplication

10
LIMBAJE FORMALE SI TRANSLATOARE 2017

B identifier name B
)
A identifier name A
= operator assignment
A identifier name A
- operator subtraction
1 constant literal value 1

Dar instructiunea Fortran:

10 DO 20 I = 1 . 30

este mai inselatoare. Cei familiari cu limbajul Fortran ar putea spune ca se decodifica în:

10 label
DO keyword
20 statement label
I INTEGER identifier
= assignment operator
1 INTEGER constant literal
, separator
30 INTEGER constant literal

în timp ce cei rautaciosi ar putea sa vada instructiunea asa cum este de fapt:

10 label
DO20I REAL identifier
= assignment operator
1 . 30 REAL constant literal

Trebuie cautat cu atentie pentru a distinge punctul de virgula asteptata. Spatiile sunt irelevante în Fortran.
Programatorii trebuie sa fie intradevar rautaciosi ca sa foloseasca identificatori avand spatii care nu sunt
necesare dar sunt sugestive. În timp ce limbajele ca C++, Basic etc au fost proiectate în asa fel încât
analiza lexicala sa fie separata în mod clar fata de restul analizei, acelasi lucru evident nu este adevarat
pentru limbajul Fortran și altele care nu au cuvinte cheie (keywords) rezervate.
Analizorul sintactic sau parser-ul grupeaza token-urile produse de scanner în structuri sintactice – lucru
pe care il face prin parsing expresii și instructiuni. (Acest proces este asemanator cu modul de analiza a
unei propozitii pentru a gasi componente ca „subiect”, „predicat” etc). Adesea parser-ul este combinat cu
analizorul de constrangeri contextuale, a carui rol este sa determine ca dintr-o structura sintactica toate
componentele satisfac reguli de scop și reguli de tip în contextul structurii analizate. De exemplu, în C++
sintaxa instructiunii WHILE este descrisa ca fiind:

WHILE (Expresion) Statement

Este normal sa consideram o instructiune de forma de mai sus cu orice tip de Expresie ca fiind sintactica
corecta, dar aceasta instructiune nu are nici o semnificatie reala decat daca valoarea Expresiei este

11
LIMBAJE FORMALE SI TRANSLATOARE 2017

constransa (în acest caz) la un tip Boolean. Nici un program nu are nici o semnificatie pana cand este
executat dinamic. Oricum, este posibil în cazul limbajelor puternic tipizate sa se prevada din perioada
compilarii daca programele sursa nu au nici o semnificatie (adica, din punct de vedere static, inainte de a
se incerca executarea programului în mod dinamic). Semantica este un termen utilizat pentru a descrie
„semnificatia” și astfel analizorul de constrangeri este adesea denumit analizor semantic static, sau pur și
simplu analizor semantic.
Iesirea fazelor corespunzatoare analizorului sintactic și analizorului semantic este cateodata exprimata în
forma unui arbore sintactic abstract (AST=abstract syntax tree) decorat. Aceasta este o reprezentare
foarte utila, pentru ca poate fi utilizata în mod ingenios pentru a optimiza generarea codului intr-o faza
ulterioara.
Pe cand sintaxa concreta a multor limbaje de programare incorporeaza multe cuvinte cheie și token-uri,
sintaxa abstracta este destul de simpla, retinand doar acele componente ale limbajului necesare pentru a
captura continutul real și (în ultima instanta) semnificatia programului. De exemplu, în timp ce sintaxa
concreta a unei instructiuni WHILE necesita prezenta cuvantului cheie WHILE, asa cum s-a prezentat mai
sus, componentele esentiale ale instructiunii WHILE sunt pur și simplu Expresia (booleana) și
instructiunea Statement.
Atfel, instructiunea C++:
WHILE (1<P && P<9) P=P+Q;
Se reprezinta prin arborele AST din fig.2.5.

Fig.2.5. Arborele AST pentru instructiunea WHILE (1<P && P<9) P=P+Q;

Un arbore sintactic abstract independent este lipsit de cateva detalii semantice; analizorul semnatic are
sarcina de a adauga „tip” și alte informatii contextuale în nosuri (de unde provine și denumirea de arbore
„decorat”).
Deșigur, se pot construi și arbori sintactici concreti. Instructiunea C++:
WHILE (1<P && P<9) P=P+Q;
Poate fi figurata în detaliu prin arborele din fig.2.6.

12
LIMBAJE FORMALE SI TRANSLATOARE 2017

Fig.2.6. Arborele sintactic concret pentru instructiunea WHILE (1<P && P<9) P=P+Q;

Toate fazele prezentate pana acum sunt de natura analitica. Cele care urmeaza sunt mai mult sintetice.
Prima dintre acestea ar putea fi un generator de cod intermediar, care, în practica, poate fi integrat și în
fazele anterioare sau chiar omis în cazul unor translatoare foarte simple. Acesta utilizeaza structurile de
date produse de fazele anterioare pentru a genera o forma a codului, poate chiar sub forma unor schelete
sau macrouri simple de cod, sau limbaj de asamblare sau chiar cod de nivel înalt pentru procesarea de
catre un asamblor extern sau de catre un compilator separat. Diferenta de baza dintre codul intermediar și
codul mașina propriu-zis este aceea ca în cazul codului intermediar nu trebuie specificate în detaliu
informatii de tipul registrii mașina exacti care se vor folosi, adresele exacte la care se face referire etc.
Instructiunea C++ considerata ca exemplu:
WHILE (1<P && P<9) P=P+Q;
Ar putea conduce la codul intermediar echivalent:
L0 if 1<P goto L1
goto L3
L1 if P<9 goto L2
goto L3
L2 P=P+Q
goto L0
L3 continue
Sau, ar putea produce ceva de genul:
L0 T1=1<P

13
LIMBAJE FORMALE SI TRANSLATOARE 2017

T2=P<9
if T1 and T2 goto L1
goto L2
L1 P=P+Q
goto L0
L2 continue

Totul depinde daca implementarea translatoarelor utilizeaza abordarea denumita conjunctie secventiala
sau scurt-circuit pentru a manipula expresiile Booleene compuse (ca în prmul caz) sau abordarea
operatorului Boolean (al doilea caz). Limbajul C++ necesita abordarea scurt-circuit. Oricum, exista
limbaje, cum ar fi limbajul Pascal, care nu specifica ce abordare sa se utilizeze.
Poante exista suplimentar și un optimizator de cod, pentru a imbunatatii codul intermediar în favoarea
vitezei sau spatiului sau ambelor. Utilizand acelasi exemplu ca mai sus, o optimizare evidenta ar conduce
la codul echivalent urmator:
L0 if 1>=P goto L1
If P>=9 goto L1
P=P+Q
goto L0
L1 continue

Cea mai importanta faza în partea back end revine ca responsabilitate generatorului de cod. În cadrul
unui compilator real, aceasta faza preia iesirea de la faza precedenta și produce codul obiect, luand decizii
în privinta locatiilor de memorie pentru date, generarea codului pentru accesarea acestor locatii, selectia
registrilor pentru calcule intermediare și indexari etc. În mod evident aceasta este o faza care necesita
multa pricepere și atentie la detalii, daca se doreste ca produsul finit sa fie eficient. Unele translatoare trec
la o alta faza prin incorporarea unui asa numit optimizator de control în cadrul caruia se incearca
reducerea operatiilor care nu sunt necesare prin examinarea mai în detaliu a unor secvente scurte din codul
generat.
În continuare este listat codul generat de diverse compilatoare pentru instructiunea analizata anterior. Este
evident ca fazele de generare a codului pentru aceste compilatoare sunt complet diferite. Asemenea

14
LIMBAJE FORMALE SI TRANSLATOARE 2017

diferente pot avea un efect profund asupra dimensiunii programului și a vitezei de executie

Un translator în mod inevitabil utilizeaza o structura complexa de date, denumita tabela de simboluri, în
care se memoreaza numele utilizate în program și proprietatile asociate acestora, cum ar fi tipul lui,
necesitatile de stocare (în cazul variabilelor) sau valorile lor (în cazul constantelor).
Asa cum bine se stie, utilizatorii limbajelor de nivel înalt pot face multe erori în procesul de dezvoltare a
programelor indiferent cat se simple ar fi acestea. Astfel diferitele faze ale unui compilator, în special
primele faze, comunica și cu un hadler de erori sau un raportor de erori, care este invocat la detectarea
erorilor. Este de dorit ca compilarea programelor cu erori sa fie continuata, daca este posibil, astfel încât
utilizatorul sa poata corecta mai multe erori din sursa inaintea recompilarii. Aceasta ridica unele aspecte
interesante privind proiectarea tehnicilor de redresare a erorilor și de corectie a erorilor. (Vorbim de
redresarea erorilor atunci cand procesul de translatie incearca sa continue dupa deterctarea unei erori și de
corectarea erorilor sau repararea erorilor atunci cand incearca sa corecteze eroarea din contexe – de obicei
un subiect controversat, deoarece corectia poate sa fie departe de ce a gandit initial programatorul).
Detectarea erorilor din codul sursa în timpul compilarii nu trebuie confundata cu detectarea erorilor în
timpul executiei, adica atunci cand se executa codul obiect. Multe generatoare de cod sunt responsabile de
adaugarea codului de verificare a erorilor la programul obiect (pentru a verifica, de exemplu, daca indexii
pentru tablouri sunt intre limitele impuse). Acest lucru poate fi destul de rudimentar, sau poate implica
adaugarea unei cantitati considerabile de cod și structuri de date pentru a fi utilizate cu sisteme de

15
LIMBAJE FORMALE SI TRANSLATOARE 2017

depanare sofisticate. Asemenea cod auxiliar poate reduce în mod drastic eficienta unui program și unele
compiltoare permit suprimarea acestei caracteristici.
Uneori greselile din program detectate în timpul compilarii sunt denumite erori, iar cele care apar în
timpul executiei sunt denumite exceptii, dar nu exista o terminologie universal valabila pentru acestea.
Analizand fig.2.4 s-ar parea ca compilatoarele lucreaza în mod serial și ca fiecare faza comunica cu
urmatoarea prin intermediul unui limbaj intermediar potrivit, dar în practica distinctia dintre diferitele faze
adesea devine insesizabila. Mai mult, multe compilatoare sunt de fapt construite în jurul unui parser
central ca și componenta dominanta, cu o structura mai asemanatoare cu cea din fig.2.7.

Fig.2.7. Structura unui compilator directionat spre parser

2.4. Translatoare multi-etape


Pe langa faptul ca sunt divizate conceptual în faze, translatoarele sunt adesea impartite în treceri, în
fiecare dintre acestea pot fi combinate sau intrepatrunse cateva faze. În mod traditional, o trecere citeste
programul sursa, sau iesirea de la o trecere precedenta, realizeaza unele transformari și apoi scrie iesirea
intr-un fisier intermediar, de unde poate fi rescanat intr-o trecere urmatoare.
Aceste treceri pot fi controlate de diferite parti integrate intr-un singur compilator, sau pot fi realizate prin
rularea a doua sau mai multe programe separate. Acestea pot comunica prin utilizarea propriilor forme
specializate de limbaje intermediare, pot comunica utilizand structuri de date interne (mai repede decat
fisiere) sau pot realiza cateva treceri asupra aceluiasi cod sursa original.
Numarul de treceri utilizate depinde de o varietate de factori. Anumite limbaje necesita cel putin doua
treceri de realizat daca se doreste generarea usoara a codului – de exemplu, acelea în care declaratiile de
identificatori pot aparea dupa prima referire la identificatorii respectivi, sau în cazul în care proprietatile
asociate unui identificator nu pot fi deduse imediat din contextul în care apar prima data. Adesea un
compilator multi-trecere poate economisi spatiu. Deși computerele moderne sunt de obicei dotate cu mult
mai multa memorie decat predecesoarele lor de acum doar cativa ani, totuși multi-trecerile pot fi un
element important daca se doreste translatarea limbajelor complicate în limitele unor sisteme mici.
Compilatoarele multi-trecere pot permite și o mai buna optimizare a codului, raportarea erorilor și tratarea
erorilor. În cele din urma, se preteaza dezvoltarii în echipa, cu diferiti membrii ai echipei realizand
diferitele treceri. Oricum, compilatoarele multi-trecere sunt de obicei mai lente decat cele intr-o singura
trecere și necesitatea de a lucra în acelasi timp cu mai multe fisiere le face destul de greu de scris și
utilizat. Compromisurile din etapa proiectarii adesea conduc la limbaje care sunt foarte potrivite pentru
compilarea intr-o singura trecere.

16
LIMBAJE FORMALE SI TRANSLATOARE 2017

În practica, se utilizeaza foarte mult translatoarele cu doua treceri în care prima etapa este un translator de
nivel înalt care converteste programul sursa în asamblare, sau chiar în alt limbaj de nivel relativ înalt
pentru care exista deja un translator bun. Procesul de compilare ar putea fi reprezentat atunci ca în fig.2.8
– exemplul prezinta un program Modula-3 pregatit pentru a fi rulat pe o mașina care are un convertor
Modula-3 în C:

Fig.2.8. Compilarea programului Modula-3 utilizand limbajul C ca limbaj intermediar

Este tot mai uzual sa se gaseasca compilatoare pentru limbaje de nivel înalt care au fost implementate în
limbajul C și care produc ele insele cod C la iesire. Succesul acestora se bazeaza pe premizele ca „toate
computerele moderne sunt echipate cu un compilator C” și „codul sursa scris în C este intradevar
portabil”. Niciuna dintre aceste premize, din pacat, nu este adevarata în intregime. Oricum, compilatoarele
scrise în acest mod sunt la fel de aproape de a-și realiza visul de portabilitate pe cat sunt și oricare dintre
cele existente în momentul de fata. Modul în care asemenea compilatoare pot fi utilizate este prezentat în
capitolul urmator.

2.5. Interpretoare, compilatoare interpretative și emulatoare


Compilatoarele de tipul discutat pana acum au cateva proprietati care pot sa nu fie prea evidente. În primul
rand, ele de obicei au ca obiectiv producerea de cod obiect care poate rula la viteza maxima pe mașina
destinatie. În al doilea rand, sunt de obicei realizate în asa fel încât sa compileze o intreaga sectiune de cod
inainte de ca acesta sa se poata executa.
În unele medii interactive sunt necesare sisteme care sa poata executa o parte din aplicatie fara a fi
necesara pregatirea intregii aplicatii, sau unele care sa permita utilizatorului sa modifice din mers cursul
actiunii. Scenariile tipice implica utilizarea de foi de calcul, de baze de date, de fisiere batch sau scripturi
shell pentru sistemele de operare. Cu asemenea sisteme poate fi posibil de modificat unele avantaje ale
vitezei de executie în favoarea obtinerii de rezultate la cerere.
Asemenea sisteme sunt construite în asa fel încât sa utilizeze un interpretor. Un interpretor este un
translator care efectiv accepta un program sursa și il executa direct fara, în aparenta, sa produca anterior
nici un cod obiect. Acest lucru se realizeaza prin preluarea programului sursa instructiune cu instructiune,
analizand fiecare instructiune pe rand și apoi executant instructiunile una cate una. În mod clar, o schema
ca aceasta, pentru a avea succes, impune unele constrangeri destul de severe asupra programului sursa.
Structurile complexe de program, cum sunt procedurile incuibarite sau instructiunile compuse nu se
preteaza prea bine unui asemenea tratament. Pe de alta parte, o interogare a unei linii dintr-o baza de date
sau simpla manipulare a unui rand sau a unei coloane dintr-o foaie de calcul pot fi realizate foarte bine.
Aceasta idee este dezvoltata un pic mai mult în cazul dezvoltarii unor translatoare pentru limbajele de
nivel înalt, cunoscute sub numele de compilatoare interpretative. Asemenea translatoare produc (ca
iesire) cod intermediar care este destul de simplu pentru a satisface constrangerile impuse de un
interpretor practic, chiar daca ar putea sa fie distanta mare fata de codul mașina al sistemului pe care se
doreste executia programului original. Decat sa se continue translatia la nivelul de cod mașina, o abordare
alternativa care ar putea sa lucreze destul de bine este sa se utilizeze codul intermediar ca parte a intrarii
unui interpretor scris special pentru acest scop. Acesta la randul sau executa algoritmul original, prin

17
LIMBAJE FORMALE SI TRANSLATOARE 2017

simularea unei masini virtuale pentru care codul intermediar este chiar codul sau mașina. Distinctia dintre
abordarile de executie cod mașina și pseudocod este prezentata pe scurt în fig.2.9.

Fig.2.9. Diferentele dintre compilatoarele cod-nativ și cele pseudo-cod

Putea reprezenta procesul utilizat în cadrul unui compilator interpretativ ruland sub MS-DOS pentru un
limbaj jucarie cum este Clang, care va fi prezentat în capitolele urmatoare, sub forma diagramelor T din
fig.2.10.

Fig.2.10. Un compilator interpretativ / interpretor pentru Clang

Nu este necesara limitarea interpretoarelor la lucrul doar cu iesiri intermediare ale unui translator. Mai
general, deșigur, chiar și o mașina reala poate fi privita ca un interpretor foarte specializat – unul care
executa instructiuni la nivel mașina prin preluarea, analiza și apoi interpretarea lor una cate una. În cadrul
unei masini reale acestea toate se realizeaza prin hardware și, deci, foarte rapid. Pe aceeasi idee, se poate
observa ca se poate scrie un program care sa permita ca o mașina reala sa emuleze orice alta mașina reala,
chiar daca poate mai lent, doar prin scrierea unui interpretor – sau, cum este mai des denumit, un
emulator – pentru a doua mașina.
De exemplu, am putea dezvolta un emulator care ruleaza pe mașina Sun SPARC și o face sa para un IBM
PC (sau invers). O data ce am realizat acest lucru, suntem (în principiu) în situatia de a executa pe o
mașina Sun SPARC orice software dezvoltat pentru un IBM PC – software-ul PC devine efectiv portabil.
Notatia prin diagrame-T este usor de extins pentru a lucra cu conceptul de asemenea masini virtuale. De
exemplu, rularea lui Turbo Pascal pe o mașina Sun SPARC poate fi prezentata ca în fig.2.11.

Fig.2.11. Executarea compilatorului Turbo Pascal pe un Sun SPARC


18
LIMBAJE FORMALE SI TRANSLATOARE 2017

Lucrul cu interpretoare/emulatoare este utilizat pe scara larga în proiectarea și dezvoltarea atat a noilor
masini, cat și a software-ului ce ruleaza pe acestea.
Utilizarea interpretoarelor poate avea cateva puncte favorabile, dupa cum urmeaza:
 Este mult mai usor sa se genereze cod mașina ipotetic (care poate fi prelucrat profilul limbajului
sursa original) decat cod mașina real (care trebuie sa aiba de-a face cu profilul fara compromisuri
al masinii reale).
 Un compilator scris pentru a produce (ca iesire) cod pseudo-mașina bine definit capabil de o
interpretare usoara pe mai multe masini poate fi facut foarte portabil, în special daca este scris intr-
un limbaj gazda care este disponibil pe scara larga (cum este ANSI C) sau chiar daca este
disponibil implementat deja în propriul sau pseudocod.
 Poate fi facut mai usor „prietenos cu utilizatorul” decat se poate prin abordarea codului nativ.
Deoarece interpretorul lucreaza mai aproape de codul sursa decat o face un program translatat
complet, se pot asocia direct acestei sursa mesaje de eroare sau alte ajutoare de depanare.
 O mare varietate de limbaje pot fi implementate destul de usor intr-o forma utila pe o mare
varietate de masini diferite. Aceasta se realizeaza prin producerea de cod intermediar la un
standard bine definit, pentru care ar trebui sa fie usor de implementat pe orice mașina reala un
interpretor destul de eficient.
 Se dovedeste a fi util în conexiune cu cross-translatoarele, asa cum s-a mentionat anterior. Codul
produs de asemenea translatoare poate fi testat cateodata mai bine prin simularea executiei pe o
mașina donor, decat dupa transferul pe mașina tinta – intarzierile inerente în cadrul transferului de
pe o mașina pe alta pot fi echilibrate prin degradarea timpului de executie intr-o simulare
interpretativa.
 În cele din urma, limbajele intermediare sunt adesea foarte compacte, permitand manipularea
programelor mari, chiar și pe masini relativ mici. Succesul masinilor UCSD Pascal și UCSD p-
System candva foarte utilizate reprezinta un exemplu privind ceea ce se poate realiza din acest
punct de vedere.

Pentru toate aceste avantaje, sistemele interpretative prezinta probleme privind timpul de executie,
deoarece executarea codului intermediar inseamna translatia virtuala în cod mașina de fiecare data cand se
realizeaza o instructiune mașina ipotetica.
Unul dintre primele cele mai cunoscute compilatoare interpretative portabile a fost cel dezvoltat la
Zurich și cunoscut sub numele de compilator Pascal-P. Acesta era furnizat intr-un kit cu trei componente:
 Prima componenta era forma sursa a unui compilator Pascal, scris intr-un subset foarte complet al
limbajului, cunoscut la Pascal-P. Scopul acestui compilator era sa translateze programele sursa
Pascal-P intr-un limbaj intermediar bine-definit și bine-documentat, cunoscut ca și cod-P, care era
„codul mașina” pentru un computer ipotetic bazat pe stiva, denumit mașina P.
 A doua componenta era o versiune compilata a primei – codurile P care erau produse de
compilatorul Pascal-P.
 Ultima componenta a kit-ului era un interpretor al limbajului P-cod, furnizat ca un algoritm Pascal.

Interpretorul a fost folosit în primul rand ca un model de scriere a unui program similar pentru mașina
tinta, pentru a-i permite sa emuleze mașina-P ipotetica. Asa cum se va vedea în continuare, emulatoarele
sunt destul de usor de dezvoltat – chiar în limbaj de asamblare, daca este necesar. O data incarcat
interpretorul – adica, versiunea sa potrivita pentru o mașina reala locala – intr-o mașina reala, se poate

19
LIMBAJE FORMALE SI TRANSLATOARE 2017

executa cod-P, și în particular codul-P al compilatorului P. Compilarea și executarea unui program


utilizator se poate realiza asa cum se prezinta în fig.2.12.

Fig.2.12. Compilarea și executarea unui program în compilatorul P


3. CONSTRUIREA COMPILATOARELOR ȘI TEHNOLOGIA BOOTSTRAPPING

Pana acum s-au prezentat cateva notiuni de baza privind translatoarele. Din acestea se poate observa ca
realizarea lor nu este prea usoara. Daca trebuie scris un translator complet pentru un limbaj sursa destul de
complex, sau un emulator pentru o noua mașina virtuala, sau un interpretor pentru un limbaj intermediar
de nivel jos, nu se va alege cu siguranta implementarea sa complet în limbaj de asamblare.
Din fericire rareori trebuie sa se realizeze implementarea completa în limbaj de asamblare. La ora actuala
sunt disponibile o mare varietate de translatoare, care sunt și bine documentate. O strategie destul de buna
atunci cand este necesar un translator pentru un limbaj vechi pe o mașina noua, sau pentru un limbaj nou
pe o mașina veche, sau chiar pentru un limbaj nou pe o mașina noua, este sa se utilizeze compilatoarele
existente pe ambele masini și sa se realizeze dezvolarea translatorului intr-un limbaj de nivel înalt. În acest
sens, în capitolul curent se prezinta cateva exemple.

3.1. Utilizarea unui limbaj gazda de nivel înalt


Daca, asa cum este tot mai frecvent, o mașina ideala M este dotata cu versiunea în cod mașina a unui
compilator pentru un limbaj bine stabilit, cum este limbajul C, atunci producerea unui compilator pentru
un limbaj ideal X este realizabila prin scrierea în C a unui nou compilator, pe care sa-l numim XtoM, și
compilarea sursei (XtoM.C) cu compilatorul C (CtoM.M) care ruleaza direct pe M (fig.3.1.). Acesta
produce versiunea obiect (XtoM.M) care poate fi apoi executata pe mașina M.

20
LIMBAJE FORMALE SI TRANSLATOARE 2017

Fig.3.1. Utilizarea limbajului C ca limbaj de implementare

Deși dezvoltarea în C este mult mai usoara decat dezvoltarea în cod mașina, procesul este totuși complex.
Asa cum s-a mentionar anterior, poate fi posibila dezvoltarea unei mari parti a sursei compilatorului
utilizand unelte de generare a compilatoarelor – presupunand, deșigur, ca acestea sunt disponibile fie în
forma executabila, fie ca sursa în C care poate fi compilata usor. Partea cea mai grea a dezvoltarii este
probabil cea asociata cu partea back end, deoarece aceasta este foarte dependenta de mașina. Daca exista
acces la codul sursa al unui compilator, ca CtoM, atunci se poate utiliza avantajos acesta. Deși
compilatoarele comerciale sunt rareori disponibile în format sursa, este disponibil codul sursa pentru
multe compilatoare produse în universitati sau ca elemente ale proiectului GNU realizat sub auspiciile
fundatiei Free Software.

3.2. Portarea unui translator de nivel înalt


Procesul de modificare a unui compilator existent pentru a lucra pe o mașina noua este adesea denumit
portarea compilatorului. În unele cazuri acest proces poate fi extrem de usor. Sa consideram, de exemplu,
scenariul destul de comun în care a fost implementat în C pe mașina A un compilator XtoC pentru un
limbaj popular X prin scrierea unui translator de nivel înalt pentru a converti în C programele scrise în X
și se doreste utilizarea limbajului X pe o mașina M ca, ca și A, are deja un compilator C propriu. Pentru a
construi un compilator în doua etape care sa fie utilizat pe ambele masini, tot ceea ce trebuie facut este, în
principiu, sa se instaleze codul sursa pentru XtoC pe mașina M și sa se recompileze.
O asemenea operatie este reprezentata în mod conventional cu diagrame T inlantuite. În fig.3.2.a se
prezinta compilarea compilatorului XtoC și în fig.3.2.b procesul de compilare în doua etape necesar
compilarii programelor scrise în X în cod-M.

Fig.3.2. Portarea și utilizarea unui translator de nivel înalt

Portabilitatea unui compilator ca XtoC.C este aproape garantata, cu conditia ca sa fie el insusi scris în
limbajul „portabil” C. Din pacate insa limbajul C nu este complet portabil. Uneori se pierde mult timp
pentru a modifica codul sursa al lui XtoC.C inainte de a fi acceptat ca intrare pentru CtoM.M, sperand ca
cei care au scris XtoC.C au utilizat doar C standard și directive pre-procesor care permit adaptarea usoara
la alte sisteme.

21
LIMBAJE FORMALE SI TRANSLATOARE 2017

Daca se doreste de la inceput scrierea unui compilator portabil pe alte sistem, acesta este scris astfel încât
sa produca la iesire cod de nivel înalt. Adesea, implementarea original a unui limbaj este scrisa ca un
translator auto-rezident cu scopul de a produce direct cod mașina pentru sistemul gazda curent.

3.3. Bootstrapping
Se pune intrebarea evidenta – cum a fost implementat primul limbaj de nivel înalt? În asamblare? Dar
atunci cum a fost produs asamblorul pentru limbajul de asamblare?
Un asamblor complet este el insusi o piese importanta de software, deși destul de simplu în comparatie cu
un compilator pentru un limbaj de nivel înalt. Este, oricum, destul de uzual sa se defineasca un limbaj ca
un subset al unui alt limbaj, astfel încât subsetul 1 este continut în subsetul 2 care la randul sau este
continut în subsetul 3 și asa mai departe, adica:
Subsetul 1 din ASM  Subsetul 2 din ASM  Subsetul 3 din ASM
Astfel, se poate scrie la inceput un asamblor în cod mașina pentru subsetul 1 al limbajului de asamblare,
poate pe principiul load-and-go (adica se scrie în asamblare și apoi se translateaza manual instructiune cu
instructiune în cod mașina). Acest program în subsetul limbajului de asamblare ar putea sa faca destul de
putine lucruri pe langa conversia mnemonicelor în forma binara corespunzatoare. Apoi se poate scrie în
subsetul 1 al limbajului de asamblare un asamblor pentru subsetul 2 al limbajului de asamblare și asa mai
departe.
Acest proces, prin care un limbaj simplu este utilizat pentru a translata un program mai complicat, care în
schimb poate lucra cu un program și mai complicat și asa mai departe, este cunoscut ca bootstrapping.

3.4. Compilatoare care se auto-compileaza


O data ce avem la dispozitie un sistem functional, se poate incepe utilizarea sa pentru a-l imbunatati.
Multe compilatoare pentru limbaje populare au fost scrise la inceput intr-un alt limbaj de implementare,
asa cum s-a arata în sectiunea 3.1, și apoi au fost rescrise în propriul limbaj sursa. Rescrierea furnizeaza
sursa pentru un compilator care poate fi apoi compilat cu compilatorul scris în limbajul de implementare
original. Aceasta este prezentat în fig.3.3.

Fig.3.3. Primul pas în dezvoltarea unui compilator care se auto-compileaza

În mod clar, scrierea unui compilator nu o data ci de doua ori este o operatie deloc usoara, daca limbajul
original de implementare nu este apropiat de limbajul sursa. Acest lucru este destul de uzual:
compilatoarele Oberon pot fi implementate în Modula-2; compilatoarele Modula-2, în schimb, au fost
implementate initial în Pascal (toate trei limbajele sunt destul de asemanatoare) și compilatoarele C++ au
fost implementate initial în C.
Dezvoltarea unui compilator care se autocompileaza are patru aspecte diferite în favoare. În primul rand,
constituie un test deloc usor de viabilitate al limbajului compilat. Al doilea, o data realizat, dezvoltarile
ulterioare pot fi facute fara a recurge la alte sisteme de translatoare. Al treilea, orice imbunatatiri care pot
fi facute la partea sa back-end se manifesta atat ca imbunatatiri ale codului obiect produs pentru programe
22
LIMBAJE FORMALE SI TRANSLATOARE 2017

generale cat și ca imbunatatiri ale compilatorului insusi. În ultimul rand, asigura verificarea destul de
exhaustiva a consistentei sale, deoarece daca acest compilator este utilizat pentru a-și compila propriul cod
sursa, ar trebuie, cu siguranta, sa fie capabil sa-și reproduca propriul cod obiect (fig.3.4).

Fig.3.4. Un compilator care se autocompileaza trebuie sa fie autoconsistent

Mai mult, fiind dat un compilator functional pentru un limbaj de nivel înalt este apoi foarte usor sa se
produca compilatoare pentru dialecte specializate ale acelui limbaj.

3.5. Jumatate de bootstrap


Compilatoarele scrise pentru a produce cod obiect pentru o mașina particulara nu sunt intrinsec portabile.
Oricum, ele sunt adesea utilizate pentru a asista operatia de portare. De exemplu, în momentul în care a
fost necesat primul compilator Pascal pentru mașinile ICL, existau doua forme ale compilatorului Pascal
disponibil în Zurich (unde a fost implementat pentru prima data limbajul Pascal pe structurile CDC).

Fig.3.5. Doua versiuni ale compilatorului Pascal original din Zurich

Primul stadiu al procesului de translatie implica modificarea lui PasToCDC.Pas pentru a genera cod
mașina ICL – producand astfel un cross compilator. Deoarece PasToCDC.Pas a fost scris intr-un limbaj
de nivel înalt, aceasta modificare nu a fost greu de realizat, și a rezultat astfel compilatorul PasToICL.Pas.
Deșigur acest compilator nu putea inca sa ruleze chiar pe orice mașina. A fost la inceput compilat
utilizand PasToCDC.CDC pe mașina CDC (fig.3.6.a). Aceasta a furnizat un cross compilator care putea
rula pa masini CDC, dar inca nu pe masini ICL. O compilatie ulterioara a lui PasToICL.Pas, utilizand
cross compilatorul PasToICL.CDC pe mașina CDC, a produs rezultatul final, PasToICL.ICL (fig.3.6.b).

23
LIMBAJE FORMALE SI TRANSLATOARE 2017

Fig.3.6. Producerea primului compilator ICL Pascal prin jumatate de bootstrap

Produsul final (PasToICL.ICL) a fost apoi copiat pe banda magnetica pe mașina ICL și incarcat destul de
usor. Avand obtinut un sistem functional, echipa ICL a putut continua dezvoltarea sistemului în limbajul
Pascal.
Aceasta operatie de portare a fost un exemplu de ceea ce se numeste sistem jumatate de boostrap. Munca
de a transporta este realizata de fapt în intregime pe mașina donor, fara a fi necesar nici un translator pe
mașina destinatie, dar o parte foarte importanta a compilatorului original (partea back end, sau generatorul
de cod) trebuie rescrisa în cadrul procesului. În mod evident metoda este riscanta – orice defect sau
omisiune în scrierea lui PasToICL.Pas ar putea duce la un dezastru. Asemenea probleme pot fi reduse prin
minimizarea modificarilor realizate asupra compilatorului original. O alta tehnica este sa se scrie un
emulator pentru mașina destinatie care sa ruleze pe mașina donor, astfel încât compilatorul final sa poata
fi testat pe mașina donor inainte de a fi transferat pe mașina destinatie.

3.6. Bootstrapping de la un compilator interpretativ portabil


Datorita dificultatilor evidente ale bootstrapping-ului pe jumatate în cazul portarii compilatoarelor, o
variatie a metodei boostrapping-ului complet descrisa pentru asambloare a fost utilizata cu succes în cazul
limbajului Pascal și a altor limbaje de nivel înalt. În acest caz cea mai mare parte a dezvoltarii se
realizeaza pe mașina destinatie, dupa ce s-a realizat destul de mare parte din munca preliminara pe mașina
donor pentru a produce un compilator interpretativ care este aproape portabil. Metoda este mai usor de
ilustrat pentru cazul kit-ului de implementare Pascal-P prezentat în sectiunea 2.5.

24
LIMBAJE FORMALE SI TRANSLATOARE 2017

Fig.3.7. Dezvoltarea unui compilator de cod nativ din compilatorul P

Utilizatorii acestui kit au inceput prin implementarea unui interpretor pentru mașina P. Procesul de
bootstrapping a fost apoi inceput prin dezvoltarea unui compilator (PasPtoM.PasP) care sa translateze
programele sursa Pascal-P în codul mașina local. Acest compilator poate fi scris în limbaj Pascal-P,
dezvoltarea ghidandu-se dupa sursa compilatorului Pascal-P în cod-P furnizat ca parte componenta a kit-
ului. Acest nou compilator a fost apoi compilat cu compilatorul interpretativ (PasPtoP.P) din kit
(fig.3.7.a) și sursa compilatorului Pascal în cod-M a fost apoi compilata cu acest nou compilator,
interpretat inca o data de mașina P, pentru a obtine produsul final, PasPtoM.M (fig.3.7.b).
Compilatorul interpretativ de cod-P Zurich poate, și a fost, utilizat ca un sisteme de dezvoltare foarte
portabil. A fost utilizat cu rezultate remarcabile la dezvoltarea sistemului USCD Pascal, care a fost prima
incercare serioasa de implementare a Pascal-ului pe microcalculatoare. Echipa USCD Pascal a mers mai
departe asigurand cadrul pentru un intreg sistem de operare, editoare și alte utilitare – toate scrise în
Pascal, și toate compilate în codul obiect bine definit cod-P. Prin simpla asigurare a unui intrepretor
alternativ se putea muta intregul sistem pe un nou microcalculator virtual nemodificat.

3.7. Asamblor de cod-P

25
LIMBAJE FORMALE SI TRANSLATOARE 2017

Exista, deșigur, și un alt mod în care un kit compilator interpretativ portabil poate fi utilizat. Se poate
incepe cu scrierea unui asamblor din cod-P în cod-M, probabil o sarcina relativ simpla. O data realizat
acest asamblor s-a obtinut produsul prezentat în fig.3.8.

Fig.3.8. Asamblor din cod-P în cod-M

Codurile-P pentru compilatorul de cod-P sunt apoi asamblate de acest sistem pentru a obtine un alt cross-
compilator (fig.3.9.a.) și acelasi asamblor cod-P/cod-M poate fi apoi utilizat ca parte back-end a cross-
compilatorului (fig.3.9.b).

Fig.3.9. Compilare și asamblare în doua treceri utilizand un compilator de cod-P

4. EMULAREA MASINILOR

În capitolul 2 s-a prezentat modul de utilizare a emulatoarelor sau interpretoarelor ca unelte de translatare
a limbajelor de programare. În acest capitol se vor prezenta în detaliu limbaje de masini ipotetice și modul
de emulare a masinilor ipotetice pentru aceste limbaje. Calculatoarele moderne sunt printre cele mai
complexe masini proiectate vreodata de inteligenta umana. Dar nu vor intra în detalii privind hardware-ul
calculatoarelor, ci ne vor rezuma la prezentarea unor limbaje obiect destul de primitive potrivite pentru
translatoare simple.

4.1. Arhitectura masinilor de calcul simple


Majoritatea unitatilor centrale CPU utilizate în calculatoarele moderne au unul sau mai multi registrii sau
acumulatori interni, care pot fi priviti ca memorie locala în care pot fi executate operatii aritmetice și
logice simple și intre care pot avea loc transferuri de date locale. Acesti registrii pot fi restrictionati la

26
LIMBAJE FORMALE SI TRANSLATOARE 2017

capacitatea unui singur octet (8 biti) sau, asa cum este în cazul majoritatii procesoarelor moderne, pot fi de
dimensiuni multipli mici ai octetilor sau cuvintelor.
Un registru intern fundamental este registrul de instructiuni (IR), prin care se trec siruri de biti (octeti)
reprezentand instructiuni fundamentale în cod mașina pe care procesorul le poate realiza. Aceste
instructiuni tind sa fie extrem de simple – gradul obisnuit de complexitate a acestor instructiuni fiind
operatii de genul „sterge un registru” sau „muta un octet dintr-un registru în altul”. Unele din aceste
instructiuni pot fi complet definite printr-un singur octet. Altele pot necesita doi sau mai multi octeti
pentru o definire completa. La aceste instructiuni multi-octet, primul octet defineste de obicei o operatie
și ceilalti reprezinta fie o valoare asupra careia se opereaza, fie adresa unei locatii de memorie în care se
gaseste valoarea cu care se opereaza.
Cele mai simple procesoare au doar cativa registrii de date care prezinta limitari privind operatiile care se
pot realiza cu continutul acestora și astfel procesoarele trebuie sa aiba posibilitatea de interfatare cu
memoria calculatorului și permit transferuri pe asa numitele linii de magistrala (bus) intre registrii interni
și numarul mult mai mare de locatii de memorie externa. Cand se transfera informatii la și de la memorie,
CPU depune pe magistrala de adrese informatiile corespunzatoare de adresa și apoi transmite sau
receptioneaza datele propriu-zise pe magistrala de date. Acest mecanism se prezinta în fig.4.1.

Fig.4.1. CPU este legat de memorie prin magistralele de adrese și date

Memoria poate fi vazuta în mod simplist ca o matrice unidimensionala de octeti, analog cu ceea ce poate
fi descris în limbajul de nivel înalt prin declaratii de genul:

typedef unsigned char BYTES;


BYTES Mem[MemSize]

Deoarece memoria este utilizata pentru a stoca nu numai date ci și instructiuni, un alt registru intern
important al unui procesor, asa numitul contor program sau pointer de instructiuni (notat prin PC sau
IP), este utilizat pentru a pastra adresa din memorie a urmatoarei instructiuni care trebuie adusa în
registrul de instructiuni (IR) al procesorului.
Poate ar fi de ajutor daca am gandi și procesorul în limbaj de nivel înalt:

struct processor {
BYTES IR;
BYTES R1, R2, R3;
unsigned PC;
};

27
LIMBAJE FORMALE SI TRANSLATOARE 2017

processor cpu;

Functionarea masinii consta în preluarea repetata a cate unui octet din memorie (pe magistrala de date),
plasarea acestuia în registrul IR și apoi executarea operatiei pe care o reprezinta acest octet. Instructiunile
multi-octet pot necesita preluarea altor octeti suplimentari inaintea decodificarii complete a instructiunii
de catre CPU. Dupa ce instructiunea reprezentata de continutul lui IR a fost executata, valoarea lui PC se
va modifica pentru a indica urmatoarea instructiune de preluat. Acest ciclu preluare-executie poate fi
descris de urmatorul algoritm:

BEGIN
CPU.PC=ValoareInitiala ; adresa primului cod de instructiune
LOOP
CPU.IR=Mem[CPU.PC] ; preluare
Increment(CPU.PC) ; trece la urmatorul cod
Execute(CPU.IR) ; poate afecta alti registrii, memorie, PC
END
END

Valoarea registrului PC se modifica în pasi mici (deoarece instructiunile sunt de obicei stocate în memorie
una dupa alta); dar executia unor instructiuni de ramificare poate avea un efect mai dramatic. La fel și în
cazul aparitiei unor intreruperi hardware, deși nu se va insista în continuare asupra lucrului cu intreruperi
hardware.
Un program pentru o asemenea mașina consta dintr-un lung sir de octeti. Aceste valori (în format binar,
zecimal sau hexazecimal) citite nu au nici o semnificatie pentru operatorul uman. Am putea, de exemplu,
avea o sectiune de program de forma:

25 45 21 34 34 30 45

Deși nu este deloc evident, aceasta succesiune de octeti ar putea fi echivalentul instructiunii:

ValFin=ValInit*2+34
Programarea la nivel mașina este de obicei realizata prin asocierea de mnemonici operatiilor realizate de
mașina, de exemplu: HLT pentru „halt” sau ADD pentru „aduna la registru”. Codul de mai sus este mult
mai usor de inteles daca este scris dupa cum urmeaza (cu comentarii):

LDA 45 ; incarca în acumulator valoarea din memorie de la adresa 45


SHL ; deplaseaza acumulatorul cu 1 bit spre stanga (inmulteste cu 2)
ADI 34 ; aduna 34 la acumulator
STA 45 ; stocheaza valoarea din acumulator în memorie la adresa 45

Programele scrise intr-un limbaj de asamblare – care mai intai trebuie asamblate inainte de a putea fi
executate – de obicei utilizeaza alte entitati cu nume, de exemplu:

ValInit EQU 34 ; CONST ValInit=34


LDA ValFin ; CPU.A=ValFin
SHL ; CPU.A=2*CPU.A

28
LIMBAJE FORMALE SI TRANSLATOARE 2017

ADI ValInit ; CPU.A=CPU.A+34


STA ValFin ; ValFin=CPU.A

Cand se utilizeaza fragmente de cod ca acesta pentru exemplificare vom realiza comentarii aratand
echivalentul fragmentului în limbaj de nivel înalt. Comentariile se scriu dupa caracterul „;”, ceea ce
reprezinta o conventie a limbajelor de asamblare.

4.2. Moduri de adresare


Asa cum arata și exemplele anterioare, programele scrise la nivel mașina adesea constau dintr-o secventa
de instructiuni simple, fiecare constand dintr-o operatie la nivel mașina și unul sau mai multi parametrii.
Un exemplu de o operatie simpla exprimata intr-un limbaj de nivel înalt ar putea fi:

Suma=Termen1+Termen2

Unele masini și limbaje de asamblare asigura asemenea operatii în termenii asa numitului cod cu trei
adrese, în care o operatie – exprimata printr-o mnemonica denumita de obicei opcode – este urmata de
doi operanzi și o destinatie. În general aceasta ia forma:

operatie destinatie, operand1, operand2

de exemplu:

ADD Suma, Termen1, Termen2

În general se poate exprima și ca un apel de functie de forma:

destinatie=operatie(operand1, operand2)

care ajuta la accentuarea ideii ca operand inseamna de fapt „o valoare”, în timp ce destinatie inseamna un
registru al procesorului sau o adresa de memorie în care se va stoca rezultatul.
În multe cazuri aceasta generalitate este restrictionata (adica, mașina sufera de neortogonalitate în
proiectare). În general este necesar ca valoarea unuia dintre operanzi sa fie valoarea aflata initial în
destinatie. Aceasta corespunde instructiunilor de nivel înalt de tipul:

Produs=Produs*Factor

și este exprimata la nivel jos prin asa numitul cod cu doua adrese de forma generala:

operatie destinatie, operand

de exemplu:

MUL Produs, Factor

Ca o curiozitate, merita evidentiata o conexiune evidenta intre unele operatii de atribuire în C++ și codul
cu doua adrese. În C++ atriburea de mai sus ar fi scrisa probabil sub forma:

29
LIMBAJE FORMALE SI TRANSLATOARE 2017

Produs*=Factor;

care este o sugestie pentru compilatorul C++ sa genereze cod de aceasta forma. (Poate acest exemplu ar
putea ajuta sa intelegem de ce C++ este privit de unii ca cel mai rafinat limbaj de asamblare.)
În cazul multor masini reale nici chiar codul cu doua adrese nu se regaseste la nivelul mașina. Una dintre
particulele destinatie sau operand ar putea fi restrictionata la un anumit registru mașina (celalalt ar putea fi
un registru mașina, o constanta sau o adresa mașina). Acesta este adesea numit cod cu o adresa și
jumatate și este exemplificat prin:

MOV R1, Valoare ; CPU.R1=Valoare


ADD Raspuns, R1 ; Raspuns=Raspuns+CPU.R1
MOV Rezultat, R2 ; Rezultat=CPU.R2

În final, în cazul masinilor acumulator s-ar putea sa fim restrictionati la cod cu o adresa, în care
destinatia este intotdeauna un registru mașina (cu exceptia acelor operatii care copiaza (stocheaza)
continutul unui registru mașina în memorie). În unele limbaje de asamblare asemenea instructiuni pot inca
parea de forma cu doua adrese, ca mai sus. Acestea pot fi scrise și în forme avand inclus implicit registrul
în mnemonica (opcode). De exemplu:

LDA Valoare ; CPU.A=Valoare


ADA Raspuns ; CPU.A=CPU.A+Raspuns
STB Rezultat ; Rezultat=CPU.B

Deși multe dintre aceste exemple ar putea lasa impresia ca operatiile corespunzatoare la nivel mașina
necesita mai multi octeti pentru reprezentare, acest lucru nu este în mod necesar adevarat. De exemplu,
operatiile care utilizeaza doar registrii mașina, exemplificate prin:

MOV R1, R2 ; CPU.R1=CPU.R2


LDA B ; CPU.A=CPU.B
TAX ; CPU.X=CPU.A

ar putea necesita un singur octet – asa cum ar fi evident intr-un limbaj de asamblare care utilizeaza a treia
reprezentare. Asamblarea unor asemenea programe este usurata considerabil printr-o notatie simpla și
autoconsistenta a codului sursa, un subiect ce va fi discutat în capitolele urmatoare.
În cazul acelor instructiuni care chiar presupun manipularea altor valori decat cele aflate doar în registrii
mașina, sunt necesare de obicei instructiuni multi-octet. Primul octet specifica de obicei chiar operatia (și
daca este posibil registrul sau registrii implicati), în timp ce ceilalti octeti specifica celelalte valori (sau
adresele de memorie ale celorlalte valori) implicate. În asemenea instructiuni exista cateva moduri de
utilizare a octetilor auxiliari. Aceasta varietate da nastere la ceea ce se numesc moduri de adresare
pentru procesor și a caror scop este sa asigure utilizarea unei adrese efective intr-o instructiune. Moduri
exacte disponibile variaza foarte mult de la un procesor la altul și de aceea vom prezenta în continuare
doar cateva exemple reprezentative. Diversele posibilitati pot fi distinse în unele limbaje de asamblare
prin utilizarea unor mnemonici diferite pentru ceea ce ar parea la o prima privire operatii aproape identice.
În alte limbaje de asamblare distinctia poate fi realizata de diferitele forme sintactice utilizate pentru a
specifica registrii, adresele sau valorile. Se pot gasi diferite limbaje de asamblare pentru acelasi procesor.

30
LIMBAJE FORMALE SI TRANSLATOARE 2017

La adresarea inerenta operandul este implicit în chiar codul operatiei și adesea instructiunea este
continuta intr-un singur octet. De exemplu, pentru a sterge un registru mașina denumit A am putea utiliza:

CLA sau CLR A ; CPU.A=0

Din nou subliniem ca, deși a doua forma pare a avea doua componente, nu inseamna intotdeuna ca se
utilizeaza doi octeti de cod la nivel mașina.

La adresarea imediata octetii auxiliari pentru o instructiune dau de obicei valoare reala care trebuie
combinata cu o valoare dintr-un registru. De exemplu:

ADI 34 sau ADD A,#34 ; CPU.A=CPU.A+34

În cazul acestor doua moduri de adresare utilizarea cuvantului „adresa” poate induce în eroare, deoarece
valoarea octetilor auxiliari adesea nu are nimic de-a face cu vreo adresa de memorie. În cazul modurilor
de adresare ce vor fi prezentate în continuare legatura cu adresele de memorie este mult mai evidenta.

La adresarea directa sau absoluta octetii auxiliari specifica de obicei adresa de memorie a valorii care
trebuie preluata sau combinata cu valoarea dintr-un registru, sau specifica unde se va stoca valoarea unui
registru. De exemplu:

LDA 34 sau MOV A,34 ; CPU.A=Mem(34)


STA 45 MOV 45,A ; Mem(45)=CPU.A
ADD 38 ADD A,38 ; CPU.A=CPU.A+Mem(38)
Incepatorii adesea fac confuzie intre adresarea imediata și adresarea directa, situatie care nu este deloc
ajutata de faptul ca nu exista consecventa în cadrul notatiilor diferitelor limbaje de asamblare și ar putea
chiar exista o varietate de moduri de exprimare a unui mod particular de adresare. De exemplu, pentru
procesoarele I80X86, codul de nivel jos este scris în forma cu doua adrese similar celei de mai sus – dar
modul de adresare imediata este realizat fara a utiliza un simbol special cum este #, în timp ca modul de
adresare directa are adresa scrisa intre paranteze:

ADD AX,34 ; CPU.AX=CPU.AX+34 Adresare imediata


MOV AX,[34] ; CPU.AX=Mem[34] Adresare directa

La adresare indexata prin registru unul dintre operanzii din instructiune specifica atat o adresa cat și un
registru index, a carui valoare în momentul executiei poate fi perceputa ca fiind indexul unui sir stocat
incepand cu aceea adresa:

LDX 34 sau MOV A,34[X] ; CPU.A=Mem[34+CPU.X]


STX 45 MOV 45[X],A ; Mem[45+CPU.X]=CPU.A
ADX 38 ADD A,38[X] ; CPU.A=CPU.A+Mem[38+CPU.X]

La adresarea indirecta prin registru unul dintre operanzii dintr-o instructiune specifica un registru a
carui valoare în momentul executiei da adresa efectiva unde se gaseste valoare operandului. Aceasta are
legatura cu conceptul de pointeri utilizati în limbajele de nivel înalt (C++). De exemplu:

31
LIMBAJE FORMALE SI TRANSLATOARE 2017

MOV R1,@R2 ; CPU.R1=Mem[CPU.R2]


MOV AX,[BX] ; CPU.AX=Mem[CPU.BX]

Nu toti registrii din cadrul unei masini pot fi folositi în aceste moduri. Unele masini au destul de multe
restrictii în aceasta privinta.
Unele procesoare permit variatii mari asupra modurilor de adresare indexata și indirecta. De exemplu, la
adresarea indexata cu memoria, un singur operand poate specifica doua adrese de memorie – prima da
adresa primului element al sirului și a doua da adresa unei variabile a carei valoare va fi utilizata ca index
al unui sir.

MOV R1,400[100] ; CPU.R1=Mem[400+Mem[100]]

Similar, la adresare indirecta cu memoria unul dintre operanzii dintr-o instructiune specifica o adresa de
memorie la care se va gasi o valoare care formeaza adresa efectiva la care se gaseste un alt operand.

MOV R1,@100 ; CPU.R1=Mem[Mem[100]]

Acest mod nu este la fel de raspandit ca celelalte; acolo unde apare corespunde direct utilizarii variabilelor
pointer în limbaje care permit acest lucru. De exemplu, codul urmator:

typedef int *ARROW;


ARROW Arrow;
int Target;
Target=*Arrow;

S-ar putea traduce în codul echivalent în limbaj de asamblare:

MOV AX,@Arrow
MOV Target, AX

Sau chiar:

MOV Target,@Arrow

unde, din nou, se poate observa o corespondenta imediata intre sintaxa în C++ și cea corespunzatoare în
asamblare.
În sfarsit, la adresare relativa un operand specifica o cantitate cu care registrul contor de program curent
PC trebuie sa se incrementeze sau decrementeze pentru a se gasi nou adresa. Aceasta adresare se gaseste
la instructiunile de ramificare și mai rar în cele care transfera date intre diversi registrii și/sau locatii de
memorie.

4.3. Studiul de caz 1 – o mașina cu un singur acumulator


Deși procesoarele moderne pot avea mai multi registrii, principiile lor de baza – în special cele care se
aplica în cazul emularii – pot fi ilustrate printr-un model al unui procesor cu un singur acumulator. În plus,
vom considera ca sistemul are toti registrii de numai un octet (8 biti).

32
LIMBAJE FORMALE SI TRANSLATOARE 2017

Arhitectura masinii
În fig.4.2. se prezinta schema bloc a acestei masini.

Fig.4.2. Un CPU simplu cu un singur acumulator

Simbolurile din schema bloc se refera la urmatoarele componente ale masinii:


ALU este unitatea aritmetica și logica, în care se realizeaza efectiv toate operatiile aritmetice și
logice.
A este acumulatorul de 8 biti, un registru pentru realizarea operatiilor aritmetice și logice.
SP este un pointer de stiva de 8 biti, un registru care indica o zona de memorie care poate fi
utilizata ca stiva.
X este un registru index de 8 biti, care este utilizat în zonele indexate de memorie care formeaza
siruri de date.
Z, P, C sunt flag-uri de conditie sau registrii de stare de 1 bit, care sunt setati pe „true” cand o
operatie conduce la modificarea unui registru în zero, intr-o valoare pozitiva sau se realizeaza un
transport (carry).
IR este registrul de instructiuni de 8 biti, în care se mentine valoarea octet a instructiuni în curs de
executie.
PC este contorul program de 8 biti, care contine adresa de memorie a instructiunii care urmeaza a
fi executata.
EAR este registrul adresei efective, care contine adresa octetului de date care este manipulat de
instructiunea curenta.

Programul model al acestui tip de mașina este destul de simplu – consta dintr-un numar de „variabile” (în
sensul C++), fiecare avand capacitatea de 1 octet. Unele dintre ele corespund registrilor procesorului, în
timp ce celelalte formeaza memoria RAM, care am presupus ca este de 256 de octeti, adresati de la 0 la
255. În aceasta memorie vor fi stocate atat datele cat și instructiunile programului în executie. Procesorul,
registrii sai și memoria RAM asociata pot fi descrisi prin urmatoarele declaratii în C++:

typedef unsigned char bytes;


struct processor {
bytes a, sp, x, ir, pc;
bool z, p, c;
};
33
LIMBAJE FORMALE SI TRANSLATOARE 2017

typedef enum { running, finished,


nodata, baddata, badop
} status;

processor cpu;
bytes mem[256];
status ps;

unde s-a introdus conceptul de stare a procesorului PS ca fiind o enumerare care defineste starile în care
se poate gasi un emulator.

Setul de instructiuni
Unele operatii ale masinii sunt descrise printrun singur octet. Altele necesita doi octeti și au formatul
urmator:

Byte1 Opcode
Byte2 Address field

Setul de functii cod mașina disponibile este destul de mic. Cele marcate cu * afecteaza flag-urile P și Z și
cele marcate cu + afecteaza flag-ul C. În continuare se prezinta o descriere informala (în original) a
semanticii acestora:
Mnemonic Hex Dec Function
opcode
NOP 00h 0 Nici o operatie (se poate folosi pentru a seta breakpoint în emulator)
CLA 01h 1 Sterge acumulatorul A
CLC + 02h 2 Sterge bitul carry C
CLX 03h 3 Sterge registrul index X
CMC + 04h 4 Complementeaza bitul carry C
INC * 05h 5 Incrementeaza acumulatorul A
DEC * 06h 6 Decrementeaza acumulatorul A
INX * 07h 7 Incrementeaza registrul index X
DEX * 08h 8 Decrementeaza registrul index X
TAX 09h 9 Transfera acumulatorul A în registrul index X
INI * 0Ah 10 Incarca acumulatorul A cu un intreg citit de la intrare în zecimal
INH * 0Bh 11 Incarca acumulatorul A cu un intreg citit de la intrare în hexazecimal
INB * 0Ch 12 Incarca acumulatorul A cu un intreg citit de la intrare în binar
INA * 0Dh 13 Incarca acumulatorul A cu valoarea ASCII citita de la intrare (caracter)
OTI 0Eh 14 Scrie valoarea acumulatorului A la iesire ca un numar zecimal cu semn
OTC 0Fh 15 Scrie valoarea acumulatorului A la iesire ca un numar zecimal fara semn
OTH 10h 16 Scrie valoarea acumulatorului A la iesire ca un numar hexa fara semn
OTB 11h 17 Scrie valoarea acumulatorului A la iesire ca un numar binar fara semn
OTA 12h 18 Scrie valoarea acumulatorului A la iesire ca un singur caracter
PSH 13h 19 Decrementeaza SP și depune valoarea acumulatorului A în stiva
POP * 14h 20 Extrage din stiva în acumulatorul A și incrementeaza SP
SHL + * 15h 21 Deplaseaza acumulatorul A un bit la stanga
34
LIMBAJE FORMALE SI TRANSLATOARE 2017

SHR + * 16h 22 Deplaseaza acumulatorul A un bit la dreapta


RET 17h 23 Revine din subrutina (adresa de revenire este extrasa din stiva)
HLT 18h 24 Opreste executia programului

Instructiunile de mai sus sunt toate de un singur octet. Urmatoarele sunt toate instructiuni de 2 octeti:
Mnemonic Hex Dec Function
opcode
LDA B * 19h 25 Incarca acumulatorul A direct cu continutul locatiei a carei adresa este
în B
LDX B * 1Ah 26 Incarca acumulatorul A cu continutul locatiei a carei adresa este în B,
indexata cu valoare din X (adresa calculata fiind B+X)
LDI B * 1Bh 27 Incarca acumulatorul A cu valoare imediata B
LSP B 1Ch 28 Incarca pointerul de stiva SP cu continutul locatiei de adresa data de B
LSI B 1Dh 29 Incarca pointerul de stiva SP cu valoare imediata B
STA B 1Eh 30 Stocheaza acumulatorul A în locatia a carei adresa este data de B
STX B 1Fh 31 Stocheaza acumulatorul A în locatia a carei adresa este data de B,
indexat cu valoarea lui X
ADD B + * 20h 32 Aduna la acumulatorul A continutul locatiei a carei adresa este data de B
ADX B + * 21h 33 Aduna la acumulatorul A continutul locatiei a carei adresa este data de
B, indexata cu valoarea lui X
ADI B + * 22h 34 Aduna valoarea imediata B la acumulatorul A
ADC B + * 23h 35 Aduna la acumulatorul A valoarea bitului carry C plus continutul
locatiei a carei adresa este data de B
ACX B + * 24h 36 Aduna la acumulatorul A valoarea bitului carry C plus continutul
locatiei a carei adresa este data de B, indexata cu valoarea lui X
ACI B + * 25h 37 Aduna valoarea imediata B + valoarea bitului carry C la acumulatorul A
SUB B + * 26h 38 Scade din acumulatorul A continutul locatiei de adresa data de B
SBX B + * 27h 39 Scade din acumulatorul A continutul locatiei de adresa data de B,
indexata cu valoare lui X
SBI B + * 28h 40 Scade valoarea imediata B din acumulatorul A
SBC B + * 29h 41 Scade din acumulatorul A valoare bitului carry C plus continutul locatiei
a carei adresa este data de B
SCX B + * 2Ah 42 Scade din acumulatorul A valoare bitului carry C plus continutul locatiei
a carei adresa este data de B, indexata cu valoare lui X
SCI B + * 2Bh 43 Scade valoarea imediata B plus valoarea bitului carry C din
acumulatorul A
CMP B + * 2Ch 44 Compara acumulatorul A cu continutul locatiei de adresa data de B
CPX B + * 2Dh 45 Compara acumulatorul A cu continutul locatiei de adresa data de B,
indexata cu valoare lui X
CPI B + * 2Eh 46 Compara acumulatorul A direct cu valoare lui B
ANA B + * 2Fh 47 AND la nivel de bit intre acumulatorul A și continutul locatiei de adresa
data de B
ANX B + * 30h 48 AND la nivel de bit intre acumulatorul A și continutul locatiei de adresa
data de B, indexata cu valoarea lui X
ANI B + * 31h 49 AND la nivel de bit intre acumulatorul A și valoarea imediata B

35
LIMBAJE FORMALE SI TRANSLATOARE 2017

ORA B + * 32h 50 OR la nivel de bit intre acumulatorul A și continutul locatiei de adresa


data de B
ORX B + * 33h 51 OR la nivel de bit intre acumulatorul A și continutul locatiei de adresa
data de B, indexata cu valoarea lui X
ORI B + * 34h 52 OR la nivel de bit intre acumulatorul A și valoarea imediata B
BRN B 35h 53 Ramificare la adresa data de B
BZE B 36h 54 Ramificare la adresa data de B daca flag-ul Z este setat
BNZ B 37h 55 Ramificare la adresa data de B daca flag-ul Z nu este setat
BPZ B 38h 56 Ramificare la adresa data de B daca flag-ul P este setat
BNG B 39h 57 Ramificare la adresa data de B daca flag-ul P nu este setat
BCC B 3Ah 58 Ramificare la adresa data de B daca flag-ul C este setat
BCS B 3Bh 59 Ramificare la adresa data de B daca flag-ul C nu este setat
JSR B 3Ch 60 Apel de subrutina a carei adresa este B, cu depunerea în stiva a adresei
de revenire
Instructiunile de comparatie se realizeaza prin scaderea virtuala a operandului din acumulatorul A și
setarea corespunzatoare a flag-urilor P și Z.
Majoritatea operatiilor listate mai sus aunt similare celor existente în cadrul masinilor reale. Exceptii
importante sunt în cazul operatiilor de I/O (intrare/iesire). Majoritatea masinilor reale au facilitati foarte
primitive de realizare directa a acestor operatii I/O, dar în continuare vom considera ca mașina propusa
dispune de cateva instructiuni de 1 octet foarte puternice pentru manipularea I/O.
O examinare atenta a masinii și a setului sau de instructiuni arata cateva caracteristici care sunt tipice
masinilor reale. Deși în acest caz exista 3 registrii de date A, X și SP, doi dintre acestia (X și SP) pot fi
utilizati doar în moduri speciale. De exemplu, este posibil sa se transfere o valoare din A în X, dar nu și
invers, și în timp ce este posibil sa se incarce o valoare în SP nu este posibil sa se citeasca aceasta valoare.
Operatiile logice afecteaza bitul carry (toate il reseteaza), dar, totuși, operatiile INC și DEC nu-l afecteaza.
În continuare vom construi un emulator pentru acest model de mașina.

Programul de proba
Se vor prezenta în continuare cateva exemple de programe scrise în limbajul de asamblare al masinii
considerate anterior pentru a intelege mai bine modul sau de functionare.
Sa consideram urmatoarea problema: se citeste un numar și apoi se numara cati biti 1 (nenuli) are acesta în
reprezentarea sa binara. Aceasta problema se rezolva conform programului urmator, în care se prezinta pe
langa instructiuni și reprezentarea hexazecimala a fiecarui octet și localizarea sa în memorie.

00 BEG ; Numara bitii dintr-un numar


00 0A INI ; Read(A)
01 LOOP ; REPEAT
01 16 SHR ; A=A DIV 2
02 3A 0D BCC EVEN ; IF A MOD 2 # 0 THEN
04 1E 13 STA TEMP ; TEMP=A
06 19 14 LDA BITS ;
08 05 INC ;
09 1E 14 STA BITS ; BITS=BITS+1
0B 19 13 LDA TEMP ; A=TEMP
0D 37 01 EVEN BNZ LOOP ; UNTIL A=0
0F 19 14 LDA BITS ;

36
LIMBAJE FORMALE SI TRANSLATOARE 2017

11 0E OTI ; Write (BITS)


12 18 HLT ; termina executia
13 TEMP DS 1 ; Var TEMP : BYTE
14 00 BITS DC 0 ; BITS : BYTE
15 END ;

Emulator pentru mașina cu un singur acumulator


Deși pentru aceasta mașina nu exista fizic (în silicon) un procesor, functionarea sa poate fi simulata usor
prin software. Trebuie doar sa scriem un emulator care modeleaza ciclul preia-executa al masinii și acest
lucru se poate realiza în orice limbaj pentru care avem deja un compilator implementat pe o mașina reala.
Pentru acest scop cel mai potrivit limbaj este limbajul C++, care are avantajul ca se pot implementa
diferitele faze ale translatarelor și emulatoarelor ca niste clase coerente și bine separate.
Pentru modelarea acestei masini ipotetice în C++ ar fi convenabil sa definim o interfata în modul obisnuit
prin intermediul interfetei publice la o clasa. Principala responsabilitate a interfetei este sa declare o rutina
emulator pentru interpretarea codului stocat în memoria masinii. Pentru eficienta vom extinde interfata
pentru a expune valorile operatiilor și memoria și pentru a asigura diferite alte facilitati utile care vor ajuta
la dezvoltarea unui asamblor sau compilator pentru aceasta mașina.

// machine instructions - order is significant


enum MC_opcodes {
MC_nop, MC_cla, MC_clc, MC_clx, MC_cmc, MC_inc, MC_dec, MC_inx, MC_dex,
MC_tax, MC_ini, MC_inh, MC_inb, MC_ina, MC_oti, MC_otc, MC_oth, MC_otb,
MC_ota, MC_psh, MC_pop, MC_shl, MC_shr, MC_ret, MC_hlt, MC_lda, MC_ldx,
MC_ldi, MC_lsp, MC_lsi, MC_sta, MC_stx, MC_add, MC_adx, MC_adi, MC_adc,
MC_acx, MC_aci, MC_sub, MC_sbx, MC_sbi, MC_sbc, MC_scx, MC_sci, MC_cmp,
MC_cpx, MC_cpi, MC_ana, MC_anx, MC_ani, MC_ora, MC_orx, MC_ori, MC_brn,
MC_bze, MC_bnz, MC_bpz, MC_bng, MC_bcc, MC_bcs, MC_jsr, MC_bad = 255 };
typedef enum { running, finished, nodata, baddata, badop } status;
typedef unsigned char MC_bytes;
class MC {
public:
MC_bytes mem[256]; // virtual machine memory
void listcode(void);
// Lists the 256 bytes stored în mem on requested output file
void emulator(MC_bytes initpc, FILE *data, FILE *results, bool tracing);
// Emulates action of the instructions stored în mem, with program counter
// initialized to initpc. data and results are used for I/O.
// Tracing at the code level may be requested
void interpret(void);
// Interactively opens data and results files, and requests entry point.
// Then interprets instructions stored în mem
MC_bytes opcode(char *str);
// Maps str to opcode, or to MC_bad (0FFH) if no match can be found
MC();
// Initializes accumulator machine
};

37
LIMBAJE FORMALE SI TRANSLATOARE 2017

Implementarea rutinei emulator trebuie sa modeleze ciclul tipic de preluare-executie al masinii ipotetice.
Acest lucru se realizeaza usor prin executia repetata a unei instructiuni switch și se urmareste algoritmul
din sectiunea 4.1, dar trebuie prevazuta posibilitatea ca programul sa se opreasca pentru a se evita anumite
erori:

BEGIN
InitializeProgramCounter(CPU.PC);
InitializeRegisters(CPU.A, CPU.X, CPU.SP, CPU.Z, CPU.P, CPU.C);
PS := running;
REPEAT
CPU.IR := Mem[CPU.PC]; Increment(CPU.PC) (* fetch *)
CASE CPU.IR OF (* execute *)
....
END
UNTIL PS # running;
IF PS # finished THEN PostMortem END
END

Un asamblor minimal pentru aceasta mașina


Avand emulatorul implementat anterior și o modalitate de asamblare și compilare a programelor, este
posibil sa se implementeze un sistem complet load-and-go pentru dezvoltarea și rularea de programe
simple. Un asamblor poate fi realizat printr-o clasa cu o interfata publica de genul:

class AS {
public:
AS(char *sourcename, MC *M);
// Opens source file from supplied sourcename
~AS();
// Closes source file
void assemble(bool &errors);
// Assembles source code from src file and loads bytes of code directly
// into memory. Returns errors = true if source code is corrupt
};

Utilizand aceste doua clase, sistemul load-and-go poate lua forma urmatoare:

void main(int argc, char *argv[])


{ bool errors;
if (argc == 1) { printf("Usage: ASSEMBLE source\n"); exit(1); }
MC *Machine = new MC();
AS *Assembler = new AS(argv[1], Machine);
Assembler->assemble(errors);
delete Assembler;
if (errors)
printf("Unable to interpret code\n");

38
LIMBAJE FORMALE SI TRANSLATOARE 2017

else
{ printf("Interpreting code ...\n");
Machine->interpret();
}
delete Machine;
}

Intr-un capitol urmator se vor prezenta în detaliu tehnicile specifice asambloarelor. Pentru moment se
observa ca pot fi scrise diverse implementari, în functie de complezitate, utilizand aceasta interfata. Cea
mai simpla dintre acestea ar necesita ca utilizatorul sa asambleze de mana programul sau și ar consta
dintr-un simplu loader:

AS::AS(char *sourcename, MC *M)


{ Machine = M;
src = fopen(sourcename, "r");
if (src == NULL) { printf("Could not open input file\n"); exit(1); }
}
AS::~AS()
{ if (src) fclose(src); src = NULL; }
void AS::assemble(bool &errors)
{ int number;
errors = false;
for (int i = 0; i <= 255; i++)
{ if (fscanf(src, "%d", &number) != 1)
{ errors = true; number = MC_bad; }
Machine->mem[i] = number % 256;
}
}

Oricum, este destul de usor sa se scrie o implementarea alternativa a rutinei assemble care permite
sistemului sa accepte a secventa de mnemonici și campuri de adrese numerice, ca cele din exemplele
anterioare. Se prezinta în continuare un cod posibil, cu comentariile de rigoare:

void readmnemonic(FILE *src, char &ch, char *mnemonic)


{ int i = 0;
while (ch > ’ ’)
{ if (i <= 2) { mnemonic[i] = ch; i++; }
ch = toupper(getc(src));
}
mnemonic[i] = ’\0’;
}
void readint(FILE *src, char &ch, int &number, bool &okay)
{ okay = true;
number = 0;
bool negative = (ch == ’-’);
if (ch == ’-’ || ch == ’+’) ch = getc(src);

39
LIMBAJE FORMALE SI TRANSLATOARE 2017

while (ch > ’ ’)


{ if (isdigit(ch))
number = number * 10 + ch - ’0’;
else
okay = false;
ch = getc(src);
}
if (negative) number = -number;
}
void AS::assemble(bool &errors)
{ char mnemonic[4]; // mnemonic for matching
MC_bytes lc = 0; // location counter
MC_bytes op; // assembled opcode
int number; // assembled number
char ch; // general character for input
bool okay; // error checking on reading numbers
printf("Assembling code ... \n");
for (int i = 0; i <= 255; i++) // fill with invalid opcodes
Machine->mem[i] = MC_bad;
lc = 0; // initialize location counter
errors = false; // optimist!
do
{ do ch = toupper(getc(src));
while (ch <= ’ ’ && !feof(src)); // skip spaces and blank lines
if (!feof(src)) // there should be a line to assemble
{ if (isupper(ch)) // we should have a mnemonic
{ readmnemonic(src, ch, mnemonic); // unpack it
op = Machine->opcode(mnemonic); // look it up
if (op == MC_bad) // the opcode was unrecognizable
{ printf("%s - Bad mnemonic at %d\n", mnemonic, lc); errors = true; }
Machine->mem[lc] = op; // store numerical equivalent
}
else // we should have a numeric constant
{ readint(src, ch, number, okay); // unpack it
if (!okay) { printf("Bad number at %d\n", lc); errors = true; }
if (number >= 0) // convert to proper byte value
Machine->mem[lc] = number % 256;
else
Machine->mem[lc] = (256 - abs(number) % 256) % 256;
}
lc = (lc + 1) % 256; // bump up location counter
}
} while (!feof(src));
}

4.4. Studiul de caz 2 – un computer orientat spre stiva

40
LIMBAJE FORMALE SI TRANSLATOARE 2017

În sectiunile urmatoare vom incerca sa dezvoltam un compilator care genereaza cod obiect pentru o
„mașina stiva” ipotetica, una care poate sa nu aiba registrii generali de date de tipul celor discutati în
studiul de caz 1, dar care functioneaza prin manipularea unui pointer de stiva și a stivei asociate. O
arhitectura ca aceasta se va vedea ca este ideala pentru evaluarea expresiilor aritmetice sau Booleene
complicate, ca și pentru implementarea limbajelor de nivel înalt care permit recursivitatea. Vom analiza
aceasta mașina în acelasi mod în care am analizat mașina cu un singur acumulator în sectiunea precedenta.

Arhitectura masinii
Comparata cu mașinile bazate pe registrii normali, aceasta ar putea parea la inceput un pic ciudata,
datorita saraciei de registrii. Ca element comun cu majoritatea masinilor vom presupune ca și aceasta
stocheaza codul și datele intr-o memorie care poate fi modelata ca un sir liniar. Elementele memoriei sunt
„cuvinte”, fiecare dintre acestea poate stoca un singur intreg – în general utilizand reprezentarea pe 16 biti
în complement fata de 2. În fig.4.4 se prezinta schema bloc a acestei masini.

Fig.4.4. O CPU simpla orientata spre stiva

Simbolurile din aceasta schema bloc se refera la urmatoarele componente ale masinii:
ALU este unitatea aritmetica și logica în care se realizeaza de fapt operatiile aritmetice și logice.
Temp este un set de registrii de 16 biti pentru stocarea rezultatelor intermediare necesare în timpul
operatiilor aritmetice și logice. Acesti registrii nu pot fi accesati în mod explicit
SP este un pointer de stiva de 16 biti, un registru care indica zona de memorie utilizata ca stiva
principala.
BP este un pointer de baza de 16 biti, un registru care indica baza unei zone de memorie din cadrul
stivei, cunoscuta ca stiva frame, care se utilizeaza pentru stocarea variabilelor.
MP este un pointer de stiva de marcaj (mark stack) de 16 biti, un registru utilizat la manipularea
apelurilor de subrutina, a carui utilitate se va vedea în capitolele urmatoare.
IR este un registru de instructiune de 16 biti, în care se mentine instructiunea curenta în curs de
executie.
PC este un contor de program de 16 biti, care contine adresa din memorie a instructiunii care
urmeaza a fi executata.
EAR este registru de adresa efectiva, care contine adresa din memorie a datelor manipulate de
instructiunea curenta.
Un program care modeleaza aceasta mașina în C++ este de forma:

Const int MemSize=512;

41
LIMBAJE FORMALE SI TRANSLATOARE 2017

typedef short address;


struct processor {
opcodes ir;
address bp, mp, sp, pc;
};
typedef enum {running, finished,
badmem, baddata, nodata,
divzero, badop
} status;
processor cpu;
int mem[MemSize];
status ps;
Pentru a simplifica lucrurile vom presupune ca în aceasta mașina codul este stocat în extremitatea de jos a
memoriei și ca extremitatea de sus a memoriei este utilizata ca stiva pentru stocarea datelor. Vom
presupune ca sectiunea cea mai inalta a acestei stive este un sector literal, în care se stocheaza constante,
cum sunt sirurile de caractere literale.
Imediat dupa aceasta zona este stiva frame, în care sunt stocate variabilele statice. Restul stivei se va
utiliza pentru stocari în timpul lucrului. O harta tipica a memoriei ar putea fi reprezentata ca în fig.4.5,
unde marcajele CodeTop și StkTop vor fi utile pentru a asigura protectia memoriei intr-un sistem emulat.

Fig.4.5. Utilizarea memoriei intr-un computer orientat spre stiva

Presupunem ca incarcatorul (loader) programului va incarca în partea de jos a memoriei codul (lasand
marcajul CodeTop sa indice spre ultimul cuvant de cod). Acesta va incarca și constantele literale în
sectorul corespunzator (lasand marcajul StkTop sa indice spre extremitatea inferioara a acestei zone). Va
continua cu initializarea atat a pointerului de stiva SP cat și a pointerului de baza BP cu valoarea lui
StkTop. Prima instructiune din orice program are sarcina de a rezerva spatiul dinstiva necesar pentru
variabilele sale, prin simpla decrementare a pointerului de stiva SP cu numarul de cuvinte necesare acestor
variabile. O variabila poate fi adresata prin adunarea unui offset la registrul de baza BP. Deoarece stiva
„creste în jos” în memorie, de la adrese mari spre adrese mici, aceste offset-uri vor avea de obicei valori
negative.

Setul de instructiuni
În continuare se descrie în mod informal un set minimal de operatii pentru aceasta mașina. În capitolele
urmatoare se vor adauga mai multe operatii la acest set. Vom utiliza mnemonicele introduse aici pentru a
coda programe pentru mașina în ceea ce pare a fi un limbaj de asamblare simplu, deși cu adresele
specificate în forma absoluta.
Cateva dintre aceste operatii apartin unei categorii cunoscuta ca instructiuni cu zero adrese. Deși sunt
absolut necesari operanzi pentru operatii cum sunt adunarea și inmultirea, adresele acestora nu sunt
specificate prin parti ale instructiunii, ci sunt implicit derivate din valoarea pointerului de stiva SP. Cei doi
operanzi se presupune ca sunt rezidenti în varful stivei; intr-o descriere informala valorile lor sunt notate

42
LIMBAJE FORMALE SI TRANSLATOARE 2017

prin TOS („top of stack”) și SOS („second on stack”). O operatie binara se realizeaza prin extragerea celor
doi operanzi ai sai din stiva în registrii interni (inaccesibili) ai CPU, realizarea operatiei și apoi depunerea
rezultatului inapoi în stiva. Asemenea operatii pot fi foarte economic codificate în conditiile în care
stocarea este preluata de insusi codul program – densitatea mare a codului mașina al masinii orientate spre
stiva este un alt punct în favoarea sa în ceea ce priveste dezvoltarea translatoarelor interpretative.

ADD Extrage TOS și SOS, aduna SOS la TOS, depune suma în noul TOS
SUB Extrage TOS și SOS, scade TOS din SOS, depune rezultatul în noul TOS
MUL Extrage TOS și SOS, inmulteste SOS cu TOS, depune rezultatul în noul TOS
DVD Extrage TOS și SOS, imparte SOS la TOS, depune rezultatul în noul TOS
EQL Extrage TOS și SOS, depunde 1 în noul TOS daca SOS=TOS, 0 altfel
NEQ Extrage TOS și SOS, depunde 1 în noul TOS daca SOS#TOS, 0 altfel
GTR Extrage TOS și SOS, depunde 1 în noul TOS daca SOS>TOS, 0 altfel
LSS Extrage TOS și SOS, depunde 1 în noul TOS daca SOS<TOS, 0 altfel
LEQ Extrage TOS și SOS, depunde 1 în noul TOS daca SOS<=TOS, 0 altfel
GEQ Extrage TOS și SOS, depunde 1 în noul TOS daca SOS>=TOS, 0 altfel
NEG Neaga TOS

STK Descarca stiva la iesire (util pentru depanare)


PRN Extrage TOS și il scrie la iesire ca o valoare intreaga
PRS A Scrie sirul nu terminator nul care a fost stocat în sectorul literal din Mem[A]
NLN Scrie o secventa newline (carrige return + line feed)
INN Citeste valoare intreaga, extrage TOS, stocheaza valoarea citita în Mem[TOS]

DSP A Decrementeaza valoare pointerului de stiva SP prin A


LIT A Depune valoarea intreaga A în stiva pentru a forma noul TOS
ADR A Depune valoarea BP+A în stiva pentru a forma noul TOS (Aceasta valoare este
adresa unei variabile stocate la un offset A în cadrul stivei frame indicata prin
registrul de baza BP)
IND Extrage TOS în Size; extrage TOS și SOS; daca 0<=TOS<Size atunci scade TOS
din SOS și depune rezultatul în noul TOS
VAL Extrage TOS și depune valoare lui Mem[TOS] în noul TOS (dereferencing)
STO Extrage TOS și SOS; stocheaza TOS în Mem[SOS]

HLT Opreste executia


BRN A Ramificare neconditionata la instructiunea A
BZE A Extrage TOS și ramificare la instructiunea A daca TOS este zero
NOP Nici o operatie
Instructiunile din primul grup se ocupa cu operatii aritmetice și logice, cele din al doilea grup cu
facilitatile I/O, cele din al treilea grup permit accesul la date din memorie prin intermediul manipularii
adreselor și a stivei și cele din ultimul grup permit controlul fluxului programului. Operatia IND permite
indexarea matricilor cu verificarea domeniului indexilor.
Ca și în exemplul anterior, operatiile de I/O nu sunt tipice pentru mașinile reale, dar ne permit tratarea
principiilor de emulare fara a intra în detaliile realizarii sistemelor de I/O reale.

43
LIMBAJE FORMALE SI TRANSLATOARE 2017

Exemple de programe
Se vor prezenta cateva exemple de coduri pentru aceasta mașina pentru a ilustra modul sau de functionare.

1. Pentru a exemplifica modul de alocare a memoriei, se considera o sectiune simpla de program care
corespunde codului de nivel înalt de forma:

X=8; Write (”Y=”,Y);


0 DSP 2 ; X is at Mem[CPU.BP-1], Y is at Mem[CPU.BP-2]
2 ADR -1 ; push address of X
4 LIT 8 ; push 8
6 STO ; X := 8
7 STK ; dump stack to look at it
8 PRS ’Y = ’ ; Write string "Y = "
10 ADR -2 ; push address of Y
12 VAL ; dereference
13 PRN ; Write integer Y
14 HLT ; terminate execution

Acesta ar fi stocat în memorie sub forma:

DSP 2 ADR -1 LIT 8 STO STK PRS 510 ADR -2 VAL


0 1 2 3 4 5 6 7 8 9 10 11 12

PRN HLT ... (Y) (X) 0 ‘‘ ‘=’ ‘‘ ‘Y’ 0


13 14 504 505 506 507 508 509 510 511

Imediat dupa incarcarea acestui program (și inainte de executarea istructiunii DSP), contorul program PC
ar trebui sa aiba valoarea 0, în timp ce registrul de baza BP și pointerul de stiva SP ar trebui fiecare sa aiba
valoarea 506.

2. Exemplul anterior este un exemplu destul de rudimentar. Un program ceva mai pretentios este prezentat
în continuare pentru urmatorul algoritm simplu:

BEGIN
Y=0;
REPEAT READ(X); Y=X+Y UNTIL X=0;
WRITE(’Totalul este ’, Y);
END

Acesta necesita o stiva frame de dimensiune 2, care sa contina variabilele X și Y. Codul mașina pentru
acest algoritm este:

0 DSP 2 ; X is at Mem[CPU.BP-1], Y is at Mem[CPU.BP-2]


2 ADR -2 ; push address of Y (CPU.BP-2) on stack
4 LIT 0 ; push 0 on stack
6 STO ; store 0 as value of Y

44
LIMBAJE FORMALE SI TRANSLATOARE 2017

7 ADR -1 ; push address of X (CPU.BP-1) on stack


9 INN ; read value, store on X
10 ADR -2 ; push address of Y on stack
12 ADR -1 ; push address of X on stack
14 VAL ; dereference - value of X now on stack
15 ADR -2 ; push address of Y on stack
17 VAL ; dereference - value of Y now on stack
18 ADD ; add X to Y
19 STO ; store result as new value of Y
20 ADR -1 ; push address of X on stack
22 VAL ; dereference - value of X now on stack
23 LIT 0 ; push constant 0 onto stack
25 EQL ; check equality
26 BZE 7 ; branch if X # 0
28 PRS ’Total is’ ; label output
30 ADR -2 ; push address of Y on stack
32 VAL ; dereference - value of Y now on stack
33 PRN ; write result
34 HLT ; terminate execution

Un emulator pentru mașina stiva


Din nou, pentru a emula aceasta mașina prin intermediul unui program scris în C++, trebuie definita o
interfata cu mașina prin intermediul unui clase. Ca și în cazul masinii acumulator, principalul element
exportat este o rutina care realizeaza emulatia propriu-zisa, dar pentru eficienta vom exporta și alte entitati
care vor usura dezvoltarea unui asamblor, compilator sau loader care va lasa pseudocod direct în memorie
dupa translatarea unui cod sursa.

const int STKMC_memsize = 512; // Limit on memory


// machine instructions - order is significant
enum STKMC_opcodes {
STKMC_adr, STKMC_lit, STKMC_dsp, STKMC_brn, STKMC_bze, STKMC_prs, STKMC_add,
STKMC_sub, STKMC_mul, STKMC_dvd, STKMC_eql, STKMC_neq, STKMC_lss, STKMC_geq,
STKMC_gtr, STKMC_leq, STKMC_neg, STKMC_val, STKMC_sto, STKMC_ind, STKMC_stk,
STKMC_hlt, STKMC_inn, STKMC_prn, STKMC_nln, STKMC_nop, STKMC_nul
};
typedef enum {
running, finished, badmem, baddata, nodata, divzero, badop, badind
} status;
typedef int STKMC_address;
class STKMC {
public:
int mem[STKMC_memsize]; // virtual machine memory
void listcode(char *filename, STKMC_address codelen);
// Lists the codelen instructions stored în mem on named output file
void emulator(STKMC_address initpc, STKMC_address codelen,
STKMC_address initsp, FILE *data, FILE *results,

45
LIMBAJE FORMALE SI TRANSLATOARE 2017

bool tracing);
// Emulates action of the codelen instructions stored în mem, with
// program counter initialized to initpc, stack pointer initialized to
// initsp. data and results are used for I/O. Tracing at the code level
// may be requested
void interpret(STKMC_address codelen, STKMC_address initsp);
// Interactively opens data and results files. Then interprets the
// codelen instructions stored în mem, with stack pointer initialized
// to initsp
STKMC_opcodes opcode(char *str);
// Maps str to opcode, or to STKMC_nul if no match can be found
STKMC();
// Initializes stack machine
};

Emulatorul trebuie sa modeleze ciclul tipic de preluare-executie al unei masini reale. Acest lucru, la fel ca
în cazul precedent, este realizat usor și urmeaza un sablon aproape identic cu cel utilizat la cealalta
mașina. În continuare se prezinta cele mai importante parti ale emulatorului:

bool STKMC::inbounds(int p)
// Check that memory pointer p does not go out of bounds. This should not
// happen with correct code, but it is just as well to check
{ if (p < stackmin || p >= STKMC_memsize) ps = badmem;
return (ps == running);
}
void STKMC::stackdump(STKMC_address initsp, FILE *results, STKMC_address pcnow)
// Dump data area - useful for debugging
{ int online = 0;
fprintf(results, "\nStack dump at %4d", pcnow);
fprintf(results, " SP:%4d BP:%4d SM:%4d\n", cpu.sp, cpu.bp, stackmin);
for (int l = stackmax - 1; l >= cpu.sp; l--)
{ fprintf(results, "%7d:%5d", l, mem[l]);
online++; if (online % 6 == 0) putc(’\n’, results);
}
putc(’\n’, results);
}
void STKMC::trace(FILE *results, STKMC_address pcnow)
// Simple trace facility for run time debugging
{ fprintf(results, " PC:%4d BP:%4d SP:%4d TOS:", pcnow, cpu.bp, cpu.sp);
if (cpu.sp < STKMC_memsize)
fprintf(results, "%4d", mem[cpu.sp]);
else
fprintf(results, "????");
fprintf(results, " %s", mnemonics[cpu.ir]);
switch (cpu.ir)
{ case STKMC_adr:

46
LIMBAJE FORMALE SI TRANSLATOARE 2017

case STKMC_prs:
case STKMC_lit:
case STKMC_dsp:
case STKMC_brn:
case STKMC_bze:
fprintf(results, "%7d", mem[cpu.pc]); break;
// no default needed
}
putc(’\n’, results);
}
void STKMC::postmortem(FILE *results, STKMC_address pcnow)
// Report run time error and position
{ putc(’\n’, results);
switch (ps)
{ case badop: fprintf(results, "Illegal opcode"); break;
case nodata: fprintf(results, "No more data"); break;
case baddata: fprintf(results, "Invalid data"); break;
case divzero: fprintf(results, "Division by zero"); break;
case badmem: fprintf(results, "Memory violation"); break;
case badind: fprintf(results, "Subscript out of range"); break;
}
fprintf(results, " at %4d\n", pcnow);
}
void STKMC::emulator(STKMC_address initpc, STKMC_address codelen,
STKMC_address initsp, FILE *data, FILE *results,
bool tracing)
{ STKMC_address pcnow; // current program counter
stackmax = initsp;
stackmin = codelen;
ps = running;
cpu.sp = initsp;
cpu.bp = initsp; // initialize registers
cpu.pc = initpc; // initialize program counter
do
{ pcnow = cpu.pc;
if (unsigned(mem[cpu.pc]) > int(STKMC_nul)) ps = badop;
else
{ cpu.ir = STKMC_opcodes(mem[cpu.pc]); cpu.pc++; // fetch
if (tracing) trace(results, pcnow);
switch (cpu.ir) // execute
{ case STKMC_adr:
cpu.sp--;
if (inbounds(cpu.sp))
{ mem[cpu.sp] = cpu.bp + mem[cpu.pc]; cpu.pc++; }
break;
case STKMC_lit:

47
LIMBAJE FORMALE SI TRANSLATOARE 2017

cpu.sp--;
if (inbounds(cpu.sp)) { mem[cpu.sp] = mem[cpu.pc]; cpu.pc++; }
break;
case STKMC_dsp:
cpu.sp -= mem[cpu.pc];
if (inbounds(cpu.sp)) cpu.pc++;
break;
case STKMC_brn:
cpu.pc = mem[cpu.pc]; break;
case STKMC_bze:
cpu.sp++;
if (inbounds(cpu.sp))
{ if (mem[cpu.sp - 1] == 0) cpu.pc = mem[cpu.pc]; else cpu.pc++; }
break;
case STKMC_prs:
if (tracing) fputs(BLANKS, results);
int loop = mem[cpu.pc];
cpu.pc++;
while (inbounds(loop) && mem[loop] != 0)
{ putc(mem[loop], results); loop--; }
if (tracing) putc(’\n’, results);
break;
case STKMC_add:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] += mem[cpu.sp - 1];
break;
case STKMC_sub:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] -= mem[cpu.sp - 1];
break;
case STKMC_mul:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] *= mem[cpu.sp - 1];
break;
case STKMC_dvd:
cpu.sp++;
if (inbounds(cpu.sp))
{ if (mem[cpu.sp - 1] == 0)
ps = divzero;
else
mem[cpu.sp] /= mem[cpu.sp - 1];
}
break;
case STKMC_eql:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] == mem[cpu.sp - 1]);

48
LIMBAJE FORMALE SI TRANSLATOARE 2017

break;
case STKMC_neq:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] != mem[cpu.sp - 1]);
break;
case STKMC_lss:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] < mem[cpu.sp - 1]);
break;
case STKMC_geq:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] >= mem[cpu.sp - 1]);
break;
case STKMC_gtr:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] > mem[cpu.sp - 1]);
break;
case STKMC_leq:
cpu.sp++;
if (inbounds(cpu.sp)) mem[cpu.sp] = (mem[cpu.sp] <= mem[cpu.sp - 1]);
break;
case STKMC_neg:
if (inbounds(cpu.sp)) mem[cpu.sp] = -mem[cpu.sp];
break;
case STKMC_val:
if (inbounds(cpu.sp) && inbounds(mem[cpu.sp]))
mem[cpu.sp] = mem[mem[cpu.sp]];
break;
case STKMC_sto:
cpu.sp++;
if (inbounds(cpu.sp) && inbounds(mem[cpu.sp]))
mem[mem[cpu.sp]] = mem[cpu.sp - 1];
cpu.sp++;
break;
case STKMC_ind:
if ((mem[cpu.sp + 1] < 0) || (mem[cpu.sp + 1] >= mem[cpu.sp]))
ps = badind;
else
{ cpu.sp += 2;
if (inbounds(cpu.sp)) mem[cpu.sp] -= mem[cpu.sp - 1];
}
break;
case STKMC_stk:
stackdump(initsp, results, pcnow); break;
case STKMC_hlt:
ps = finished; break;

49
LIMBAJE FORMALE SI TRANSLATOARE 2017

case STKMC_inn:
if (inbounds(cpu.sp) && inbounds(mem[cpu.sp]))
{ if (fscanf(data, "%d", &mem[mem[cpu.sp]]) == 0)
ps = baddata;
else
cpu.sp++;
}
break;
case STKMC_prn:
if (tracing) fputs(BLANKS, results);
cpu.sp++;
if (inbounds(cpu.sp)) fprintf(results, " %d", mem[cpu.sp - 1]);
if (tracing) putc(’\n’, results);
break;
case STKMC_nln:
putc(’\n’, results); break;
case STKMC_nop:
break;
default:
ps = badop; break;
}
}
} while (ps == running);
if (ps != finished) postmortem(results, pcnow);
}

Trebuie sa observam ca acest interpretor contine destul de mult cod de verificare a erorilor. Acest lucru va
diminua eficienta interpretorului, dar este cod care probabil este foarte necesar la testarea sistemului.

Un asamblor minimal pentru mașina


Pentru a putea utiliza acest sistem trebuie, deșigur, sa avem o modalitate de incarcarea sau asamblare a
codului în memorie. Se poate dezvolta usor un asamblor utilizand urmatoarea interfata, foarte similara cu
cea utilizata pentru mașina cu un singur acumulator.

class STKASM {
public:
STKASM(char *sourcename, STKMC *M);
// Opens source file from supplied sourcename
~STKASM();
// Closes source file
void assemble(bool &errors, STKMC_address &codetop,
STKMC_address &stktop);
// Assembles source code from an input file and loads codetop
// words of code directly into memory mem[0 .. codetop-1],
// storing strings în the string pool at the top of memory în
// mem[stktop .. STKMC_memsize-1].

50
LIMBAJE FORMALE SI TRANSLATOARE 2017

//
// Returns
// codetop = number of instructions assembled and stored
// în mem[0] .. mem[codetop - 1]
// stktop = 1 + highest byte în memory available
// below string pool în mem[stktop] .. mem[STK_memsize-1]
// errors = true if erroneous instruction format detected
// Instruction format :
// Instruction = [Label] Opcode [AddressField] [Comment]
// Label = Integer
// Opcode = STKMC_Mnemonic
// AddressField = Integer | ’String’
// Comment = String
//
// A string AddressField may only be used with a PRS opcode
// Instructions are supplied one to a line; terminated at end of input file
};

Aceasta interfata ne va permite sa dezvoltam asambloare sofisticate fara a altera restul sistemului – numai
implementarea. În particular putem scrie foarte usor un asamblor/interpretor load-and-go, utilizand în
esenta acelasi sistem cu cel sugerat în cazul precedent.
Obiectivul acestui capitol este prezentarea principiilor de emulare a masinilor și nu preocuparea cu
problemele legate de asamblare. Daca ne limitam la asamblarea codului în care operatiile sunt notate prin
mnemonici, dar toate adresele și deplasamentele sunt scrise în forma absoluta, ca în exemplele anterioare,
atunci poate fi scris destul de usor un asamblor rudimentar. Esenta acestuia este descrisa informal printr-
un algoritm de genul:

BEGIN
CodeTop := 0;
REPEAT
SkipLabel;
IF NOT EOF(SourceFile) THEN
Extract(Mnemonic);
Convert(Mnemonic, OpCode);
Mem[CodeTop] := OpCode; Increment(CodeTop);
IF OpCode = PRS THEN
Extract(String); Store(String, Address);
Mem[CodeTop] := Address; Increment(CodeTop);
ELSIF OpCode în {ADR, LIT, DSP, BRN, BZE} THEN
Extract(Address); Mem[CodeTop] := Address; Increment(CodeTop);
END;
IgnoreComments;
END
UNTIL EOF(SourceFile)
END

51
LIMBAJE FORMALE SI TRANSLATOARE 2017

O implementare a acestuia poate fi realizata în C++. Se presupune ca intregul cod este introdus în mașina
intr-un format liber, o instructiune pe linie. Pot fi adaugate comentarii și etichete, ca în exemplele de mai
sus, dar acestea sunt ignorate de catre asamblor. Deoarece sunt necesare adrese absolute, orice etichete
sunt mai mult o pacoste decat o valoare.

52

S-ar putea să vă placă și