Logică și structuri discrete - Gramatici

Exercițiu opțional

Gramatici și compilatoare
Când o gramatică definește un limbaj de programare, ea e și punctul de plecare pentru compilator, care traduce cod sursă în cod executabil care poate fi rulat de procesor.
Vom modela întâi un procesor simplu, și apoi vom traduce fragmente simple de program (doar cu atribuire, if și while) pentru a fi rulate pe acest procesor.
Gramatica noastră are structura:
  instr ::= var op_atr valoare | if boolexp lista_instr else lista_instr | while boolexp lista_instr
  boolexp ::= var op_cmp valoare
  lista_instr ::= instr*
  var ::= caracter
  valoare ::= var | întreg
  op_atr ::= = | += | -= | *= | /= | %=
  op_cmp ::= == | != | < | <= | > | >=
Avem deci un fragment de C puternic simplificat:
-- am omis detalii de sintaxă concretă (paranteze la expresii boolene, punct-virgulă și acolade la instrucțiuni), exprimând doar sintaxa abstractă (structura). Nu vom porni de la textul programului, ci direct de la reprezentarea lui în ML
-- singurele operații sunt de atribuire (eventual compusă cu operatori aritmetici), având în partea dreaptă o constantă sau o variabilă: x = 5, a -= b (a = a - b), i += 1 (i = i + 1), etc. Aceste operații simple seamănă cu instrucțiunile procesoarelor uzuale. Am limitat numărul variabilelor pentru că și procesorul are un număr limitat de regiștri (locații interne de memorie cu care poate opera direct), adesea denumite cu litere (deși nu neapărat așa de multe).
Avem deci o corespondență 1:1 între atribuiri și instrucțiunile procesorului și va trebui să tratăm doar secvențierea instrucțiunilor compuse if și while.
Reprezentarea programului sursă:

type register = char                             (* processor registers = variables *)
type rightpart = R of register | I of int        (* second arg of binary op *)
type binop = LD | ADD | SUB | MUL | DIV | MOD    (* =, +=, -=, *=, /=, %= *)
type cmpop = Eq | Ne | Lt | Le | Gt | Ge         (* ==, !=, <, <=, >, >= *)
type boolexp = register * cmpop * rightpart
type statement = Asgn of binop * register * rightpart
                 | If of boolexp * statement list * statement list
                 | While of boolexp * statement list
Am scris ca exemple factorialul pentru n=5 și conjectura 3n+1 (numărul de pași până la 1 pentru n=10):
let factprog =
  [ Asgn (LD, 'n', I 5) ; Asgn (LD, 'r', I 1) ;        (* n = 5; r = 1; *)
    While (('n', Gt, I 0), [Asgn (MUL, 'r', R 'n'); Asgn (SUB, 'n', I 1)]) ]        (* while (n > 0) { r *= n; n -= 1; } *)

let collprog =
  [ Asgn (LD, 'n', I 10); Asgn (LD, 's', I 0);        (* n = 10; s = 0; *)
    While (('n', Gt, I 1),                            (* while (n > 1) { *)
           [ Asgn (ADD, 's', I 1); Asgn (LD, 'p', R 'n'); Asgn (MOD, 'p', I 2);        (* s += 1; p = n; p %= 2; *)
             If (('p', Eq, I 0), [ Asgn (DIV, 'n', I 2) ],                    (* if (p == 0) n /= 2; *)
             [ Asgn (MUL, 'n', I 3); Asgn (ADD, 'n', I 1) ]) ]) ]             (* else { n *= 3; n += 1; } } *)
