Merge remote-tracking branch 'origin/main'

This commit is contained in:
2022-10-05 11:47:04 +02:00
117 changed files with 24212 additions and 21 deletions
+77
View File
@@ -0,0 +1,77 @@
"""
This file defines the base class :py:class:`Allocator`
and the naïve implementation :py:class:`NaiveAllocator`.
"""
from Lib.Operands import Temporary, Operand, DataLocation, GP_REGS
from Lib.Statement import Instruction
from Lib.Errors import AllocationError
from Lib.FunctionData import FunctionData
from typing import Dict, List
class Allocator():
"""General base class for Naive, AllInMem and Smart Allocators.
Replace all temporaries in the code with actual data locations.
Allocation is done in two steps:
- First, :py:meth:`prepare` is responsible for calling
:py:meth:`Lib.Operands.TemporaryPool.set_temp_allocation`
with a mapping from temporaries to where they should actually be stored
(in registers or in memory).
- Then, :py:meth:`replace` is called for each instruction in order to
replace the temporary operands with the previously assigned locations
(and possibly add some instructions before or after).
Concretely, it returns a list of instructions that should replace the original
instruction. The actual iteration over all the instructions is handled transparently
by :py:meth:`Lib.LinearCode.LinearCode.iter_statements`.
"""
_fdata: FunctionData
def __init__(self, fdata: FunctionData):
self._fdata = fdata
def prepare(self) -> None: # pragma: no cover
pass
def replace(self, instr: Instruction) -> List[Instruction]:
"""Transform an instruction with temporaries into a list of instructions."""
return [instr]
def rewriteCode(self, listcode) -> None:
"""Modify the code to replace temporaries with
registers or memory locations.
"""
listcode.iter_statements(self.replace)
class NaiveAllocator(Allocator):
"""Naive Allocator: try to assign a register to each temporary,
fails if there are more temporaries than registers.
"""
def replace(self, old_instr: Instruction) -> List[Instruction]:
"""Replace Temporary operands with the corresponding allocated Register."""
subst: Dict[Operand, Operand] = {}
for arg in old_instr.args():
if isinstance(arg, Temporary):
subst[arg] = arg.get_alloced_loc()
new_instr = old_instr.substitute(subst)
return [new_instr]
def prepare(self) -> None:
"""Allocate all temporaries to registers.
Fail if there are too many temporaries."""
regs = list(GP_REGS) # Get a writable copy
temp_allocation: Dict[Temporary, DataLocation] = dict()
for tmp in self._fdata._pool.get_all_temps():
try:
reg = regs.pop()
except IndexError:
raise AllocationError(
"Too many temporaries ({}) for the naive allocation, sorry."
.format(len(self._fdata._pool.get_all_temps())))
temp_allocation[tmp] = reg
self._fdata._pool.set_temp_allocation(temp_allocation)
+179
View File
@@ -0,0 +1,179 @@
"""
This file defines the base class :py:class:`FunctionData`,
containing metadata on a RiscV function, as well as utility
functions common to the different intermediate representations.
"""
from typing import (List, Callable, TypeVar)
from Lib.Errors import AllocationError
from Lib.Operands import (
Offset, Temporary, TemporaryPool,
S, T, FP)
from Lib.Statement import (Statement, Instruction, Label, Comment)
class FunctionData:
"""
Stores some metadata on a RiscV function:
name of the function, label names, temporary variables
(using :py:class:`Lib.Operands.TemporaryPool`),
and div_by_zero label.
This class is usually used indirectly through the
different intermediate representations we work with,
such as :py:attr:`Lib.LinearCode.LinearCode.fdata`.
"""
_nblabel: int
_dec: int
_pool: TemporaryPool
_name: str
_label_div_by_zero: Label
def __init__(self, name: str):
self._nblabel = -1
self._dec = 0
self._pool = TemporaryPool()
self._name = name
self._label_div_by_zero = self.fresh_label("div_by_zero")
def get_name(self) -> str:
"""Return the name of the function."""
return self._name
def fresh_tmp(self) -> Temporary:
"""
Return a new fresh Temporary,
which is added to the pool.
"""
return self._pool.fresh_tmp()
def fresh_offset(self) -> Offset:
"""
Return a new offset in the memory stack.
Offsets are decreasing relative to FP.
"""
self._dec = self._dec + 1
# For ld or sd, an offset on 12 signed bits is expected
# Raise an error if the offset is too big
if -8 * self._dec < - 2 ** 11:
raise AllocationError(
"Offset given by the allocation too big to be manipulated ({}), sorry."
.format(self._dec))
return Offset(FP, -8 * self._dec)
def get_offset(self) -> int:
"""
Return the current offset in the memory stack.
"""
return self._dec
def _fresh_label_name(self, name) -> str:
"""
Return a new unique label name based on the given string.
"""
self._nblabel = self._nblabel + 1
return name + "_" + str(self._nblabel) + "_" + self._name
def fresh_label(self, name) -> Label:
"""
Return a new label, with a unique name based on the given string.
"""
return Label(self._fresh_label_name(name))
def get_label_div_by_zero(self) -> Label:
return self._label_div_by_zero
_T = TypeVar("_T", bound=Statement)
def _iter_statements(
listIns: List[_T], f: Callable[[_T], List[_T]]) -> List[_T | Comment]:
"""Iterate over instructions.
For each real instruction i (not label or comment), replace it
with the list of instructions given by f(i).
"""
newListIns: List[_T | Comment] = []
for old_i in listIns:
# Do nothing for label or comment
if not isinstance(old_i, Instruction):
newListIns.append(old_i)
continue
new_i_list = f(old_i)
# Otherwise, replace the instruction by the list
# returned by f, with comments giving the replacement
newListIns.append(Comment("Replaced " + str(old_i)))
newListIns.extend(new_i_list)
return newListIns
def _print_code(listIns: List, fdata: FunctionData, output,
init_label=None, fin_label=None, fin_div0=False, comment=None) -> None:
"""
Please use print_code from LinearCode or CFG, not directly this one.
Print the instructions from listIns, forming fdata, on output.
If init_label is given, add an initial jump to it before the generated code.
If fin_label is given, add it after the generated code.
If fin_div0 is given equal to true, add the code for returning an
error when dividing by 0, at the very end.
"""
# compute size for the local stack - do not forget to align by 16
fo = fdata.get_offset() # allocate enough memory for stack
cardoffset = 8 * (fo + (0 if fo % 2 == 0 else 1)) + 16
output.write(
"##Automatically generated RISCV code, MIF08 & CAP\n")
if comment is not None:
output.write("##{} version\n".format(comment))
output.write("\n\n##prelude\n")
# We put an li t0, cardoffset in case it is greater than 2**11
# We use t0 because it is caller-saved
output.write("""
.text
.globl {0}
{0}:
li t0, {1}
sub sp, sp, t0
sd ra, 0(sp)
sd fp, 8(sp)
add fp, sp, t0
""".format(fdata.get_name(), cardoffset))
# Stack in RiscV is managed with SP
if init_label is not None:
# Add a jump to init_label before the generated code.
output.write("""
j {0}
""".format(init_label))
output.write("\n\n##Generated Code\n")
# Generated code
for i in listIns:
i.printIns(output)
output.write("\n\n##postlude\n")
if fin_label is not None:
# Add fin_label after the generated code.
output.write("""
{0}:
""".format(fin_label))
# We put an li t0, cardoffset in case it is greater than 2**11
# We use t0 because it is caller-saved
output.write("""
ld ra, 0(sp)
ld fp, 8(sp)
li t0, {0}
add sp, sp, t0
ret
""".format(cardoffset))
if fin_div0:
# Add code for division by 0 at the end.
output.write("""
{0}:
la a0, {0}_msg
call println_string
li a0, 1
call exit
""".format(fdata._label_div_by_zero))
# Add the data for the message of the division by 0
output.write("""
{0}_msg: .string "Division by 0"
""".format(fdata._label_div_by_zero))
+101
View File
@@ -0,0 +1,101 @@
"""
CAP, CodeGeneration, LinearCode API
Classes for a RiscV linear code.
"""
from typing import List
from Lib.Operands import (A0, Function, DataLocation)
from Lib.Statement import (
Instru3A, AbsoluteJump, ConditionalJump, Comment, Label
)
from Lib.RiscV import (mv, call)
from Lib.FunctionData import (FunctionData, _iter_statements, _print_code)
CodeStatement = Comment | Label | Instru3A | AbsoluteJump | ConditionalJump
class LinearCode:
"""
Representation of a RiscV program as a list of instructions.
:py:meth:`add_instruction` is repeatedly called in the codegen visitor
to build a complete list of RiscV instructions for the source program.
The :py:attr:`fdata` member variable contains some meta-information
on the program, for instance to allocate a new temporary.
See :py:class:`Lib.FunctionData.FunctionData`.
For debugging purposes, :py:meth:`print_code` allows to print
the RiscV program to a file.
"""
"""
The :py:attr:`fdata` member variable contains some meta-information
on the program, for instance to allocate a new temporary.
See :py:class:`Lib.FunctionData.FunctionData`.
"""
fdata: FunctionData
_listIns: List[CodeStatement]
def __init__(self, name: str):
self._listIns = []
self.fdata = FunctionData(name)
def add_instruction(self, i: CodeStatement) -> None:
"""
Utility function to add an instruction in the program.
See also :py:mod:`Lib.RiscV` to generate relevant instructions.
"""
self._listIns.append(i)
def iter_statements(self, f) -> None:
"""Iterate over instructions.
For each real instruction (not label or comment), call f,
which must return either None or a list of instruction. If it
returns None, nothing happens. If it returns a list, then the
instruction is replaced by this list.
"""
self._listIns = _iter_statements(self._listIns, f)
def get_instructions(self) -> List[CodeStatement]:
"""Return the list of instructions of the program."""
return self._listIns
# each instruction has its own "add in list" version
def add_label(self, s: Label) -> None:
"""Add a label in the program."""
return self.add_instruction(s)
def add_comment(self, s: str) -> None:
"""Add a comment in the program."""
self.add_instruction(Comment(s))
def add_instruction_PRINTLN_INT(self, reg: DataLocation) -> None:
"""Print integer value, with newline. (see Expand)"""
# a print instruction generates the temp it prints.
self.add_instruction(mv(A0, reg))
self.add_instruction(call(Function('println_int')))
def __str__(self):
return '\n'.join(map(str, self._listIns))
def print_code(self, output, comment=None) -> None:
"""Outputs the RiscV program as text to a file at the given path."""
_print_code(self._listIns, self.fdata, output, init_label=None,
fin_label=None, fin_div0=True, comment=comment)
def print_dot(self, filename: str, DF=None, view=False) -> None: # pragma: no cover
"""Outputs the RiscV program as graph to a file at the given path."""
# import graphviz here so that students who don't have it can still work on lab4
from graphviz import Digraph
graph = Digraph()
# nodes
content = ""
for i in self._listIns:
content += str(i) + "\\l"
graph.node("Code", label=content, shape='rectangle')
# no edges
graph.render(filename, view=view)
+265
View File
@@ -0,0 +1,265 @@
"""
This file defines the base class :py:class:`Operand`
and its subclasses for different operands: :py:class:`Condition`,
:py:class:`DataLocation` and :py:class:`Function`.
The class :py:class:`DataLocation` itself has subclasses:
:py:class:`Register`, :py:class:`Offset` for address in memory,
:py:class:`Immediate` for constants and :py:class:`Temporary`
for location not yet allocated.
This file also define shortcuts for registers in RISCV.
"""
from typing import Dict, List
from MiniCParser import MiniCParser
from Lib.Errors import MiniCInternalError
class Operand():
pass
# signed version for riscv
all_ops = ['blt', 'bgt', 'beq', 'bne', 'ble', 'bge', 'beqz', 'bnez']
opdict = {MiniCParser.LT: 'blt', MiniCParser.GT: 'bgt',
MiniCParser.LTEQ: 'ble', MiniCParser.GTEQ: 'bge',
MiniCParser.NEQ: 'bne', MiniCParser.EQ: 'beq'}
opnot_dict = {'bgt': 'ble',
'bge': 'blt',
'blt': 'bge',
'ble': 'bgt',
'beq': 'bne',
'bne': 'beq',
'beqz': 'bnez',
'bnez': 'beqz'}
class Condition(Operand):
"""Condition, i.e. comparison operand for a CondJump.
Example usage :
- Condition('beq') = branch if equal.
- Condition(MiniCParser.LT) = branch if lower than.
- ...
The constructor's argument shall be a string in the list all_ops, or a
comparison operator in MiniCParser.LT, MiniCParser.GT, ... (one of the keys
in opdict).
A 'negate' method allows getting the negation of this condition.
"""
_op: str
def __init__(self, optype):
if optype in opdict:
self._op = opdict[optype]
elif str(optype) in all_ops:
self._op = str(optype)
else:
raise MiniCInternalError("Unsupported comparison operator %s", optype)
def negate(self) -> 'Condition':
"""Return the opposite condition."""
return Condition(opnot_dict[self._op])
def __str__(self):
return self._op
class Function(Operand):
"""Operand for build-in function call."""
_name: str
def __init__(self, name: str):
self._name = name
def __str__(self):
return self._name
class DataLocation(Operand):
""" A Data Location is either a register, a temporary
or a place in memory (offset).
"""
pass
# map for register shortcuts
reg_map = dict([(0, 'zero'), (1, 'ra'), (2, 'sp')] + # no (3, 'gp') nor (4, 'tp')
[(i+5, 't'+str(i)) for i in range(3)] +
[(8, 'fp'), (9, 's1')] +
[(i+10, 'a'+str(i)) for i in range(8)] +
[(i+18, 's'+str(i+2)) for i in range(10)] +
[(i+28, 't'+str(i+3)) for i in range(4)])
class Register(DataLocation):
""" A (physical) register."""
_number: int
def __init__(self, number: int):
self._number = number
def __repr__(self):
if self._number not in reg_map:
raise Exception("Register number %d should not be used", self._number)
else:
return ("{}".format(reg_map[self._number]))
def __eq__(self, other):
return isinstance(other, Register) and self._number == other._number
def __hash__(self):
return self._number
# Shortcuts for registers in RISCV
# Only integer registers
ZERO = Register(0)
RA = Register(1)
SP = Register(2)
GP = Register(3) # Register not used for this course
TP = Register(4) # Register not used for this course
A = tuple(Register(i + 10) for i in range(8))
S = tuple(Register(i + 8) for i in range(2)) + tuple(Register(i + 18) for i in range(10))
T = tuple(Register(i + 5) for i in range(3)) + tuple(Register(i + 28) for i in range(4))
A0 = A[0] # function args/return Values: A0, A1
A1 = A[1]
FP = S[0] # Frame Pointer = Saved register 0
# General purpose registers, usable for the allocator
GP_REGS = S[4:] + T # s0, s1, s2 and s3 are special
class Offset(DataLocation):
""" Offset = address in memory computed with base + offset."""
_basereg: Register
_offset: int
def __init__(self, basereg: Register, offset: int):
self._basereg = basereg
self._offset = offset
def __repr__(self):
return ("{}({})".format(self._offset, self._basereg))
def get_offset(self) -> int:
"""Return the value of the offset."""
return self._offset
class Immediate(DataLocation):
"""Immediate operand (integer)."""
_val: int
def __init__(self, val):
self._val = val
def __str__(self):
return str(self._val)
class Temporary(DataLocation):
"""Temporary, a location that has not been allocated yet.
It will later be mapped to a physical register (Register) or to a memory location (Offset).
"""
_number: int
_pool: 'TemporaryPool'
def __init__(self, number: int, pool: 'TemporaryPool'):
self._number = number
self._pool = pool
def __repr__(self):
return ("temp_{}".format(str(self._number)))
def get_alloced_loc(self) -> DataLocation:
"""Return the DataLocation allocated to this Temporary."""
return self._pool.get_alloced_loc(self)
class TemporaryPool:
"""Manage a pool of temporaries."""
_all_temps: List[Temporary]
_current_num: int
_allocation: Dict[Temporary, DataLocation]
def __init__(self):
self._all_temps = []
self._current_num = 0
self._allocation = dict()
def get_all_temps(self) -> List[Temporary]:
"""Return all the temporaries of the pool."""
return self._all_temps
def get_alloced_loc(self, t: Temporary) -> DataLocation:
"""Get the actual DataLocation allocated for the temporary t."""
return self._allocation[t]
def add_tmp(self, t: Temporary):
"""Add a temporary to the pool."""
self._all_temps.append(t)
self._allocation[t] = t # While no allocation, return the temporary itself
def set_temp_allocation(self, allocation: Dict[Temporary, DataLocation]) -> None:
"""Give a mapping from temporaries to actual registers.
The argument allocation must be a dict from Temporary to
DataLocation other than Temporary (typically Register or Offset).
Typing enforces that keys are Temporary and values are Datalocation.
We check the values are indeed not Temporary.
"""
for v in allocation.values():
assert not isinstance(v, Temporary), (
"Incorrect allocation scheme: value " +
str(v) + " is a Temporary.")
self._allocation = allocation
def fresh_tmp(self) -> Temporary:
"""Give a new fresh Temporary and add it to the pool."""
t = Temporary(self._current_num, self)
self._current_num += 1
self.add_tmp(t)
return t
class Renamer:
"""Manage a renaming of temporaries."""
_pool: TemporaryPool
_env: Dict[Temporary, Temporary]
def __init__(self, pool: TemporaryPool):
self._pool = pool
self._env = dict()
def fresh(self, t: Temporary) -> Temporary:
"""Give a fresh rename for a Temporary."""
new_t = self._pool.fresh_tmp()
self._env[t] = new_t
return new_t
def replace(self, t: Temporary) -> Temporary:
"""Give the rename for a Temporary (which is itself if it is not renamed)."""
return self._env.get(t, t)
def defined(self, t: Temporary) -> bool:
"""True if the Temporary is renamed."""
return t in self._env
def copy(self):
"""Give a copy of the Renamer."""
r = Renamer(self._pool)
r._env = self._env.copy()
return r
+89
View File
@@ -0,0 +1,89 @@
"""
MIF08, CAP, CodeGeneration, RiscV API
Functions to define instructions.
"""
from Lib.Errors import MiniCInternalError
from Lib.Operands import (
Condition, Immediate, Operand, Function, ZERO)
from Lib.Statement import (Instru3A, AbsoluteJump, ConditionalJump, Label)
def call(function: Function) -> Instru3A:
"""Function call."""
return Instru3A('call', function)
def jump(label: Label) -> AbsoluteJump:
"""Unconditional jump to label."""
return AbsoluteJump(label)
def conditional_jump(label: Label, op1: Operand, cond: Condition, op2: Operand):
"""Add a conditional jump to the code.
This is a wrapper around bge, bgt, beq, ... c is a Condition, like
Condition('bgt'), Condition(MiniCParser.EQ), ...
"""
op2 = op2 if op2 != Immediate(0) else ZERO
return ConditionalJump(cond=cond, op1=op1, op2=op2, label=label)
def add(dr: Operand, sr1: Operand, sr2orimm7: Operand) -> Instru3A:
if isinstance(sr2orimm7, Immediate):
return Instru3A("addi", dr, sr1, sr2orimm7)
else:
return Instru3A("add", dr, sr1, sr2orimm7)
def mul(dr: Operand, sr1: Operand, sr2orimm7: Operand) -> Instru3A:
if isinstance(sr2orimm7, Immediate):
raise MiniCInternalError("Cant multiply by an immediate")
else:
return Instru3A("mul", dr, sr1, sr2orimm7)
def div(dr: Operand, sr1: Operand, sr2orimm7: Operand) -> Instru3A:
if isinstance(sr2orimm7, Immediate):
raise MiniCInternalError("Cant divide by an immediate")
else:
return Instru3A("div", dr, sr1, sr2orimm7)
def rem(dr: Operand, sr1: Operand, sr2orimm7: Operand) -> Instru3A:
if isinstance(sr2orimm7, Immediate):
raise MiniCInternalError("Cant divide by an immediate")
return Instru3A("rem", dr, sr1, sr2orimm7)
def sub(dr: Operand, sr1: Operand, sr2orimm7: Operand) -> Instru3A:
if isinstance(sr2orimm7, Immediate):
raise MiniCInternalError("Cant substract by an immediate")
return Instru3A("sub", dr, sr1, sr2orimm7)
def land(dr: Operand, sr1: Operand, sr2orimm7: Operand) -> Instru3A:
return Instru3A("and", dr, sr1, sr2orimm7)
def lor(dr: Operand, sr1: Operand, sr2orimm7: Operand) -> Instru3A:
return Instru3A("or", dr, sr1, sr2orimm7)
def xor(dr: Operand, sr1: Operand, sr2orimm7: Operand) -> Instru3A: # pragma: no cover
return Instru3A("xor", dr, sr1, sr2orimm7)
def li(dr: Operand, imm7: Immediate) -> Instru3A:
return Instru3A("li", dr, imm7)
def mv(dr: Operand, sr: Operand) -> Instru3A:
return Instru3A("mv", dr, sr)
def ld(dr: Operand, mem: Operand) -> Instru3A:
return Instru3A("ld", dr, mem)
def sd(sr: Operand, mem: Operand) -> Instru3A:
return Instru3A("sd", sr, mem)
+242
View File
@@ -0,0 +1,242 @@
"""
The base class for RISCV ASM statements is :py:class:`Statement`.
It is inherited by :py:class:`Comment`, :py:class:`Label`
and :py:class:`Instruction`. In turn, :py:class:`Instruction`
is inherited by :py:class:`Instru3A`
(for regular non-branching 3-address instructions),
:py:class:`AbsoluteJump` and :py:class:`ConditionalJump`.
"""
from dataclasses import dataclass
from typing import (List, Dict, TypeVar)
from Lib.Operands import (Operand, Renamer, Temporary, Condition)
from Lib.Errors import MiniCInternalError
def regset_to_string(registerset):
"""Utility function: pretty-prints a set of locations."""
return "{" + ",".join(str(x) for x in registerset) + "}"
# Temporary until we can use Typing.Self in python 3.11
TStatement = TypeVar("TStatement", bound="Statement")
@dataclass(unsafe_hash=True)
class Statement:
"""A Statement, which is an instruction, a comment or a label."""
def defined(self) -> List[Operand]:
return []
def used(self) -> List[Operand]:
return []
def substitute(self: TStatement, subst: Dict[Operand, Operand]) -> TStatement:
raise Exception(
"substitute: Operands {} are not present in instruction {}"
.format(subst, self))
def printIns(self, stream):
"""
Print the statement on the output.
Should never be called on the base class.
"""
raise NotImplementedError
@dataclass(unsafe_hash=True)
class Comment(Statement):
"""A comment."""
comment: str
def __str__(self): # use only for print_dot !
return "# {}".format(self.comment)
def printIns(self, stream):
print(' # ' + self.comment, file=stream)
@dataclass(unsafe_hash=True)
class Label(Statement, Operand):
"""A label is both a Statement and an Operand."""
name: str
def __str__(self):
return ("lbl_{}".format(self.name))
def __repr__(self):
return ("{}".format(self.name))
def printIns(self, stream):
print(str(self) + ':', file=stream)
@dataclass(init=False)
class Instruction(Statement):
ins: str
_read_only: bool
def is_read_only(self):
"""
True if the instruction only reads from its operands.
Otherwise, the first operand is considered as the destination
and others are source.
"""
return self._read_only
def rename(self, renamer: Renamer) -> None:
raise NotImplementedError
def args(self) -> List[Operand]:
raise NotImplementedError
def defined(self):
if self.is_read_only():
defs = []
else:
defs = [self.args()[0]]
return defs
def used(self):
if self.is_read_only():
uses = self.args()
else:
uses = self.args()[1:]
return uses
def __str__(self):
s = self.ins
first = True
for arg in self.args():
if first:
s += ' ' + str(arg)
first = False
else:
s += ', ' + str(arg)
return s
def __hash__(self):
return hash((self.ins, *self.args()))
def printIns(self, stream):
"""Print the instruction on the output."""
print(' ', str(self), file=stream)
@dataclass(init=False)
class Instru3A(Instruction):
_args: List[Operand]
def __init__(self, ins, *args: Operand):
# convention is to use lower-case in RISCV
self.ins = ins.lower()
self._args = list(args)
self._read_only = (self.ins == "call"
or self.ins == "ld"
or self.ins == "lw"
or self.ins == "lb")
if (self.ins.startswith("b") or self.ins == "j"):
raise MiniCInternalError
def args(self):
return self._args
def rename(self, renamer: Renamer):
old_replaced = dict()
for i, arg in enumerate(self._args):
if isinstance(arg, Temporary):
if i == 0 and not self.is_read_only():
old_replaced[arg] = renamer.replace(arg)
new_t = renamer.fresh(arg)
elif arg in old_replaced.keys():
new_t = old_replaced[arg]
else:
new_t = renamer.replace(arg)
self._args[i] = new_t
def substitute(self, subst: Dict[Operand, Operand]):
for op in subst:
if op not in self.args():
raise Exception(
"substitute: Operand {} is not present in instruction {}"
.format(op, self))
args = [subst.get(arg, arg)
if isinstance(arg, Temporary) else arg
for arg in self.args()]
return Instru3A(self.ins, *args)
def __hash__(self):
return hash(super)
@dataclass(init=False)
class AbsoluteJump(Instruction):
""" An Absolute Jump is a specific kind of instruction"""
ins = "j"
label: Label
_read_only = True
def __init__(self, label: Label):
self.label = label
def args(self):
return [self.label]
def rename(self, renamer: Renamer):
pass
def substitute(self, subst: Dict[Operand, Operand]):
if subst != {}:
raise Exception(
"substitute: No possible substitution on instruction {}"
.format(self))
return self
def __hash__(self):
return hash(super)
def targets(self) -> List[Label]:
return [self.label]
@dataclass(init=False)
class ConditionalJump(Instruction):
""" A Conditional Jump is a specific kind of instruction"""
cond: Condition
label: Label
op1: Operand
op2: Operand
_read_only = True
def __init__(self, cond: Condition, op1: Operand, op2: Operand, label: Label):
self.cond = cond
self.label = label
self.op1 = op1
self.op2 = op2
self.ins = str(self.cond)
def args(self):
return [self.op1, self.op2, self.label]
def rename(self, renamer: Renamer):
if isinstance(self.op1, Temporary):
self.op1 = renamer.replace(self.op1)
if isinstance(self.op2, Temporary):
self.op2 = renamer.replace(self.op2)
def substitute(self, subst: Dict[Operand, Operand]):
for op in subst:
if op not in self.args():
raise Exception(
"substitute: Operand {} is not present in instruction {}"
.format(op, self))
op1 = subst.get(self.op1, self.op1) if isinstance(self.op1, Temporary) \
else self.op1
op2 = subst.get(self.op2, self.op2) if isinstance(self.op2, Temporary) \
else self.op2
return ConditionalJump(self.cond, op1, op2, self.label)
def __hash__(self):
return hash(super)
View File
+12 -9
View File
@@ -9,12 +9,9 @@ ifdef TEST_FILES
export TEST_FILES
endif
ifdef SSA
MINICC_OPTS+=--ssa
endif
ifdef SSA_OPTIM
MINICC_OPTS+=--ssa-optim
# code generation mode
ifdef MODE
MINICC_OPTS+=--mode $(MODE)
endif
ifdef TYPECHECK_ONLY
@@ -45,8 +42,8 @@ main-deps: MiniCLexer.py MiniCParser.py TP03/MiniCInterpretVisitor.py TP03/MiniC
.PHONY: test test-interpret test-codegen clean clean-tests tar antlr
test: test-interpret
test: test-interpret test-codegen
test-pyright: antlr
pyright .
@@ -57,10 +54,16 @@ test-interpret: test-pyright test_interpreter.py main-deps
# Test for naive allocator (also runs test_expect to check // EXPECTED directives):
test-naive: test-pyright antlr
ifndef MODE
export MINICC_OPTS="${MINICC_OPTS} --mode codegen-linear"
endif
python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py -k 'naive or expect'
# Test for all but the smart allocator, i.e. everything that lab4 should pass:
test-notsmart: test-pyright antlr
test-lab4: test-pyright antlr
ifndef MODE
export MINICC_OPTS="${MINICC_OPTS} --mode codegen-linear"
endif
python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py -k 'not smart'
# Test just the smart allocator (quicker than tests)
@@ -83,7 +86,7 @@ define CLEAN
import glob
import os
for f in glob.glob("**/tests/**/*.c", recursive=True):
for s in ("{}-{}.s".format(f[:-2], test) for test in ("naive", "smart", "gcc", "all_in_mem")):
for s in ("{}-{}.s".format(f[:-2], test) for test in ("naive", "smart", "gcc", "all-in-mem")):
try:
os.remove(s)
print("Removed {}".format(s))
+12 -12
View File
@@ -1,8 +1,8 @@
#! /usr/bin/env python3
"""
Code generation lab, main file. Code Generation with Smart IRs.
Evaluation and code generation labs, main file.
Usage:
python3 MiniCC.py <filename>
python3 MiniCC.py --mode <mode> <filename>
python3 MiniCC.py --help
"""
import traceback
@@ -58,7 +58,7 @@ def valid_modes():
return modes
try:
import TP05c.OptimSSA # type: ignore[import]
import TPoptim.OptimSSA # type: ignore[import]
modes.append('codegen-optim')
except ImportError:
pass
@@ -146,9 +146,9 @@ def main(inputname, reg_alloc, mode,
from TP04.BuildCFG import build_cfg # type: ignore[import]
from Lib.CFG import CFG # type: ignore[import]
code = build_cfg(function)
assert(isinstance(code, CFG))
assert (isinstance(code, CFG))
if debug_graphs:
s = "{}.{}.dot".format(basename, code.fdata._name)
s = "{}.{}.dot".format(basename, code.fdata.get_name())
print("CFG:", s)
code.print_dot(s, view=True)
if mode.value >= Mode.SSA.value:
@@ -157,14 +157,14 @@ def main(inputname, reg_alloc, mode,
DF = enter_ssa(cast(CFG, code), basename, debug, ssa_graphs)
if ssa_graphs:
s = "{}.{}.ssa.dot".format(basename, code.fdata._name)
s = "{}.{}.ssa.dot".format(basename, code.fdata.get_name())
print("SSA:", s)
code.print_dot(s, DF, True)
if mode == Mode.OPTIM:
from TP05c.OptimSSA import OptimSSA # type: ignore[import]
from TPoptim.OptimSSA import OptimSSA # type: ignore[import]
OptimSSA(cast(CFG, code), debug=debug)
if ssa_graphs:
s = "{}.{}.optimssa.dot".format(basename, code.fdata._name)
s = "{}.{}.optimssa.dot".format(basename, code.fdata.get_name())
print("SSA after optim:", s)
code.print_dot(s, view=True)
allocator = None
@@ -178,7 +178,7 @@ def main(inputname, reg_alloc, mode,
comment = "all-in-memory allocation"
elif reg_alloc == "smart":
liveness = None
if mode == Mode.SSA:
if mode.value >= Mode.SSA.value:
from TP05.LivenessSSA import LivenessSSA # type: ignore[import]
try:
from Lib.CFG import CFG # type: ignore[import]
@@ -205,15 +205,15 @@ liveness file not found for {}.".format(form))
raise ValueError("Invalid allocation strategy:" + reg_alloc)
if allocator:
allocator.prepare()
if mode == Mode.SSA:
if mode.value >= Mode.SSA.value:
from Lib.CFG import CFG # type: ignore[import]
from TP05.SSA import exit_ssa # type: ignore[import]
exit_ssa(cast(CFG, code))
comment += " with SSA"
if allocator:
allocator.rewriteCode(code)
if mode == Mode.SSA and ssa_graphs:
s = "{}.{}.exitssa.dot".format(basename, code.fdata._name)
if mode.value >= Mode.SSA.value and ssa_graphs:
s = "{}.{}.exitssa.dot".format(basename, code.fdata.get_name())
print("CFG after SSA:", s)
code.print_dot(s, view=True)
code.print_code(output, comment=comment)
+57
View File
@@ -0,0 +1,57 @@
# MiniC Compiler
LAB4 (simple code generation), MIF08 / CAP 2022-23
# Authors
YOUR NAME HERE
# Contents
TODO for STUDENTS : Say a bit about the code infrastructure ...
# Test design
TODO: explain your tests
# Design choices
TODO: explain your choices. How did you implement boolean not? Did you implement an extension?
# Known bugs
TODO: Bugs and limitations.
# Checklists
A check ([X]) means that the feature is implemented
and *tested* with appropriate test cases.
## Code generation
- [ ] Number Atom
- [ ] Boolean Atom
- [ ] Id Atom
- [ ] Additive expression
- [ ] Multiplicative expression
- [ ] UnaryMinus expression
- [ ] Or expression
- [ ] And expression
- [ ] Equality expression
- [ ] Relational expression (! many cases -> many tests)
- [ ] Not expression
## Statements
- [ ] Prog, assignements
- [ ] While
- [ ] Cond Block
- [ ] If
- [ ] Nested ifs
- [ ] Nested whiles
## Allocation
- [ ] Naive allocation
- [ ] All in memory allocation
- [ ] Massive tests of memory allocation
+33
View File
@@ -0,0 +1,33 @@
from Lib import RiscV
from Lib.Operands import Temporary, Operand, S
from Lib.Statement import Instruction
from Lib.Allocator import Allocator
from typing import List, Dict
class AllInMemAllocator(Allocator):
def replace(self, old_instr: Instruction) -> List[Instruction]:
"""Replace Temporary operands with the corresponding allocated
memory location. FP points to the stack."""
numreg = 1
before: List[Instruction] = []
after: List[Instruction] = []
subst: Dict[Operand, Operand] = {}
# TODO (Exercise 7): compute before,after,args.
# TODO (Exercise 7): iterate over old_args, check which argument
# TODO (Exercise 7): is a temporary (e.g. isinstance(..., Temporary)),
# TODO (Exercise 7): and if so, generate ld/sd accordingly. Replace the
# TODO (Exercise 7): temporary with S[1], S[2] or S[3] physical registers.
new_instr = old_instr.substitute(subst)
return before + [new_instr] + after
def prepare(self):
"""Allocate all temporaries to memory.
Invariants:
- Expanded instructions can use s2 and s3
(to store the values of temporaries before the actual instruction).
"""
self._fdata._pool.set_temp_allocation(
{temp: self._fdata.fresh_offset()
for temp in self._fdata._pool.get_all_temps()})
+194
View File
@@ -0,0 +1,194 @@
from typing import List, Tuple
from MiniCVisitor import MiniCVisitor
from MiniCParser import MiniCParser
from Lib.LinearCode import LinearCode
from Lib import RiscV
from Lib.RiscV import Condition
from Lib import Operands
from antlr4.tree.Trees import Trees
from Lib.Errors import MiniCInternalError, MiniCUnsupportedError
"""
CAP, MIF08, three-address code generation + simple alloc
This visitor constructs an object of type "LinearCode".
"""
class MiniCCodeGen3AVisitor(MiniCVisitor):
_current_function: LinearCode
def __init__(self, debug, parser):
super().__init__()
self._parser = parser
self._debug = debug
self._functions = []
self._lastlabel = ""
def get_functions(self) -> List[LinearCode]:
return self._functions
def printSymbolTable(self): # pragma: no cover
print("--variables to temporaries map--")
for keys, values in self._symbol_table.items():
print(keys + '-->' + str(values))
# handle variable decl
def visitVarDecl(self, ctx) -> None:
type_str = ctx.typee().getText()
vars_l = self.visit(ctx.id_l())
for name in vars_l:
if name in self._symbol_table:
raise MiniCInternalError(
"Variable {} has already been declared".format(name))
else:
tmp = self._current_function.fdata.fresh_tmp()
self._symbol_table[name] = tmp
if type_str not in ("int", "bool"):
raise MiniCUnsupportedError("Unsupported type " + type_str)
# Initialization to 0 or False, both represented with 0
self._current_function.add_instruction(
RiscV.li(tmp, Operands.Immediate(0)))
def visitIdList(self, ctx) -> Operands.Temporary:
t = self.visit(ctx.id_l())
t.append(ctx.ID().getText())
return t
def visitIdListBase(self, ctx) -> List[str]:
return [ctx.ID().getText()]
# expressions
def visitParExpr(self, ctx) -> Operands.Temporary:
return self.visit(ctx.expr())
def visitIntAtom(self, ctx) -> Operands.Temporary:
val = Operands.Immediate(int(ctx.getText()))
dest_temp = self._current_function.fdata.fresh_tmp()
self._current_function.add_instruction(RiscV.li(dest_temp, val))
return dest_temp
def visitFloatAtom(self, ctx) -> Operands.Temporary:
raise MiniCUnsupportedError("float literal")
def visitBooleanAtom(self, ctx) -> Operands.Temporary:
# true is 1 false is 0
raise NotImplementedError() # TODO (Exercise 5)
def visitIdAtom(self, ctx) -> Operands.Temporary:
try:
# get the temporary associated to id
return self._symbol_table[ctx.getText()]
except KeyError: # pragma: no cover
raise MiniCInternalError(
"Undefined variable {}, this should have failed to typecheck."
.format(ctx.getText())
)
def visitStringAtom(self, ctx) -> Operands.Temporary:
raise MiniCUnsupportedError("string atom")
# now visit expressions
def visitAtomExpr(self, ctx) -> Operands.Temporary:
return self.visit(ctx.atom())
def visitAdditiveExpr(self, ctx) -> Operands.Temporary:
assert ctx.myop is not None
raise NotImplementedError() # TODO (Exercise 2)
def visitOrExpr(self, ctx) -> Operands.Temporary:
raise NotImplementedError() # TODO (Exercise 5)
def visitAndExpr(self, ctx) -> Operands.Temporary:
raise NotImplementedError() # TODO (Exercise 5)
def visitEqualityExpr(self, ctx) -> Operands.Temporary:
return self.visitRelationalExpr(ctx)
def visitRelationalExpr(self, ctx) -> Operands.Temporary:
assert ctx.myop is not None
c = Condition(ctx.myop.type)
if self._debug:
print("relational expression:")
print(Trees.toStringTree(ctx, None, self._parser))
print("Condition:", c)
raise NotImplementedError() # TODO (Exercise 5)
def visitMultiplicativeExpr(self, ctx) -> Operands.Temporary:
assert ctx.myop is not None
div_by_zero_lbl = self._current_function.fdata.get_label_div_by_zero()
raise NotImplementedError() # TODO (Exercise 8)
def visitNotExpr(self, ctx) -> Operands.Temporary:
raise NotImplementedError() # TODO (Exercise 5)
def visitUnaryMinusExpr(self, ctx) -> Operands.Temporary:
raise NotImplementedError("unaryminusexpr") # TODO (Exercise 2)
def visitProgRule(self, ctx) -> None:
self.visitChildren(ctx)
def visitFuncDef(self, ctx) -> None:
funcname = ctx.ID().getText()
self._current_function = LinearCode(funcname)
self._symbol_table = dict()
self.visit(ctx.vardecl_l())
self.visit(ctx.block())
self._current_function.add_comment("Return at end of function:")
# This skeleton doesn't deal properly with functions, and
# hardcodes a "return 0;" at the end of function. Generate
# code for this "return 0;".
self._current_function.add_instruction(
RiscV.li(Operands.A0, Operands.Immediate(0)))
self._functions.append(self._current_function)
del self._current_function
def visitAssignStat(self, ctx) -> None:
if self._debug:
print("assign statement, rightexpression is:")
print(Trees.toStringTree(ctx.expr(), None, self._parser))
expr_temp = self.visit(ctx.expr())
name = ctx.ID().getText()
self._current_function.add_instruction(RiscV.mv(self._symbol_table[name], expr_temp))
def visitIfStat(self, ctx) -> None:
if self._debug:
print("if statement")
end_if_label = self._current_function.fdata.fresh_label("end_if")
raise NotImplementedError() # TODO (Exercise 5)
self._current_function.add_label(end_if_label)
def visitWhileStat(self, ctx) -> None:
if self._debug:
print("while statement, condition is:")
print(Trees.toStringTree(ctx.expr(), None, self._parser))
print("and block is:")
print(Trees.toStringTree(ctx.stat_block(), None, self._parser))
raise NotImplementedError() # TODO (Exercise 5)
# visit statements
def visitPrintlnintStat(self, ctx) -> None:
expr_loc = self.visit(ctx.expr())
if self._debug:
print("print_int statement, expression is:")
print(Trees.toStringTree(ctx.expr(), None, self._parser))
self._current_function.add_instruction_PRINTLN_INT(expr_loc)
def visitPrintlnboolStat(self, ctx) -> None:
expr_loc = self.visit(ctx.expr())
self._current_function.add_instruction_PRINTLN_INT(expr_loc)
def visitPrintlnfloatStat(self, ctx) -> None:
raise MiniCUnsupportedError("Unsupported type float")
def visitPrintlnstringStat(self, ctx) -> None:
raise MiniCUnsupportedError("Unsupported type string")
def visitStatList(self, ctx) -> None:
for stat in ctx.stat():
self._current_function.add_comment(Trees.toStringTree(stat, None, self._parser))
self.visit(stat)
+9
View File
@@ -0,0 +1,9 @@
#include "printlib.h"
int main() {
println_int(42);
return 0;
}
// EXPECTED
// 42
+11
View File
@@ -0,0 +1,11 @@
#include "printlib.h"
int main() {
int x,y;
x=4;
y=12+x;
return 0;
}
// EXPECTED
+11
View File
@@ -0,0 +1,11 @@
#include "printlib.h"
int main() {
int a,n;
n=1;
a=n+12;
return 0;
}
// EXPECTED
+11
View File
@@ -0,0 +1,11 @@
#include "printlib.h"
int main() {
int n;
n=6;
return 0;
}
// EXPECTED
@@ -0,0 +1,11 @@
#include "printlib.h"
int main() {
println_int(43);
return 0;
}
// EXPECTED
// 43
@@ -0,0 +1,12 @@
#include "printlib.h"
int main() {
int x;
x = 42;
println_int(x);
return 0;
}
// EXPECTED
// 42
@@ -0,0 +1,16 @@
#include "printlib.h"
int main() {
int x;
x = 42;
println_int(x + x);
println_int(x + 1);
println_int(1 + x);
return 0;
}
// EXPECTED
// 84
// 43
// 43
@@ -0,0 +1,20 @@
#include "printlib.h"
int main() {
int x, y;
x = 42;
y = 66;
println_int(x + y);
x = 1;
println_int(x + y);
y = 2;
println_int(x + y);
return 0;
}
// EXPECTED
// 108
// 67
// 3
+16
View File
@@ -0,0 +1,16 @@
#include "printlib.h"
int main() {
int x,y;
x = 9;
if (x < 2)
y=7;
else
y=12;
x = y;
return 0;
}
// EXPECTED
+13
View File
@@ -0,0 +1,13 @@
#include "printlib.h"
int main() {
int n;
bool a,b;
n=1;
a=true;
b=(a==(n<6));
return 0;
}
// EXPECTED
+13
View File
@@ -0,0 +1,13 @@
#include "printlib.h"
int main() {
int x,y;
x=3;
if (x<5) {
y=x+1;
}
return 0;
}
// EXPECTED
+16
View File
@@ -0,0 +1,16 @@
#include "printlib.h"
int main() {
int x,y,z;
x=2;
if (x<3) {
y=7;
} else {
y=8;
}
z=y+1;
return 0;
}
// EXPECTED
+20
View File
@@ -0,0 +1,20 @@
#include "printlib.h"
int main() {
int x,y,z,u;
x=3;
if (x < 4) {
z=4;
}
else if ( x < 5) {
z=5;
}
else {
z=6 ;
}
u=z+1;
return 0;
}
// EXPECTED
@@ -0,0 +1,19 @@
#include "printlib.h"
int main() {
bool b;
b = false;
println_bool(b);
b = true;
println_bool(b);
println_bool(true);
println_bool(false);
return 0;
}
// EXPECTED
// 0
// 1
// 1
// 0
@@ -0,0 +1,10 @@
#include "printlib.h"
int main() {
println_bool(3 >= 2);
return 0;
}
// EXPECTED
// 1
@@ -0,0 +1,19 @@
#include "printlib.h"
int main() {
if (10 == 10) {
println_int(12);
} else if (10 == 10) {
println_int(15);
} else {
println_int(13);
}
println_int(14);
return 0;
}
// EXPECTED
// 12
// 14
@@ -0,0 +1,25 @@
#include "printlib.h"
int main() {
int n;
n = 9;
while (n > 0) {
n = n-1 ;
println_int(n) ;
}
return 0;
}
// EXPECTED
// 8
// 7
// 6
// 5
// 4
// 3
// 2
// 1
// 0
+18
View File
@@ -0,0 +1,18 @@
#include "printlib.h"
int main() {
int a,n;
n = 1;
a = 7;
while (n < a) {
n = n+1;
}
println_int(n);
return 0;
}
// EXPECTED
// 7
@@ -0,0 +1,9 @@
int main() {
float f;
return 0;
}
// SKIP TEST EXPECTED
// EXITCODE 5
// EXPECTED
// Unsupported type float
@@ -0,0 +1,9 @@
int main() {
println_float(0.0);
return 0;
}
// SKIP TEST EXPECTED
// EXITCODE 5
// EXPECTED
// Unsupported type float
@@ -0,0 +1,9 @@
int main() {
println_string("Hello");
return 0;
}
// SKIP TEST EXPECTED
// EXITCODE 5
// EXPECTED
// Unsupported type string
@@ -0,0 +1,9 @@
int main() {
string b;
return 0;
}
// SKIP TEST EXPECTED
// EXITCODE 5
// EXPECTED
// Unsupported type string
+1
View File
@@ -0,0 +1 @@
Add your own tests in this directory.
Binary file not shown.
+241
View File
@@ -0,0 +1,241 @@
#! /usr/bin/env python3
import os
import sys
import pytest
import glob
import subprocess
import re
from test_expect_pragma import (
TestExpectPragmas, cat, testinfo, env_str_variable
)
"""
Usage:
python3 test_codegen.py
(or make test)
"""
"""
MIF08 and CAP, 2019
Unit test infrastructure for testing code generation:
1) compare the actual output to the expected one (in comments)
2) compare the actual output to the one obtained by simulation
3) for different allocation algorithms
"""
MINICC_OPTS = []
if "MINICC_OPTS" in os.environ and os.environ["MINICC_OPTS"]:
MINICC_OPTS = os.environ["MINICC_OPTS"].split()
else:
MINICC_OPTS = ["--mode=codegen-cfg"]
DISABLE_TYPECHECK = "--disable-typecheck" in MINICC_OPTS
HERE = os.path.dirname(os.path.realpath(__file__))
if HERE == os.path.realpath('.'):
HERE = '.'
TEST_DIR = HERE
IMPLEM_DIR = HERE
MINIC_COMPILE = os.path.join(IMPLEM_DIR, 'MiniCC.py')
ALL_FILES = glob.glob(os.path.join(TEST_DIR, 'TP04/tests/**/[a-zA-Z]*.c'), recursive=True)
ALLOC_FILES = glob.glob(os.path.join(HERE, 'TP05/tests/**/*.c'), recursive=True)
ASM = 'riscv64-unknown-elf-gcc'
SIMU = 'spike'
SKIP_NOT_IMPLEMENTED = False
if 'SKIP_NOT_IMPLEMENTED' in os.environ:
SKIP_NOT_IMPLEMENTED = True
if 'TEST_FILES' in os.environ:
ALL_FILES = glob.glob(os.environ['TEST_FILES'], recursive=True)
MINIC_EVAL = os.path.join(
HERE, '..', '..', 'TP03', 'MiniC-type-interpret', 'Main.py')
# if 'COMPIL_MINIC_EVAL' in os.environ:
# MINIC_EVAL = os.environ['COMPIL_MINIC_EVAL']
# else:
# MINIC_EVAL = os.path.join(
# HERE, '..', '..', 'TP03', 'MiniC-type-interpret', 'Main.py')
# Avoid duplicates
ALL_IN_MEM_FILES = list(set(ALL_FILES) | set(ALLOC_FILES))
ALL_IN_MEM_FILES.sort()
ALL_FILES = list(set(ALL_FILES))
ALL_FILES.sort()
if 'TEST_FILES' in os.environ:
ALLOC_FILES = ALL_FILES
ALL_IN_MEM_FILES = ALL_FILES
class TestCodeGen(TestExpectPragmas):
# Not in test_expect_pragma to get assertion rewritting
def assert_equal(self, actual, expected):
if DISABLE_TYPECHECK and expected.exitcode != 0:
# Test should fail at typecheck, and we don't do
# typechecking => nothing to check.
pytest.skip("Test that doesn't typecheck with --disable-typecheck")
if expected.output is not None and actual.output is not None:
assert actual.output == expected.output, \
"Output of the program is incorrect."
assert actual.exitcode == expected.exitcode, \
"Exit code of the compiler is incorrect"
assert actual.execcode == expected.execcode, \
"Exit code of the execution (spike) is incorrect"
def naive_alloc(self, file, info):
return self.compile_and_simulate(file, info, reg_alloc='naive')
def all_in_mem(self, file, info):
return self.compile_and_simulate(file, info, reg_alloc='all-in-mem')
def smart_alloc(self, file, info):
return self.compile_and_simulate(file, info, reg_alloc='smart')
def run_with_gcc(self, file, info):
return self.compile_and_simulate(file, info, reg_alloc='gcc', use_gcc=True)
def compile_with_gcc(self, file, output_name):
print("Compiling with GCC...")
result = self.run_command(
[ASM, '-S', '-I./',
'--output=' + output_name,
'-Werror',
'-Wno-div-by-zero', # We need to accept 1/0 at compile-time
file])
print(result.output)
print("Compiling with GCC... DONE")
return result
def compile_with_ours(self, file, output_name, reg_alloc):
print("Compiling ...")
self.remove(output_name)
alloc_opt = '--reg-alloc=' + reg_alloc
out_opt = '--output=' + output_name
cmd = [sys.executable, MINIC_COMPILE,
alloc_opt, out_opt]
cmd += MINICC_OPTS
cmd += [file]
result = self.run_command(cmd)
print(' '.join(cmd))
print("Exited with status:", result.exitcode)
print(result.output)
if result.exitcode == 4:
if "AllocationError" in result.output:
if reg_alloc == 'naive':
pytest.skip("Too big for the naive allocator")
elif reg_alloc == 'all-in-mem':
pytest.skip("Too big for the all in memory allocator")
else:
raise Exception("AllocationError should only happen "
"for reg_alloc='naive' or reg_alloc='all_in_mem'")
elif ("NotImplementedError" in result.output and
SKIP_NOT_IMPLEMENTED):
pytest.skip("Feature not implemented in this compiler")
if result.exitcode != 0:
# May either be a failing test or a test with expected
# compilation failure (bad type, ...). Let the caller
# do the assertion and decide:
return result
assert(os.path.isfile(output_name))
print("Compiling ... OK")
return result
def link_and_run(self, output_name, exec_name, info):
self.remove(exec_name)
cmd = [
ASM, output_name, '../TP01/riscv/libprint.s',
'-o', exec_name
] + info.linkargs
print(info)
print("Assembling and linking " + output_name + ": " + ' '.join(cmd))
try:
subprocess.check_output(cmd, timeout=60, stderr=subprocess.STDOUT)
except subprocess.CalledProcessError as e:
print("Assembling failed:\n")
print(e.output.decode())
print("Assembler code below:\n")
cat(output_name)
pytest.fail()
assert (os.path.isfile(exec_name))
sys.stdout.write("Assembling and linking ... OK\n")
try:
result = self.run_command(
[SIMU,
'-m100', # Limit memory usage to 100MB, more than enough and
# avoids crashing on a VM with <= 2GB RAM for example.
'pk',
exec_name],
scope="runtime")
output = re.sub(r'bbl loader\r?\n', '', result.output)
return testinfo(execcode=result.execcode,
exitcode=result.exitcode,
output=output,
linkargs=[],
skip_test_expected=False)
except subprocess.TimeoutExpired:
pytest.fail("Timeout executing program. Infinite loop in generated code?")
def compile_and_simulate(self, file, info, reg_alloc, use_gcc=False):
basename, _ = os.path.splitext(file)
output_name = basename + '-' + reg_alloc + '.s'
if use_gcc:
result = self.compile_with_gcc(file, output_name)
if result.exitcode != 0:
# We don't consider the exact exitcode, and ignore the
# output (our error messages may be different from
# GCC's)
return result._replace(exitcode=1,
output=None)
else:
result = self.compile_with_ours(file, output_name, reg_alloc)
if reg_alloc == 'none' or info.exitcode != 0 or result.exitcode != 0:
# Either the result is meaningless, or we already failed
# and don't need to go any further:
return result
# Only executable code past this point.
exec_name = basename + '-' + reg_alloc + '.riscv'
return self.link_and_run(output_name, exec_name, info)
@pytest.mark.parametrize('filename', ALL_FILES)
def test_expect(self, filename):
"""Test the EXPECTED annotations in test files by launching the
program with GCC."""
expect = self.get_expect(filename)
if expect.skip_test_expected:
pytest.skip("Skipping test because it contains SKIP TEST EXPECTED")
if expect.exitcode != 0:
# GCC is more permissive than us, so trying to compile an
# incorrect program would bring us no information (it may
# compile, or fail with a different message...)
pytest.skip("Not testing the expected value for tests expecting exitcode!=0")
gcc_result = self.run_with_gcc(filename, expect)
self.assert_equal(gcc_result, expect)
@pytest.mark.parametrize('filename', ALL_FILES)
def test_naive_alloc(self, filename):
expect = self.get_expect(filename)
naive = self.naive_alloc(filename, expect)
self.assert_equal(naive, expect)
@pytest.mark.parametrize('filename', ALL_IN_MEM_FILES)
def test_alloc_mem(self, filename):
expect = self.get_expect(filename)
actual = self.all_in_mem(filename, expect)
self.assert_equal(actual, expect)
@pytest.mark.parametrize('filename', ALLOC_FILES)
def test_smart_alloc(self, filename):
"""Generate code with smart allocation."""
expect = self.get_expect(filename)
actual = self.smart_alloc(filename, expect)
self.assert_equal(actual, expect)
if __name__ == '__main__':
pytest.main(sys.argv)