Commit 1846958b authored by Matthieu Moy's avatar Matthieu Moy
Browse files

TP3 online

parent 7332c951
[report]
exclude_lines =
pragma: no cover
raise MiniCInternalError
raise MiniCUnsupportedError
if debug:
/MiniCLexer.py
/MiniCParser.py
/MiniCVisitor.py
/TP*/tests/**/*.s
/TP*/**/*.riscv
/TP*/**/*-naive.s
/TP*/**/*-all_in_mem.s
/TP*/**/*-smart.s
/TP*/**/*.trace
/TP*/**/*.dot
/TP*/**/*.dot.pdf
/MiniC-stripped.g4
.coverage
class MiniCRuntimeError(Exception):
pass
class MiniCInternalError(Exception):
pass
class MiniCUnsupportedError(Exception):
pass
class AllocationError(Exception):
pass
MYNAME = JohnDoe
PACKAGE = MiniC
# Example: stop at the first failed test:
# make PYTEST_OPTS=-x tests
PYTEST_OPTS =
# Run the whole test infrastructure for a subset of test files e.g.
# make TEST_FILES='TP03/**/bad*.c' tests
ifdef TEST_FILES
export TEST_FILES
endif
PYTEST_BASE_OPTS=-vv -rs --failed-first --cov="$(PWD)" --cov-report=term --cov-report=html
ifndef ANTLR4
abort:
$(error variable ANTLR4 is not set)
endif
all: antlr
.PHONY: antlr
antlr MiniCLexer.py MiniCParser.py: $(PACKAGE).g4
$(ANTLR4) $< -Dlanguage=Python3 -visitor -no-listener
main-deps: MiniCLexer.py MiniCParser.py TP03/MiniCInterpretVisitor.py TP03/MiniCTypingVisitor.py
.PHONY: tests tests-interpret tests-codegen tests-regalloc clean clean-tests tar
tests: tests-pyright tests-interpret
tests-pyright: antlr
pyright .
tests-interpret: test_interpreter.py main-deps
python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) test_interpreter.py
# Test for naive allocator (also runs test_expect to check // EXPECTED directives):
tests-naive: antlr
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:
tests-notsmart: antlr
python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py -k 'not smart'
# Test just the smart allocation (quicker than tests)
tests-smart: antlr
python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py -k 'smart'
# Complete testsuite (should pass for lab5):
tests-codegen: antlr
python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py
tar: clean
dir=$$(basename "$$PWD") && cd .. && \
tar cvfz $(MYNAME).tgz --exclude="*.riscv" --exclude=".git" --exclude=".pytest_cache" \
--exclude="htmlcov" "$$dir"
@echo "Created ../$(MYNAME).tgz"
# Remove any assembly file that was created by a test.
# Don't just find -name \*.s -exec rm {} \; because there may be legitimate .s files in the testsuite.
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")):
try:
os.remove(s)
print("Removed {}".format(s))
except OSError:
pass
endef
export CLEAN
clean-tests:
@python3 -c "$$CLEAN"
clean: clean-tests
find . \( -iname "*~" -or -iname ".cache*" -or -iname "*.diff" -or -iname "log*.txt" -or -iname "__pycache__" -or -iname "*.tokens" -or -iname "*.interp" \) -print0 | xargs -0 rm -rf \;
rm -rf *~ $(PACKAGE)Parser.py $(PACKAGE)Lexer.py $(PACKAGE)Visitor.py .coverage .benchmarks
grammar MiniC;
prog: function* EOF #progRule;
// For now, we don't have "real" functions, just the main() function
// that is the main program, with a hardcoded profile and final
// 'return 0'.
function: INTTYPE ID OPAR CPAR OBRACE vardecl_l block
RETURN INT SCOL CBRACE #funcDecl;
vardecl_l: vardecl* #varDeclList;
vardecl: typee id_l SCOL #varDecl;
id_l
: ID #idListBase
| ID COM id_l #idList
;
block: stat* #statList;
stat
: assignment SCOL
| if_stat
| while_stat
| print_stat
;
assignment: ID ASSIGN expr #assignStat;
if_stat: IF OPAR expr CPAR then_block=stat_block (ELSE else_block=stat_block)? #ifStat;
stat_block
: OBRACE block CBRACE
| stat
;
while_stat: WHILE OPAR expr CPAR stat_block #whileStat;
print_stat
: PRINTLN_INT OPAR expr CPAR SCOL #printlnintStat
| PRINTLN_FLOAT OPAR expr CPAR SCOL #printlnfloatStat
| PRINTLN_STRING OPAR expr CPAR SCOL #printlnstringStat
;
expr_l
: /* Nothing */ #exprListEmpty
| expr #exprListBase
| expr COM expr_l #exprList
;
expr
: MINUS expr #unaryMinusExpr
| NOT expr #notExpr
| expr myop=(MULT|DIV|MOD) expr #multiplicativeExpr
| expr myop=(PLUS|MINUS) expr #additiveExpr
| expr myop=(GT|LT|GTEQ|LTEQ) expr #relationalExpr
| expr myop=(EQ|NEQ) expr #equalityExpr
| expr AND expr #andExpr
| expr OR expr #orExpr
| atom #atomExpr
;
atom
: OPAR expr CPAR #parExpr
| INT #intAtom
| FLOAT #floatAtom
| (TRUE | FALSE) #booleanAtom
| ID #idAtom
| STRING #stringAtom
;
typee
: mytype=(INTTYPE|FLOATTYPE|BOOLTYPE|STRINGTYPE) #basicType
;
OR : '||';
AND : '&&';
EQ : '==';
NEQ : '!=';
GT : '>';
LT : '<';
GTEQ : '>=';
LTEQ : '<=';
PLUS : '+';
MINUS : '-';
MULT : '*';
DIV : '/';
MOD : '%';
NOT : '!';
COL: ':';
SCOL : ';';
COM : ',';
ASSIGN : '=';
OPAR : '(';
CPAR : ')';
OBRACE : '{';
CBRACE : '}';
TRUE : 'true';
FALSE : 'false';
IF : 'if';
ELSE : 'else';
WHILE : 'while';
RETURN : 'return';
PRINTLN_INT : 'println_int';
PRINTLN_STRING : 'println_string';
PRINTLN_FLOAT : 'println_float';
INTTYPE: 'int';
FLOATTYPE: 'float';
STRINGTYPE: 'string';
BOOLTYPE : 'bool';
ID
: [a-zA-Z_] [a-zA-Z_0-9]*
;
INT
: [0-9]+
;
FLOAT
: [0-9]+ '.' [0-9]*
| '.' [0-9]+
;
STRING
: '"' (~["\r\n] | '""')* '"'
;
COMMENT
// # is a comment in Mini-C, and used for #include in real C so that we ignore #include statements
: ('#' | '//') ~[\r\n]* -> skip
;
SPACE
: [ \t\r\n] -> skip
;
from MiniCLexer import MiniCLexer
from MiniCParser import MiniCParser
from TP03.MiniCInterpretVisitor import MiniCInterpretVisitor
from Errors import MiniCRuntimeError, MiniCInternalError
from TP03.MiniCTypingVisitor import MiniCTypingVisitor, MiniCTypeError
import sys
import argparse
import antlr4
from antlr4.error.ErrorListener import ErrorListener
class CountErrorListener(ErrorListener):
"""Count number of errors.
Parser provides getNumberOfSyntaxErrors(), but the Lexer
apparently doesn't provide an easy way to know if an error occurred
after the fact. Do the counting ourserves with a listener.
"""
def __init__(self):
super(CountErrorListener, self).__init__()
self.count = 0
def syntaxError(self, recognizer, offending_symbol, line, column, msg, e):
self.count += 1
enable_typing = True
def main():
# command line
parser = argparse.ArgumentParser(description='Exec/Type mu files.')
parser.add_argument('path', type=str,
help='file to exec and type')
args = parser.parse_args()
# lex and parse
input_s = antlr4.FileStream(args.path, encoding='utf8')
lexer = MiniCLexer(input_s)
counter = CountErrorListener()
lexer._listeners.append(counter)
stream = antlr4.CommonTokenStream(lexer)
parser = MiniCParser(stream)
parser._listeners.append(counter)
tree = parser.prog()
if counter.count > 0:
exit(3) # Syntax or lexicography errors occurred
# typing Visitor
if enable_typing:
typing_visitor = MiniCTypingVisitor()
try:
typing_visitor.visit(tree)
except MiniCTypeError as e:
print(e.args[0])
exit(2)
# interpret Visitor
interpreter_visitor = MiniCInterpretVisitor()
try:
interpreter_visitor.visit(tree)
except MiniCRuntimeError as e:
print(e.args[0])
exit(1)
except MiniCInternalError as e:
print(e.args[0], file=sys.stderr)
exit(4)
if __name__ == '__main__':
main()
# MiniC interpreter and typer
LAB3, MIF08 / CAP 2021-22
# Authors
YOUR NAME HERE
# Contents
TODO for STUDENTS : Say a bit about the code infrastructure ...
# Howto
`make tests-interpret TEST_FILES='TP03/tests/provided/examples/test00.c'` for a single run
it should print 42
`make tests` to test all the files in `*/tests/*` according to `EXPECTED` results.
You can select the files you want to test by using `make tests TEST_FILES='TP03/**/*bad*.c'` (`**` means
"any number of possibly nested directories").
# Test design
TODO : explain your tests. Do not repeat what test files already contain, just give the main objectives of the tests.
# Design choices
TODO : explain your choices - explain the limitations of your implementation.
# Known bugs
TODO : document any known bug and limitations. Did you do everything asked for?
# Visitor to *interpret* MiniC files
from typing import Dict, List
import typing
from MiniCVisitor import MiniCVisitor
from MiniCParser import MiniCParser
from Errors import MiniCRuntimeError, MiniCInternalError
MINIC_VALUE = typing.Union[int, str, bool, float, List['MINIC_VALUE']]
class MiniCInterpretVisitor(MiniCVisitor):
_memory: Dict[str, MINIC_VALUE]
def __init__(self):
self._memory = dict() # store all variable ids and values.
self.has_main = False
# visitors for variable declarations
def visitVarDecl(self, ctx) -> None:
# Initialise all variables in self._memory
type_str = ctx.typee().getText()
raise NotImplementedError()
def visitIdList(self, ctx) -> List[str]:
raise NotImplementedError()
def visitIdListBase(self, ctx) -> List[str]:
return [ctx.ID().getText()]
# visitors for atoms --> value
def visitParExpr(self, ctx) -> MINIC_VALUE:
return self.visit(ctx.expr())
def visitIntAtom(self, ctx) -> int:
return int(ctx.getText())
def visitFloatAtom(self, ctx) -> float:
return float(ctx.getText())
def visitBooleanAtom(self, ctx) -> bool:
return ctx.getText() == "true"
def visitIdAtom(self, ctx) -> MINIC_VALUE:
raise NotImplementedError()
def visitStringAtom(self, ctx) -> str:
return ctx.getText()[1:-1]
# visit expressions
def visitAtomExpr(self, ctx) -> MINIC_VALUE:
return self.visit(ctx.atom())
def visitOrExpr(self, ctx) -> bool:
lval = self.visit(ctx.expr(0))
rval = self.visit(ctx.expr(1))
return lval | rval
def visitAndExpr(self, ctx) -> bool:
lval = self.visit(ctx.expr(0))
rval = self.visit(ctx.expr(1))
return lval & rval
def visitEqualityExpr(self, ctx) -> bool:
lval = self.visit(ctx.expr(0))
rval = self.visit(ctx.expr(1))
# be careful for float equality
if ctx.myop.type == MiniCParser.EQ:
return lval == rval
else:
return lval != rval
def visitRelationalExpr(self, ctx) -> bool:
lval = self.visit(ctx.expr(0))
rval = self.visit(ctx.expr(1))
if ctx.myop.type == MiniCParser.LT:
return lval < rval
elif ctx.myop.type == MiniCParser.LTEQ:
return lval <= rval
elif ctx.myop.type == MiniCParser.GT:
return lval > rval
elif ctx.myop.type == MiniCParser.GTEQ:
return lval >= rval
else:
raise MiniCInternalError(
"Unknown comparison operator '%s'" % ctx.myop
)
def visitAdditiveExpr(self, ctx) -> MINIC_VALUE:
lval = self.visit(ctx.expr(0))
rval = self.visit(ctx.expr(1))
if ctx.myop.type == MiniCParser.PLUS:
if any(isinstance(x, str) for x in (lval, rval)):
return '{}{}'.format(lval, rval)
else:
return lval + rval
elif ctx.myop.type == MiniCParser.MINUS:
return lval - rval
else:
raise MiniCInternalError(
"Unknown additive operator '%s'" % ctx.myop)
def visitMultiplicativeExpr(self, ctx) -> MINIC_VALUE:
lval = self.visit(ctx.expr(0))
rval = self.visit(ctx.expr(1))
if ctx.myop.type == MiniCParser.MULT:
return lval * rval
elif ctx.myop.type == MiniCParser.DIV:
if rval == 0:
raise MiniCRuntimeError("Division by 0")
if isinstance(lval, int):
return lval // rval
else:
return lval / rval
elif ctx.myop.type == MiniCParser.MOD:
# TODO : interpret modulo
raise NotImplementedError()
else:
raise MiniCInternalError(
"Unknown multiplicative operator '%s'" % ctx.myop)
def visitNotExpr(self, ctx) -> bool:
return not self.visit(ctx.expr())
def visitUnaryMinusExpr(self, ctx) -> MINIC_VALUE:
return -self.visit(ctx.expr())
# visit statements
def visitPrintlnintStat(self, ctx) -> None:
val = self.visit(ctx.expr())
if isinstance(val, bool):
val = '1' if val else '0'
print(val)
def visitPrintlnfloatStat(self, ctx) -> None:
val = self.visit(ctx.expr())
if isinstance(val, float):
val = "%.2f" % val
print(val)
def visitPrintlnstringStat(self, ctx) -> None:
val = self.visit(ctx.expr())
print(val)
def visitAssignStat(self, ctx) -> None:
raise NotImplementedError()
def visitIfStat(self, ctx) -> None:
raise NotImplementedError()
def visitWhileStat(self, ctx) -> None:
raise NotImplementedError()
# TOPLEVEL
def visitProgRule(self, ctx) -> None:
self.visitChildren(ctx)
if not self.has_main:
# A program without a main function is compilable (hence
# it's not a typing error per se), but not executable,
# hence we consider it a runtime error.
raise MiniCRuntimeError("No main function in file")
# Visit a function: ignore if non main!
def visitFuncDecl(self, ctx) -> None:
funname = ctx.ID().getText()
if funname == "main":
self.has_main = True
self.visit(ctx.vardecl_l())
self.visit(ctx.block())
from MiniCVisitor import MiniCVisitor
from MiniCParser import MiniCParser
from Errors import MiniCInternalError
from enum import Enum
class MiniCTypeError(Exception):
pass
class BaseType(Enum):
Float, Integer, Boolean, String = range(4)
# Basic Type Checking for MiniC programs.
class MiniCTypingVisitor(MiniCVisitor):
def __init__(self):
self._memorytypes = dict() # id-> types
self._current_function = "main"
def _raise(self, ctx, for_what, *types):
raise MiniCTypeError(
'In function {}: Line {} col {}: invalid type for {}: {}'.format(
self._current_function,
ctx.start.line, ctx.start.column, for_what,
' and '.join(t.name.lower() for t in types)))
def _raiseMismatch(self, ctx, for_what, *types):
raise MiniCTypeError(
'In function {}: Line {} col {}: type mismatch for {}: {}'.format(
self._current_function,
ctx.start.line, ctx.start.column, for_what,
' and '.join(t.name.lower() for t in types)))
def _raiseNonType(self, ctx, message):
raise MiniCTypeError(
'In function {}: Line {} col {}: {}'.format(
self._current_function,
ctx.start.line, ctx.start.column, message))
# type declaration
def visitVarDecl(self, ctx):
vars_l = self.visit(ctx.id_l())
tt = self.visit(ctx.typee())
for name in vars_l:
if name in self._memorytypes:
self._raiseNonType(ctx,
"Variable {0} already declared".
format(name))
self._memorytypes[name] = tt
def visitBasicType(self, ctx):
if ctx.mytype.type == MiniCParser.INTTYPE:
return BaseType.Integer
elif ctx.mytype.type == MiniCParser.FLOATTYPE:
return BaseType.Float
elif ctx.mytype.type == MiniCParser.BOOLTYPE:
return BaseType.Boolean
elif ctx.mytype.type == MiniCParser.STRINGTYPE:
return BaseType.String
else:
raise MiniCInternalError("Type not implemented")
def visitIdList(self, ctx):
t = self.visit(ctx.id_l())
t.append(ctx.ID().getText())
return t
def visitIdListBase(self, ctx):
return [ctx.ID().getText()]