Reprezentarea codului executabil
O diferență între limbajele de nivel înalt și codul mașină e în reprezentarea fluxului de control (ordinii de execuție a instrucțiunilor). În limbajele de nivel înalt, regulile sunt date de structura instrucțiunilor: dacă am executat blocul then al unei instrucțiuni if, sărim peste partea de else și continuăm cu următoarea instrucțiune. Procesorul are un numărător de program care avansează implicit de la o instrucțiune la alta și instrucțiuni explicite de salt cu un număr de adrese înainte sau înapoi (deobicei se numără față de instrucțiunea următoare, care s-ar executa în absența salutlui). Pentru execuție condiționată, folosim două instrucțiuni: o instrucțiune de comparare, care memorează într-un registru de biți numiți fanioane dacă rezultatul a fost negativ, zero, etc. și apoi o instrucțiune de salt condiționat care efectuează saltul după cum fanionul indică egal (zero), diferit (nonzero), mai mic (negativ), etc., altfel continuă cu următoarea instrucțiune.
Definim următoarele instrucțiuni pentru procesor și reprezentăm cele două programe de mai sus:
type flag_t = Neg | Z | Pos        (* processor flags after CMP instruction *)
type jmpcond = T | E | NE | L | LE | G | GE
type instruction = HLT | BOP of binop * register * rightpart
                   | CMP of register * rightpart | JMP of jmpcond * int

let factbin =
  [|BOP (LD, 'n', I 5); BOP (LD, 'r', I 1); CMP ('n', I 0); JMP (LE, +3);
    BOP (MUL, 'r', R 'n'); BOP (SUB, 'n', I 1); JMP (T, -5); HLT|]
let collbin =
  [|BOP (LD, 'n', I 10); BOP (LD, 's', I 0); JMP (T, +9);
    BOP (ADD, 's', I 1); BOP (LD, 'p', R 'n'); BOP (MOD, 'p', I 2);
    CMP ('p', I 0); JMP (NE, +2); BOP (DIV, 'n', I 2); JMP (T, +2);
    BOP (MUL, 'n', I 3); BOP (ADD, 'n', I 1); 
    CMP ('n', I 1); JMP (G, -11); HLT|]
Instrucțiunile aritmetice/de atribuire corespund 1:1 cu cele ale procesorului. Pentru factorial, condiția n > 0 devine o instrucțiune de comparație;apoi, dacă rezultatul e mai mic sau egal (LE, adică fals în programul sursă), se sare cu 3 instrucțiuni față de instrucțiunea următoare, la HLT (oprirea programului). Altfel, se continuă cu corpul ciclului: înmulțirea, etc. La sfârșitul unei iterații din ciclu se sare necondiționat (reprezentat prin condiția T) cu 5 instrucțiuni înapoi față de următoarea, deci la CMP (testul din ciclu). Sintaxa cu [| |] e pentru tablou (modulul Array care spre deosebire de liste permite accesul direct cu indice la elemente).
Aveți dat un program care simulează rularea unui astfel de model de procesor pe un program reprezentat ca vector de instrucțiuni; el tipărește la fiecare pas indicele de program și valorile variabilelor folosite.
Problema de rezolvat
Scrieți o funcție care ia ca parametru un program de nivel înalt (o listă de instructiuni Asgn/If/While) și produce un program pentru procesor (de tip instruction array).
Indicații: Problema se reduce la a introduce instrucțiunile de salt cu deplasamentele potrivite. Puteți traduce programul de la coadă la cap (cu List.fold_right), astfel când traduceți o instrucțiune If/While aveți deja codul la care trebuie sărit după încheierea ei. Construiți incremental o pereche cu o listă de instrucțiuni de procesor și lungimea ei (evitând recalcularea). În final, produceți un tablou cu Array.of_list.
Pentru saltul înapoi la condiția din While puteți introduce întâi un salt oarecare și după ce ați tradus întregul corp al ciclului să-l înlocuiți cu un salt cu numărul corespunzător de instrucțiuni, sau să traduceți separat corpul ciclului și apoi să-l concatenați cu instrucțiunile de după.
Puteți de asemenea plasa comparația și saltul condiționat înapoi după corpul ciclului, iar la început un salt necondiționat spre comparație. Astfel, fiecare iterație din ciclu va executa un singur salt, nu două.


Marius Minea
Last modified: Mon Dec 28 14:15:00 EET 2015