Implementazione di Linguaggi 2 PARTE 6 Testo: A.V. Aho, R. Sethi, J.D.Ullman Compilers Principles,Techniques and Tools, Addison Wesley
Type Checking Si divide in statico se eseguito durante la compilazione e dinamico se eseguito durante l’esecuzione del programma. È affiancato da altri controlli: Flow-of-control: goto return exit … Unicità di definizione: identificatori nello stesso scope, label di switch/case stat. controlli contestuali dei nomi es. (Modula) PROCEDURE P;…END P;
Ruolo del Type Checker Sostanzialmente controllare che le operazioni vengano applicate a tipi compatibili.
Type Expressions (TE) Sono espressioni che servono per definire i tipi di dato di un linguaggio. usano operatori detti type constructors e operandi detti tipi base. usano operatori detti type constructors e operandi detti tipi base. Le TE denotano i tipi di un linguaggio e sono fortemente dipendenti da esso. Le TE denotano i tipi di un linguaggio e sono fortemente dipendenti da esso.
Type Expressions (TE) Formalmente: Un tipo base (int, float, integer, real, complex, …) è una type expression; Un tipo base (int, float, integer, real, complex, …) è una type expression; Un tipo può avere un nome, per cui il nome di un tipo è una type expression; Un tipo può avere un nome, per cui il nome di un tipo è una type expression; Un costruttore di tipo applicato ad una type expression genera una nuova type expression: Un costruttore di tipo applicato ad una type expression genera una nuova type expression:
Type Expressions (TE) Costruttori di tipo Costruttori di tipo 1. Array: se T è una TE allora array(I,T) è una TE che denota un tipo array con tipo base T e tipo indice I 2. Prodotti cartesiani: se T 1 e T 2 sono TE allora T 1 T 2 è una TE che denota l’insieme delle coppie (t 1,t 2 ) con t 1 T 1 e t 2 T 2 ( associa a sinistra) 3. Record: il prodotto del tipo dei suoi campi, differisce dal prodotto per la commutativitò degli elementi dovuta alla selezione mediante nome (field label) ES. ES. {f 1 : 1 ;…; f n : n } un record con campi f i di tipo i
Type Expressions (TE) 4. Puntatori: se T è una TE allora pointer(T) è una TE che denota un riferimento (puntatore) ad un oggetto di tipo T 5. Funzioni: è il tipo di una funzione che mappa un tipo dominio D in un tipo rango R e denotato D R. 6. Class: è un misto tra il concetto di modulo e di tipo. È modellabile come un record con campi di tipo procedura e di tipo funzione detti metodi.
Type System Un Type System è una collezione di regole per assegnare TE alle parti di un programma. Un Type System è una collezione di regole per assegnare TE alle parti di un programma. È interessante implementare type system con DGDS È interessante implementare type system con DGDS Un Type Checker (TC) è un programma che implementa un Type System. Un Type Checker (TC) è un programma che implementa un Type System. Ogni TC può essere applicato a run time purché il codice oggetto contenga informazioni che specificano il tipo di ogni dato. Ogni TC può essere applicato a run time purché il codice oggetto contenga informazioni che specificano il tipo di ogni dato.
Type System Un Type System è detto sound se elimina la necessità di type checking a run time, cioè se il TC assegna un tipo type-error ad ogni dato del programma, elimina la necessità di type checking a run time, cioè se il TC assegna un tipo type-error ad ogni dato del programma, allora nessun errore di tipo si può verificare durante l’esecuzione.
Type checking: Schema di traduzione DS Un semplice type checker: P D ; E D D ; D | id : T T char | integer | array[num] of T | ^T E literal | num | id | E mod E | E [E] | E^ Esempio key: integer; key: integer; key mod 1999 key mod 1999
Type checking: Schema di traduzione DS Aggiungo il tipo base type_error: P D ; E D D ; D D id : T addtype(id.entry,T.type) T char T.type:=char T integer T.type:=integer T array[num] of T 1 T.type:=array(1..num.val,T 1.type) T ^T 1 T.type:=pointer(T 1.type)
Type Checking di Espressioni E literal E.type:=char E num E.type:=integer E id E.type:=lookup(id.entry) E E 1 mod E 2 E.type:= if E 1.type=integer & E 2.type=integer then integer else type_error else type_error
Type Checking di Espressioni E E 1 [E 2 ] {E.type:= if E 2.type=integer & E 1.type=array(s,t) then t else type_error} else type_error} E E 1 ^ {E.type:= if E 1.type=pointer(t) then t else type_error} else type_error}
Type Checking di Istruzioni S id := E S.type:= if id.type=E.type then void else type_error else type_error S if E then S 1 S.type:= if E.type =boolean then S 1.type else type_error else type_error S while E do S 1 S.type:= if E.type=boolean then S 1.type else type_error else type_error S S 1 ; S 2 S.type:= if S 1.type=void & S 2.type=void then S.type=void then S.type=void else type_error else type_error
Type Checking di Funzioni T T 1 ’ ’ T 2 T.type:=T 1.type T 2.type E E 1 (E 2 ) E.type:=if E 2.type= s & E 1.type= s t then t then t else type_error else type_error Le funzioni n-arie f(t 1 :T 1,t 2 :T 2,…,t n :T n ) vengono assimilate a funzioni unarie di tipo T=T 1 T 2 … T n root: (real real) real real
Equivalenza di Tipo I linguaggi usano diversi modi e definizioni per stabilire se due tipo sono equivalenti. Gli approcci principali sono: Equivalenza strutturale (Fortran, C, C++, …) Equivalenza strutturale (Fortran, C, C++, …) Equivalenza per nome (Modula, Oberon, Ada, Pascal, …) Equivalenza per nome (Modula, Oberon, Ada, Pascal, …) Misti. Misti.
Equivalenza di Tipo Esempio in Modula-2. Dati: TYPE A=ARRAY[0..n] OF REAL; B=ARRAY[0..n] OF REAL; Dati: TYPE A=ARRAY[0..n] OF REAL; B=ARRAY[0..n] OF REAL; Definiscono due tipi non equivalenti, perché dotati di nome diverso (equivalenza per nome). Tuttavia, A e B sono identici e quindi strutturalmente equivalenti.
Equivalenza di Tipo: per Nome Due variabili hanno tipo equivalente per nome: 1. se appartengono alla stessa dichiarazione, 2. se appaiono in dichiarazioni diverse ma abbinate allo stesso identificatore di tipo. 3. nei linguaggi Oberon-like si ha che due nomi di tipo T e T’ dichiarati uguali denotano due tipi T e T’ identici. NOTA: Linguaggi come PL/M considerano i tipi T e T’ definiti in 3. non equivalenti. Linguaggi come PL/M considerano i tipi T e T’ definiti in 3. non equivalenti.
Equivalenza di Tipo: per Nome Esempi: 1. VAR a, b: ARRAY[1..n] OF REAL; (*a b stessa dichiarazione*) 2. PROCEDURE O(a:T); VAR b:T (* a e b sono equivalenti perché usano lo stesso type id T*) (* a e b sono equivalenti perché usano lo stesso type id T*) 3. TYPE T’ = T (* T e T’ denotano due tipi identici perché dichiarati uguali *)
Equivalenza di Tipo: per Nome Due tipi sono compatibili se: sono identici, oppure sono identici, oppure uno è sottorango dell’altro, oppure uno è sottorango dell’altro, oppure entrambi sono sottorango di uno stesso tipo. entrambi sono sottorango di uno stesso tipo. Un sottorango è definito restringendo ad un intervallo l’insieme dei valori possibili di un tipo ordinale discreto (es. integer o elencazione). Il concetto di sottorango è un caso particolare di sottotipo, un concetto che vedremo in seguito.
Equivalenza di Tipo: per Nome Esempi. TYPE link = ^cell; VAR next : link; last : link; last : link; p : ^cell; p : ^cell; q : ^cell; q : ^cell; Equivalenza per nome: 1. next e last hanno lo stesso tipo perché sono associati alla stessa type expression, 2. p, q, r hanno lo stesso tipo, 3. p e next non sono equivalenti perché associati a type expression diverse.
Compatibilità per Assegnamento Un’espressione E di tipo T 2 è detta compati- bile rispetto all’assegnamento ad un tipo T 1 sse: 1. T 1 e T 2 sono identici e nessuno è di tipo file, 2. T 1 è real e T 2 integer, 3. T 1 e T 2 sono tipi ordinali compatibili e il valo- re di E appartiene all’intervallo chiuso speci- ficato da T 1, 4. T 1 e T 2 sono tipi stringa compatibili.
Equivalenza di Tipo: Strutturale Due type expression E 1 e E 2 sono struttural-mente equivalenti se: 1. E 1 e E 2 sono lo stesso tipo base, oppure 2. E 1 e E 2 sono ottenute applicando lo stesso costruttore di tipo a tipi strutturalmente equivalenti. Quindi due tipi sono strutturalmente equivalenti se sono identici.
Equivalenza di Tipo: Strutturale Esempi. 1. la type expression integer è equivalente solo a integer 2. Pointer(integer) è equivalente solo a pointer(integer)
Equivalenza di Tipo: Strutturale Spesso è possibile codificare le type expression in modo da semplificare la verifica di equivalenza strutturale. Famosa è quella adottata nel compilatore C di Ritchie e Johnson. Costruttori di tipo: pointer(t), freturns(t) e array(t). Costruttori di tipo: pointer(t), freturns(t) e array(t).
Codifica delle type expression. Poiché tutti i costruttori di tipo considerati sono operatori unari le type expression formate applicando questi costruttori ai tipi base hanno una struttura molto uniforme: char char freturns(char) freturns(char) pointer(freturns(char)) pointer(freturns(char)) array(pointer(freturns(char))) array(pointer(freturns(char)))
Codifica delle type expression. TypeconstructorEncoding pointer pointer01 array array10 freturns freturns11 Basic Type Encoding boolean boolean0000 char char0001 integer0010 real real0011 Type Expression Encoding char char freturns(char) pointer(freturns(char)) array(pointer(freturns(char)))
Algoritmo Ricorsivo per Equivalenza Strutturale FUNCTION SEQ(s,t):BOOLEAN; BEGIN IF s & t are the same basic type THEN RETURN TRUE IF s & t are the same basic type THEN RETURN TRUE ELSIF s=array(s1,s2) & t=array(t1,t2) THEN ELSIF s=array(s1,s2) & t=array(t1,t2) THEN RETURN SEQ(s1,t1) & SEQ(s2,t2) RETURN SEQ(s1,t1) & SEQ(s2,t2) ELSIF s=s1 s2 & t=t1 t2 THEN ELSIF s=s1 s2 & t=t1 t2 THEN RETURN SEQ(s1,t1) & SEQ(s2,t2) RETURN SEQ(s1,t1) & SEQ(s2,t2) ELSIF s=pointer(s1) & t=pointer(t1) THEN ELSIF s=pointer(s1) & t=pointer(t1) THEN RETURN SEQ(s1,t1) RETURN SEQ(s1,t1) ELSIF s=s1 s2 & t=t1 t2 THEN ELSIF s=s1 s2 & t=t1 t2 THEN RETURN SEQ(s1,t1) & SEQ(s2,t2) RETURN SEQ(s1,t1) & SEQ(s2,t2) ELSE RETURN FALSE ELSE RETURN FALSEEND
Cicli nelle rappresentazioni di tipi Molte strutture dati sono definite ricorsivamente: Esempio. Linguaggi che permette di assegnare un nome a un tipo. TYPE link= ^cell: cell= RECORD cell= RECORD info:INTEGER; info:INTEGER; next: link next: link END; END;
Cicli nelle rappresentazioni di tipi I nomi di tipo definiti ricorsivamente possono esse- re sostituiti se siamo disposti ad introdurre i cicli nel type graph. Sostituendo link con pointer(cell) Sostituendo link con pointer(cell) cell = record x xx infointegernextpointer cell
Cicli nelle rappresentazioni di tipi Possiamo eliminare il riferimento a cell nel type graph: cell = record x xx infointegernextpointer
Cicli nelle rappresentazioni di tipi Esempio. Il C evita i cicli nei type graph usando l’equivalenza strutturale per tutti i tipi tranne i record. Struct cell{ int info; int info; struct cell *next; struct cell *next;};
Conversioni di Tipo Consideriamo l’espressione: Consideriamo l’espressione: x + i con x di tipo real e i di tipo integer x + i con x di tipo real e i di tipo integer i e x hanno rappresentazioni diverse, i e x hanno rappresentazioni diverse, le istruzioni macchina usate per real e integer sono diverse, le istruzioni macchina usate per real e integer sono diverse, il compilatore deve convertire gli operandi del + per renderli dello stesso tipo. il compilatore deve convertire gli operandi del + per renderli dello stesso tipo. Il type checker può inserire le conversioni nella rappresentazione intermedia del programma.
Conversioni di Tipo La conversione da un tipo ad un altro è detta implicita se è fatta in modo auto- matico dal compilatore. Le conversioni implicite vengono anche dette coercions. La conversione è detta esplicita se il programmatore deve scrivere qualcosa per causare la conversione.
Monomorfismo e Polimorfismo In molti linguaggi tipati staticamente (es. Modula) una variabile T può ricevere valori solo di tipo T. I tipi sottorango di Modula verificano banalmente questa proprietà detta monomorfismo. I linguaggi object-oriented sono polimorfi, nel senso che le variabili possono ricevere valori di più di un tipo.
Generi di Polimorfismo Cardelli e Wegner hanno classificato il polimorfismo in Universale suddiviso in Parametrico (generici) Inclusione (object-oriented) Ad Hoc suddiviso in OverloadingOverloading CoercionCoercion
Polimorfismo Parametrico e Ad Hoc Polimorfismo Parametrico e Ad Hoc Nel polimorfismo parametrico una funzione opera uniformemente su un insieme di tipi che hanno una struttura comune Nel polimorfismo ad hoc una funzione opera su tipi distinti e su ciascuno si comporta in maniera indipendente dagli altri
Polimorfismo Parametrico Polimorfismo Parametrico Nel polimorfismo parametrico l’uniformità è ottenuta mediante parametri di tipo. Il polimorfismo universale opera, in generale, su un insieme potenzialmente infinito di tipi. Nel polimorfismo di inclusione un oggetto appartiene a molte classi diverse ma legate tra loro da ua relazione di inclusione.
Sottotipazione E’ un concetto legato a quello di tipo e di polimorfismo che definisce molti aspetti del type checking. E’ una relazione di preordine su tipi basata sul principio di subsumption: se è allora un valore di tipo è accettato in ogni contesto in cui è previsto un valore di tipo . In formule: E’ un concetto legato a quello di tipo e di polimorfismo che definisce molti aspetti del type checking. E’ una relazione di preordine su tipi basata sul principio di subsumption: se è un sottotipo di allora un valore di tipo è accettato in ogni contesto in cui è previsto un valore di tipo . In formule: ⊦ e: : ⊦ e: <: ___________ ___________ ⊦ e: ⊦ e:
Sottotipazione strutturale e per nome L’equivalenza di tipo adottata influisce anche sulla sottotipazione. Strutturalmente diciamo che 1 2 è un sottotipo di 1 2 se 1 : 1 e 2 : 2. Come detto per i record e le classi si adotta in genere una sottotipazione per nome basato sull’ idea di type extension: una classe C2 estende C1 se C2 ha i tutti i campi di C1 e ne cambia i tipi in modo compatibile L’equivalenza di tipo adottata influisce anche sulla sottotipazione. Strutturalmente diciamo che 1 2 è un sottotipo di 1 2 se 1 <: 1 e 2 <: 2. Come detto per i record e le classi si adotta in genere una sottotipazione per nome basato sull’ idea di type extension: una classe C2 estende C1 se C2 ha i tutti i campi di C1 e ne cambia i tipi in modo compatibile ( i ( i)(i [1,n] i <: i ) _________________________ _________________________ {f 1 : 1 ;…; f n : n }<: {f 1 : 1 ;…; f n : n }
Sottotipazione strutturale e per nome 2 i ( i)(i [1,n] i <: i ) _________________________ _________________________ {f 1 : 1 ;…; f n : n }<: {f 1 : 1 ;…; f n : n } n<m __________________________ __________________________ {f 1 : 1 ;…; f m : m }<: {f 1 : 1 ;…; f n : n }
Conversioni (coercions) Coercion: data l’espressione: x + i Con x reale e i intero, essendo le rappresentazioni interne di x e i diverse e diversi gli algoritmi aritmetici, occorre effettuare una conversione dal tipo piu’ ristretto a quello piu’ generale. In notazione postfissa l’espressione diviene: x i inttoreal real+ x i inttoreal real+ Le conversioni sono implicite se effettuate dal compilatore, esplicite se comandate dall’utente. FOR i:=1 TO N DO x[i]:=float(1) FOR i:=1 TO N DO x[i]:=1.0 Possono avere una differenza di 8.96 unità di tempo per iterazione
Conversioni (coercions) 2 E numE.type:=integer E num. numE.type:=real E idE.type:=lookup(id.entry) E E 1 op E 2 E.type:=IF E 1.type=int&E 2.type=int THEN int ELSIF E 1.type=int & E 2.type=real THEN real ELSIF E 1.type=real&E 2.type=int THEN real ELSIF E 1.type=real&E 2.type=real THEN real ELSE type.error
Overloading 2 L’espressione a+b può indicare somma tra interi, reali, doppia precisione, complessi, matrici o unione tra dati di tipo SET. In questo caso si dice che il simbolo + è overloaded. Un overloading è detto risolvibile se è possibile dedurre un unico tipo per una data espressione.
Overloading Ovviamente, questo non è sempre possibile function “*” (i,j:integer) return complex; function”*”(x,y:integer) return complex; Da cui si deducono I possibili tipi di “*”: int int int int int int int int complex int int complex complex complex complex complex complex complex Dati 2,3 e 5, il valore di 3*5 puo’ essere sia int che complex es: 2*(3*5) 3*5:int; mentre (3*5)*z & z:complex 3*5:complex
Overloading di funzioni Type checking di funzioni overloaded E’ E E’.types:=E.type E id E.types:=lookup(id.entry) E E 1 (E 2 ) E.types:={t| s E 2.types: s t E 1.types} La terza regola asserisce che se s è un tipo di E 2 e uno dei tipi di E 1 mappa s in t, allora t è uno dei tipi di E 1. Qualora si riscontri una incompatibilità che porti a E.types= si avrà un messaggio di errore.
Overloading di funzioni 2 Type checking di funzioni overloaded E:{I,c} E:{i} *:{i i i, i i c, c c c} E:{i} 3:{i}5:{i} Come restringere l’insieme dei tipi possibili Ada e altri linguaggi richiedono un unico tipo per ogni espressione completa. Se questo non e’ possibile si dichiara un errore. Si usa un attributo types che un set di tipi possibili (feasible)
Overloading di funzioni 3 Type checking di funzioni overloaded E EE’.types:=E.types E.uniq:=IF E’.types={t} THEN t ELSE typerr E’.cod:=E.cod E id E.types:=lookup(id.entry) E.cod:=gen(id.lexeme’:’ E.uniq) E E1(E2)E.types:={s’| s E2.types & s s’ E1.types} t:=E.uniq; S:={s|s E2.types& s t E1.types} E2.uniq:=IF S={s} THEN s ELSE typerr; E1.uniq:=IF S={s} THEN s t ELSE typerr; E.cod:=E1.cod||E2.cod||gen(‘apply’)’:’,E.uniq); L’attributo erditato uniq (unique) e’ usato con l’attributo sintetizzato code. La definizione e’ implementata da due visite depth-first dell’albero sintattico nella prima si calcola types bottom upnella prima si calcola types bottom up nella seconda si propaga verso il basso uniq e al ritorno della visita si genera cod.nella seconda si propaga verso il basso uniq e al ritorno della visita si genera cod.
Ereditarietà e sottotipazione La procedura seguente implementa il concetto di record type extension di Wirth (Oberon) PROCEDURE RecordType(VAR typ: Struct) VAR adr,sise:INTEGER;fld,fld0,fld1:Object; ftyp,btyp:Struct;base:Item; ftyp,btyp:Struct;base:Item;BEGIN adr:=0; typ:=NewStr(record); typ.BaseTyp:=NIL; typ.n:=0;
Ereditarietà e sottotipazione 2 IF sym=lparen THEN Get(Sym); (*record extension*) IF sym=ident THEN qualident(base); IF sym=ident THEN qualident(base); IF (base.mode=Typ) & (base.typ.form=Record) THEN IF (base.mode=Typ) & (base.typ.form=Record) THEN typ.BaseTyp:=base.typ;typ.n:=base.typ.n+1 typ.BaseTyp:=base.typ;typ.n:=base.typ.n+1 adr:=base.typ.size adr:=base.typ.size ELSE Error(..) ELSE Error(..) END; END; ELSE Error(..); ELSE Error(..); END; END; CheckSym(rparen) CheckSym(rparen)END;
Ereditarietà e sottotipazione 3 OpenScope(0); fld:=NIL; fld1:=topScope; LOOP LOOP IF sym=ident THEN IF sym=ident THEN LOOP LOOP IF sym=ident THEN IF sym=ident THEN IF typ.BaseTyp # NIL THEN FindField(typ.BaseTyp, fld0); IF typ.BaseTyp # NIL THEN FindField(typ.BaseTyp, fld0); IF fld0 # NIL THEN Error(..) END IF fld0 # NIL THEN Error(..) END END; END; Insert(OCS.name,fld); CheckMark(fld.marked); Insert(OCS.name,fld); CheckMark(fld.marked); fld.mode:=Fld fld.mode:=Fld ELSE Error(..) ELSE Error(..) END; END; IF sym=comma THEN Get(sym) IF sym=comma THEN Get(sym) ELSIF sym=ident THEN Error(..) ELSIF sym=ident THEN Error(..) ELSE E XIT ELSE E XIT END; END;
Ereditarietà e sottotipazione 4 CheckSym(colon); Type(ftyp); CheckSym(colon); Type(ftyp); size:=ftyp.size; btyp:=ftyp; size:=ftyp.size; btyp:=ftyp; WHILE btyp.form=Array DO btyp:=btyp.BaseTyp END; WHILE btyp.form=Array DO btyp:=btyp.BaseTyp END; IF btyp.size>=4 THEN INC(adr, (-adr)MOD 4) IF btyp.size>=4 THEN INC(adr, (-adr)MOD 4) ELSIF btyp.size=2 THEN INC(adr, adr MOD 2) ELSIF btyp.size=2 THEN INC(adr, adr MOD 2) END; END; WHILE fld1.next #NIL DO fld1:=fld1.next; fld1.typ:=ftyp; fld1:=fld1.next; fld1.typ:=ftyp; fld1.a0:=adr; INC(adr,size) fld1.a0:=adr; INC(adr,size) END ENDEND;
Ereditarietà e sottotipazione 5 IF sym=semicolon THEN Get(sym) ELSIF sym=ident THEN Error(..) ELSIF sym=ident THEN Error(..) ELSE EXIT ELSE EXIT END ENDEND; typ.size:=(-adr) MOD 4 + 4; typ.link:=topScope.next; CheckUndefPointerTypes; CloseScope; END RecordType;
Fine Type checking Proseguire con gli argomenti del seminario sul type checking