https://i.postimg.cc/fbw0xvTT/compiler-series.jpg
[Custom Thumbnail]
All the Code of the series can be found at the Github repository:
https://github.com/drifter1/compiler
#define UNDEF 0
#define INT_TYPE 1
#define REAL_TYPE 2
#define CHAR_TYPE 3
#define ARRAY_TYPE 4
#define POINTER_TYPE 5
#define FUNCTION_TYPE 6
That's because, right now, our simple C language only has:typedef struct list_t{
// name, size of name, scope and occurrences (lines)
char st_name[MAXTOKENLEN];
int st_size;
int scope;
RefList *lines;
// to store value and sometimes more information
int st_ival; double st_fval; char st_sval;
// "main" type
int st_type;
// for arrays (info type), for pointers (pointing type)
// and for functions (return type)
int inf_type;
// array stuff
int *i_vals; double *f_vals; char *s_vals;
int array_size;
// function parameters
Param *parameters;
int num_of_pars;
// pointer to next item in the list
struct list_t *next;
}list_t;
When declaring a variable we have to be able to set the type of the identifier afterwards, using the type-information stored in the "type"-rule for example (something that I will not do today, but later on). So, we need some kind of "set type" function that let's us actually declare a variable or an identifier, by now setting the "undefined" type of the identifier to the correct type that was used in the declaration. Such a function will call lookup() to find the corresponding entry of the needed identifier to then access the entries st_type and inf_type, so that we can store the "main" type and info/data, pointing or return type when talking about pointers, arrays or functions.void set_type(char *name, int st_type, int inf_type){
/* lookup entry */
list_t *l = lookup(name);
/* set "main" type */
l->st_type = st_type;
/* if array, pointer or function */
if(inf_type != UNDEF){
l->inf_type = inf_type;
}
}
Of course we could also do this by hand every time, but I think a function that does it for us is much better in the circumstance! In the same way, we should also be able to get the type of the identifier using some "get type" function that gives us the "actual" type of the entry. For "simple" variables we just have to return the value from "st_type", but for the others (array, pointer or function) we have to return the info, pointing or return type, which is stored in "inf_type".int get_type(char *name){
/* lookup entry */
list_t *l = lookup(name);
/* if "simple" type */
if(l->st_type == INT_TYPE || l->st_type == REAL_TYPE || l->st_type == CHAR_TYPE){
return l->st_type;
}
/* if array, pointer or function */
else{
return l->inf_type;
}
}
And the "set_type" function is actually all that we have to call in the actual parser when setting or better said declaring the type of an identifier. The other function will be used, so that we are able to access the type of an identifier when "type checking".
int get_result_type(int type_1, int type_2, int op_type);switch(op_type){
case NONE: /* type compatibility only, '1': compatible */
// first type INT
if(type_1 == INT_TYPE){
// second type INT or CHAR
if(type_2 == INT_TYPE || type_2 == CHAR_TYPE){
return 1;
}
else{
type_error(type_1, type_2, op_type);
}
}
// first type REAL
else if(type_1 == REAL_TYPE){
// second type INT, REAL or CHAR
if(type_2 == INT_TYPE || type_2 == REAL_TYPE || type_2 == CHAR_TYPE){
return 1;
}
else{
type_error(type_1, type_2, op_type);
}
}
// first type CHAR
else if(type_1 == CHAR_TYPE){
// second type INT or CHAR
if(type_2 == INT_TYPE || type_2 == CHAR_TYPE){
return 1;
}
else{
type_error(type_1, type_2, op_type);
}
}
break;
...code continues...
You can see that we have a switch case with the operator type "op_type" and than the current case of "NONE" operator (no operator). That way depending on the first type we see if the second type is compatible with it or not. When they are compatible we return '1'. When not, well, then we call a new function called "type_error". This function just prints out that the two types are incompatible when using this operator type.void type_error(int type_1, int type_2, int op_type){
fprintf(stderr, "Type conflict between %d and %d using op type %d\n",
type_1, type_2, op_type);
exit(1);
}
You can see that we just print out that a type conflict occurred and then end the program with return code '1', meaning error! Pretty easy I guess!...
case ARITHM_OP: /* arithmetic operator */
// first type INT
if(type_1 == INT_TYPE){
// second type INT or CHAR
if(type_2 == INT_TYPE || type_2 == CHAR_TYPE){
return INT_TYPE;
}
// second type REAL
else if(type_2 == REAL_TYPE){
return REAL_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
}
// first type REAL
else if(type_1 == REAL_TYPE){
// second type INT, REAL or CHAR
if(type_2 == INT_TYPE || type_2 == REAL_TYPE || type_2 == CHAR_TYPE){
return REAL_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
}
// first type CHAR
else if(type_1 == CHAR_TYPE){
// second type INT or CHAR
if(type_2 == INT_TYPE || type_2 == CHAR_TYPE){
return CHAR_TYPE;
}
// second type REAL
else if(type_2 == REAL_TYPE){
return REAL_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
}
else{
type_error(type_1, type_2, op_type);
}
break;
...
The special case of INCR operator is very simple. We just return the type of the first parameter. The second type is non-existent and mostly UNDEF. That way we just have to write:...
case INCR_OP: /* special case of INCR */
// type INT
if(type_1 == INT_TYPE){
return INT_TYPE;
}
// type REAL
else if(type_1 == REAL_TYPE){
return REAL_TYPE;
}
// type CHAR
else if(type_1 == CHAR_TYPE){
return CHAR_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
break;
...
...
case BOOL_OP: /* Boolean operator */
// first type INT
if(type_1 == INT_TYPE){
// second type INT or CHAR
if(type_2 == INT_TYPE || type_2 == CHAR_TYPE){
return INT_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
}
// first type CHAR
else if(type_1 == CHAR_TYPE){
// second type INT or CHAR
if(type_2 == INT_TYPE || type_2 == CHAR_TYPE){
return CHAR_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
}
else{
type_error(type_1, type_2, op_type);
}
break;
...
The special case of NOT is similar to INCR. We just return the type of the first parameter. This means that the code is:...
case NOT_OP: /* special case of NOTOP */
// type INT
if(type_1 == INT_TYPE){
return INT_TYPE;
}
// type CHAR
else if(type_1 == CHAR_TYPE){
return INT_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
break;
...
...
case REL_OP: /* Relational operator */
// first type INT
if(type_1 == INT_TYPE){
// second type INT, REAL or CHAR
if(type_2 == INT_TYPE || type_2 == REAL_TYPE || type_2 == CHAR_TYPE){
return INT_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
}
else if(type_1 == REAL_TYPE){
// second type INT, REAL or CHAR
if(type_2 == INT_TYPE || type_2 == REAL_TYPE || type_2 == CHAR_TYPE){
return INT_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
}
// first type CHAR
else if(type_1 == CHAR_TYPE){
// second type INT, REAL or CHAR
if(type_2 == INT_TYPE || type_2 == REAL_TYPE || type_2 == CHAR_TYPE){
return INT_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
}
else{
type_error(type_1, type_2, op_type);
}
break;
...
...
case EQU_OP: /* Equality operator */
// first type INT
if(type_1 == INT_TYPE){
// second type INT or CHAR
if(type_2 == INT_TYPE || type_2 == CHAR_TYPE){
return INT_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
}
else if(type_1 == REAL_TYPE){
// second type REAL
if(type_2 == REAL_TYPE){
return INT_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
}
// first type CHAR
else if(type_1 == CHAR_TYPE){
// second type INT or CHAR
if(type_2 == INT_TYPE || type_2 == CHAR_TYPE){
return INT_TYPE;
}
else{
type_error(type_1, type_2, op_type);
}
}
else{
type_error(type_1, type_2, op_type);
}
break;
/* ---------------------------------------------------------- */
default: /* wrong choice case */
fprintf(stderr, "Error in operator selection!\n");
exit(1);
}
I would like to note that this type checking is of course not final at all! We just got into the general structure of "Type checking" and created the rules for most of the types! Of course Pointers need a special handling, that we will get into some articles later on. In arrays and functions we just use the info/data or return value, meaning that we use the types that we already covered!