All identifiers must be defined before they are used. This includes variables and functions. For example, this kind of code generates an error:
a = 3; // Error: 'a' not defined
int a;
Note: typenames such as "int"
, "float"
, and "char"
are built-in names that should be defined at the start of the program.
All literal symbols must be assigned a type of "int"
, "float"
, "char"
or "string"
.
For example:
42; // Type "int"
4.2; // Type "float"
'x'; // Type "char"
"forty"; // Type "string"
To do this assignment, check the Python type of the literal value and attach a type name as appropriate.
Binary operators only operate on operands of the same type and produce a result of the same type. Otherwise, you get a type error. For example:
int a = 2;
float b = 3.14;
int c = a + 3; // OK
int d = a + b; // Error: int + float
int e = b + 4.5; // Error: int = float
Unary operators return a result that's the same type as the operand.
Attempts to use unsupported operators should result in an error. For example:
char a[] = "Hello" + "World"; // OK
char b[] = "Hello" * "World"; // Error: unsupported op *
The left- and right-hand sides of an assignment operation must be declared as the same type. The size of objects must match. The index of an array must be of type int. Etc. See the examples below:
int v[4] = {1, 2, 3}; // Error: size mismatch on initialization
float f;
int j = v[f]; // Error: array index must be of type int
j = f; // Error: cannot assign float to int
However, string literals can be assigned to array of chars. See the example below:
char c[] = "Susy"; // Ok
In this case, the size of c
must be inferred from the initialization.
Your SSA (Static Single Assignment) code should only contain the following operators, represented as tuples of the form (operation, operands, ..., destination)
:
('alloc_type', varname) # Allocate on stack (ref by register) a variable of a given type
('global_type', varname, value) # Allocate on heap a global var of a given type. value is optional
('load_type', varname, target) # Load the value of a variable (stack/heap) into target (register)
('store_type', source, target) # Store the source/register into target/varname
('literal_type', value, target) # Load a literal value into target
('elem_type', source, index, target) # Load into target the address of source (array) indexed by index
('add_type', left, right, target) # target = left + right
('sub_type', left, right, target) # target = left - right
('mul_type', left, right, target) # target = left * right
('div_type', left, right, target) # target = left / right (integer truncation)
('mod_type', left, right, target) # target = left % right
('fptosi', fvalue, target) # target = (int)fvalue == cast float to int
('sitofp', ivalue, target) # target = (float)ivalue == cast int to float
(`oper`_type, left, right, target) # target = left `oper` right, where `oper` is:
# lt, le, ge, gt, eq, ne, and, or
('label:', ) # Label definition
('jump', target) # Jump to a target label
('cbranch', expr_test, true_target, false_target) # Conditional branch
('define', source) # Function definition. `source` is a function label
('end', ) # End of a function definition
('call', source, target) # Call a function. `target` is an optional return value
('return_type', target) # Return from function. `target` is an optional return value
('param_type', source) # `source` is an actual parameter
('read_type', source) # Read value to `source`
('print_type', source) # Print value of `source`
The dimensions of an array in uC are known at compile time. Thus, the type described in the allocation must express the dimension of the same. The initializer_list
are always allocated in the heap, either directly in the declaration of the variable, if it is global, or by defining a new temporary, based on the name of the local variable.
The allocation and operations with pointers in uC follow the same structure used for arrays. The exception is that reading the referenced value requires two instructions.