automated commit TP3

This commit is contained in:
Rémi Di Guardia 2022-09-28 09:28:41 +02:00
parent 530ff7e694
commit 2e15c1352e
45 changed files with 1723 additions and 1 deletions

9
MiniC/.coveragerc Normal file
View File

@ -0,0 +1,9 @@
[report]
exclude_lines =
pragma: no cover
raise MiniCInternalError
raise MiniCUnsupportedError
if debug:
if debug_graphs:
if self._debug:
if self._debug_graphs:

16
MiniC/.gitignore vendored Normal file
View File

@ -0,0 +1,16 @@
/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
htmlcov/
doc/api
doc/_build

18
MiniC/Lib/Errors.py Normal file
View File

@ -0,0 +1,18 @@
class MiniCRuntimeError(Exception):
pass
class MiniCInternalError(Exception):
pass
class MiniCUnsupportedError(Exception):
pass
class MiniCTypeError(Exception):
pass
class AllocationError(Exception):
pass

99
MiniC/Makefile Normal file
View File

@ -0,0 +1,99 @@
MYNAME = JohnDoe
PACKAGE = MiniC
# Example: stop at the first failed test:
# make PYTEST_OPTS=-x test
PYTEST_OPTS =
# Run the whole test infrastructure for a subset of test files e.g.
# make TEST_FILES='TP03/**/bad*.c' test
ifdef TEST_FILES
export TEST_FILES
endif
ifdef SSA
MINICC_OPTS+=--ssa
endif
ifdef SSA_OPTIM
MINICC_OPTS+=--ssa-optim
endif
ifdef TYPECHECK_ONLY
MINICC_OPTS+=--disable-codegen
endif
ifdef PARSE_ONLY
MINICC_OPTS+=--disable-typecheck --disable-codegen
endif
export MINICC_OPTS
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: test test-interpret test-codegen clean clean-tests tar antlr
test: test-interpret
test-pyright: antlr
pyright .
test-interpret: test-pyright 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):
test-naive: test-pyright 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:
test-notsmart: test-pyright antlr
python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py -k 'not smart'
# Test just the smart allocator (quicker than tests)
test-smart: test-pyright antlr
python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py -k 'smart'
# Complete testsuite (should pass for lab5):
test-codegen: test-pyright 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

141
MiniC/MiniC.g4 Normal file
View File

@ -0,0 +1,141 @@
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 #funcDef;
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 body=stat_block #whileStat;
print_stat
: PRINTLN_INT OPAR expr CPAR SCOL #printlnintStat
| PRINTLN_FLOAT OPAR expr CPAR SCOL #printlnfloatStat
| PRINTLN_BOOL OPAR expr CPAR SCOL #printlnboolStat
| PRINTLN_STRING OPAR expr CPAR SCOL #printlnstringStat
;
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_BOOL : 'println_bool';
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
;

310
MiniC/MiniCC.py Normal file
View File

@ -0,0 +1,310 @@
#! /usr/bin/env python3
"""
Code generation lab, main file. Code Generation with Smart IRs.
Usage:
python3 MiniCC.py <filename>
python3 MiniCC.py --help
"""
import traceback
from typing import cast
from MiniCLexer import MiniCLexer
from MiniCParser import MiniCParser
from TP03.MiniCTypingVisitor import MiniCTypingVisitor, MiniCTypeError
from TP03.MiniCInterpretVisitor import MiniCInterpretVisitor
from Lib.Errors import (MiniCUnsupportedError, MiniCInternalError,
MiniCRuntimeError, AllocationError)
from enum import Enum
import argparse
from antlr4 import FileStream, CommonTokenStream
from antlr4.error.ErrorListener import ErrorListener
import os
import sys
class Mode(Enum):
PARSE = 0
EVAL = 1
LINEAR = 2
CFG = 3
SSA = 4
OPTIM = 5
def is_codegen(self) -> bool:
return self.value >= Mode.LINEAR.value
def valid_modes():
modes = ['parse', 'typecheck', 'eval']
try:
import TP04.MiniCCodeGen3AVisitor # type: ignore[import]
modes.append('codegen-linear')
except ImportError:
return modes
try:
import Lib.CFG # type: ignore[import]
modes.append('codegen-cfg')
except ImportError:
return modes
try:
import TP05.SSA # type: ignore[import]
modes.append('codegen-ssa')
except ImportError:
return modes
try:
import TP05c.OptimSSA # type: ignore[import]
modes.append('codegen-optim')
except ImportError:
pass
return modes
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
def main(inputname, reg_alloc, mode,
typecheck=True, stdout=False, output_name=None, debug=False,
debug_graphs=False, ssa_graphs=False):
(basename, rest) = os.path.splitext(inputname)
if mode.is_codegen():
if stdout:
output_name = None
print("Code will be generated on standard output")
elif output_name is None:
output_name = basename + ".s"
print("Code will be generated in file " + output_name)
input_s = FileStream(inputname, encoding='utf-8')
lexer = MiniCLexer(input_s)
counter = CountErrorListener()
lexer._listeners.append(counter)
stream = CommonTokenStream(lexer)
parser = MiniCParser(stream)
parser._listeners.append(counter)
tree = parser.prog()
if counter.count > 0:
exit(3) # Syntax or lexicography errors occurred, don't try to go further.
if typecheck:
typing_visitor = MiniCTypingVisitor()
try:
typing_visitor.visit(tree)
except MiniCTypeError as e:
print(e.args[0])
exit(2)
if mode == Mode.EVAL:
# 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)
return
if not mode.is_codegen():
if debug:
print("Not running code generation because of --typecheck-only.")
return
# Codegen 3@ CFG Visitor, first argument is debug mode
from TP04.MiniCCodeGen3AVisitor import MiniCCodeGen3AVisitor # type: ignore[import]
visitor3 = MiniCCodeGen3AVisitor(debug, parser)
# dump generated code on stdout or file.
with open(output_name, 'w') if output_name else sys.stdout as output:
visitor3.visit(tree)
for function in visitor3.get_functions():
fdata = function.fdata
# Allocation part
if mode == Mode.LINEAR:
code = function
else:
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))
if debug_graphs:
s = "{}.{}.dot".format(basename, code.fdata._name)
print("CFG:", s)
code.print_dot(s, view=True)
if mode.value >= Mode.SSA.value:
from TP05.SSA import enter_ssa # type: ignore[import]
from Lib.CFG import CFG # type: ignore[import]
DF = enter_ssa(cast(CFG, code), basename, debug, ssa_graphs)
if ssa_graphs:
s = "{}.{}.ssa.dot".format(basename, code.fdata._name)
print("SSA:", s)
code.print_dot(s, DF, True)
if mode == Mode.OPTIM:
from TP05c.OptimSSA import OptimSSA # type: ignore[import]
OptimSSA(cast(CFG, code), debug=debug)
if ssa_graphs:
s = "{}.{}.optimssa.dot".format(basename, code.fdata._name)
print("SSA after optim:", s)
code.print_dot(s, view=True)
allocator = None
if reg_alloc == "naive":
from Lib.Allocator import NaiveAllocator # type: ignore[import]
allocator = NaiveAllocator(fdata)
comment = "naive allocation"
elif reg_alloc == "all-in-mem":
from TP04.AllInMemAllocator import AllInMemAllocator # type: ignore[import]
allocator = AllInMemAllocator(fdata)
comment = "all-in-memory allocation"
elif reg_alloc == "smart":
liveness = None
if mode == Mode.SSA:
from TP05.LivenessSSA import LivenessSSA # type: ignore[import]
try:
from Lib.CFG import CFG # type: ignore[import]
liveness = LivenessSSA(cast(CFG, code), debug=debug)
except NameError:
form = "CFG in SSA form"
raise ValueError("Invalid dataflow form: \
liveness file not found for {}.".format(form))
else:
try:
from TP05.LivenessDataFlow import LivenessDataFlow # type: ignore[import]
liveness = LivenessDataFlow(code, debug=debug)
except NameError:
form = "CFG not in SSA form"
raise ValueError("Invalid dataflow form: \
liveness file not found for {}.".format(form))
from TP05.SmartAllocator import SmartAllocator # type: ignore[import]
allocator = SmartAllocator(fdata, basename, liveness,
debug, debug_graphs)
comment = "smart allocation with graph coloring"
elif reg_alloc == "none":
comment = "non executable 3-Address instructions"
else:
raise ValueError("Invalid allocation strategy:" + reg_alloc)
if allocator:
allocator.prepare()
if mode == Mode.SSA:
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)
print("CFG after SSA:", s)
code.print_dot(s, view=True)
code.print_code(output, comment=comment)
if debug:
visitor3.printSymbolTable()
# command line management
if __name__ == '__main__':
modes = valid_modes()
parser = argparse.ArgumentParser(description='Generate code for .c file')
parser.add_argument('filename', type=str,
help='Source file.')
parser.add_argument('--mode', type=str,
choices=valid_modes(),
required=True,
help='Operation to perform on the input program')
parser.add_argument('--debug', action='store_true',
default=False,
help='Emit verbose debug output')
parser.add_argument('--disable-typecheck', action='store_true',
default=False,
help="Don't run the typechecker before evaluation or code generation")
if "codegen-linear" in modes:
parser.add_argument('--reg-alloc', type=str,
choices=['none', 'naive', 'all-in-mem', 'smart'],
help='Register allocation to perform during code generation')
parser.add_argument('--stdout', action='store_true',
help='Generate code to stdout')
parser.add_argument('--output', type=str,
help='Generate code to outfile')
if "codegen-cfg" in modes:
parser.add_argument('--graphs', action='store_true',
default=False,
help='Display graphs (CFG, conflict graph).')
if "codegen-ssa" in modes:
parser.add_argument('--ssa-graphs', action='store_true',
default=False,
help='Display SSA graphs (DT, DF).')
args = parser.parse_args()
reg_alloc = args.reg_alloc if "codegen-linear" in modes else None
to_stdout = args.stdout if "codegen-linear" in modes else False
outfile = args.output if "codegen-linear" in modes else None
graphs = args.graphs if "codegen-cfg" in modes else False
ssa_graphs = args.ssa_graphs if "codegen-ssa" in modes else False
if reg_alloc is None and "codegen" in args.mode:
print("error: the following arguments is required: --reg-alloc")
exit(1)
elif reg_alloc is not None and "codegen" not in args.mode:
print("error: register allocation is only available in code generation mode")
exit(1)
typecheck = not args.disable_typecheck
if args.mode == "parse":
mode = Mode.PARSE
typecheck = False
elif args.mode == "typecheck":
mode = Mode.PARSE
elif args.mode == "eval":
mode = Mode.EVAL
elif args.mode == "codegen-linear":
mode = Mode.LINEAR
if reg_alloc == "smart":
print("error: smart register allocation is not compatible with linear code generation")
exit(1)
elif args.mode == "codegen-cfg":
mode = Mode.CFG
elif args.mode == "codegen-ssa":
mode = Mode.SSA
elif args.mode == "codegen-optim":
mode = Mode.OPTIM
else:
raise ValueError("Invalid mode:" + args.mode)
try:
main(args.filename, reg_alloc, mode,
typecheck,
to_stdout, outfile, args.debug,
graphs, ssa_graphs)
except MiniCUnsupportedError as e:
print(e)
exit(5)
except (MiniCInternalError, AllocationError):
traceback.print_exc()
exit(4)

View File

@ -0,0 +1,32 @@
# MiniC interpreter and typer
LAB3, MIF08 / CAP 2022-23
# Authors
TODO: YOUR NAME HERE
# Contents
TODO for STUDENTS : Say a bit about the code infrastructure ...
# Howto
`make test-interpret TEST_FILES='TP03/tests/provided/examples/test_print_int.c'` for a single run
`make test` to test all the files in `*/tests/*` according to `EXPECTED` results.
You can select the files you want to test by using `make test 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? Did you implement an extension?

View File

@ -0,0 +1,178 @@
# Visitor to *interpret* MiniC files
from typing import Dict, List, cast
from MiniCVisitor import MiniCVisitor
from MiniCParser import MiniCParser
from Lib.Errors import MiniCRuntimeError, MiniCInternalError
MINIC_VALUE = 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] # Remove the ""
# 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:
assert ctx.myop is not None
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:
assert ctx.myop is not None
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:
assert ctx.myop is not None
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:
assert ctx.myop is not None
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())
print(val)
def visitPrintlnfloatStat(self, ctx) -> None:
val = self.visit(ctx.expr())
if isinstance(val, float):
val = "%.2f" % val
print(val)
def visitPrintlnboolStat(self, ctx) -> None:
val = self.visit(ctx.expr())
print('1' if val else '0')
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 visitFuncDef(self, ctx) -> None:
funname = ctx.ID().getText()
if funname == "main":
self.has_main = True
self.visit(ctx.vardecl_l())
self.visit(ctx.block())

View File

@ -0,0 +1,147 @@
# Visitor to *typecheck* MiniC files
from typing import List
from MiniCVisitor import MiniCVisitor
from MiniCParser import MiniCParser
from Lib.Errors import MiniCInternalError, MiniCTypeError
from enum import Enum
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
# For now, we don't have real functions ...
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 _assertSameType(self, ctx, for_what, *types):
if not all(types[0] == t for t in 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) -> None:
raise NotImplementedError()
def visitBasicType(self, ctx):
assert ctx.mytype is not None
if ctx.mytype.type == MiniCParser.INTTYPE:
return BaseType.Integer
elif ctx.mytype.type == MiniCParser.FLOATTYPE:
return BaseType.Float
else: # TODO: same for other types
raise NotImplementedError()
def visitIdList(self, ctx) -> List[str]:
raise NotImplementedError()
def visitIdListBase(self, ctx) -> List[str]:
raise NotImplementedError()
# typing visitors for expressions, statements !
# visitors for atoms --> type
def visitParExpr(self, ctx):
return self.visit(ctx.expr())
def visitIntAtom(self, ctx):
return BaseType.Integer
def visitFloatAtom(self, ctx):
return BaseType.Float
def visitBooleanAtom(self, ctx):
raise NotImplementedError()
def visitIdAtom(self, ctx):
try:
return self._memorytypes[ctx.getText()]
except KeyError:
self._raiseNonType(ctx,
"Undefined variable {}".format(ctx.getText()))
def visitStringAtom(self, ctx):
return BaseType.String
# now visit expr
def visitAtomExpr(self, ctx):
return self.visit(ctx.atom())
def visitOrExpr(self, ctx):
raise NotImplementedError()
def visitAndExpr(self, ctx):
raise NotImplementedError()
def visitEqualityExpr(self, ctx):
raise NotImplementedError()
def visitRelationalExpr(self, ctx):
raise NotImplementedError()
def visitAdditiveExpr(self, ctx):
assert ctx.myop is not None
raise NotImplementedError()
def visitMultiplicativeExpr(self, ctx):
raise NotImplementedError()
def visitNotExpr(self, ctx):
raise NotImplementedError()
def visitUnaryMinusExpr(self, ctx):
raise NotImplementedError()
# visit statements
def visitPrintlnintStat(self, ctx):
etype = self.visit(ctx.expr())
if etype != BaseType.Integer:
self._raise(ctx, 'println_int statement', etype)
def visitPrintlnfloatStat(self, ctx):
etype = self.visit(ctx.expr())
if etype != BaseType.Float:
self._raise(ctx, 'println_float statement', etype)
def visitPrintlnboolStat(self, ctx):
etype = self.visit(ctx.expr())
if etype != BaseType.Boolean:
self._raise(ctx, 'println_int statement', etype)
def visitPrintlnstringStat(self, ctx):
etype = self.visit(ctx.expr())
if etype != BaseType.String:
self._raise(ctx, 'println_string statement', etype)
def visitAssignStat(self, ctx):
raise NotImplementedError()
def visitWhileStat(self, ctx):
raise NotImplementedError()
def visitIfStat(self, ctx):
raise NotImplementedError()

View File

@ -0,0 +1,13 @@
#include "printlib.h"
int main(){
int n;
n=17;
m=n+3;
println_int(m);
return 0;
}
// EXPECTED
// EXITCODE 2
// In function main: Line 6 col 2: Undefined variable m

View File

@ -0,0 +1,10 @@
#include "printlib.h"
int main(){
int x;
x="blablabla";
return 0;
}
// EXITCODE 2
// EXPECTED
// In function main: Line 5 col 2: type mismatch for x: integer and string

View File

@ -0,0 +1,14 @@
#include "printlib.h"
int main(){
int n;
string s;
n=17;
s="seventeen";
s = n*s;
return 0;
}
// EXITCODE 2
// EXPECTED
// In function main: Line 8 col 6: invalid type for multiplicative operands: integer and string

View File

@ -0,0 +1,11 @@
#include "printlib.h"
int main(){
string x;
x=1;
return 0;
}
// EXITCODE 2
// EXPECTED
// In function main: Line 5 col 2: type mismatch for x: string and integer

View File

@ -0,0 +1,10 @@
#include "printlib.h"
int main(){
int x;
x=34+f;
return 0;
}
// EXITCODE 2
// EXPECTED
// In function main: Line 5 col 7: Undefined variable f

View File

@ -0,0 +1,11 @@
#include "printlib.h"
int main(){
println_int("foo");
return 0;
}
// EXITCODE 2
// EXPECTED
// In function main: Line 4 col 2: invalid type for println_int statement: string

View File

@ -0,0 +1,9 @@
#include "printlib.h"
int main(){
println_int(true+true);
return 0;
}
// EXITCODE 2
// EXPECTED
// In function main: Line 4 col 14: invalid type for additive operands: boolean and boolean

View File

@ -0,0 +1,12 @@
#include "printlib.h"
int main(){
int x,y;
int z,x;
x=42;
return 0;
}
// EXITCODE 2
// EXPECTED
// In function main: Line 5 col 2: Variable x already declared

View File

@ -0,0 +1,11 @@
#include "printlib.h"
int toto(){
println_int(42);
return 0;
}
// EXITCODE 1
// EXPECTED
// No main function in file

View File

@ -0,0 +1,13 @@
#include "printlib.h"
int main()
{
int n,u;
n=17;
u=n;
println_int(n);
return 0;
}
// EXPECTED
// 17

View File

@ -0,0 +1,47 @@
#include "printlib.h"
int main()
{
if (2 < 3)
{
println_int(1);
}
if (2 > 3)
{
println_int(2);
}
if (2 <= 2)
{
println_int(3);
}
if (2 >= 2)
{
println_int(4);
}
if (2 == 3)
{
println_int(10);
}
if (2 != 3)
{
println_int(20);
}
if (2 == 2)
{
println_int(30);
}
if (2 != 2)
{
println_int(40);
}
return 0;
}
// EXPECTED
// 1
// 3
// 4
// 20
// 30

View File

@ -0,0 +1,11 @@
#include "printlib.h"
int main() {
if ((1.0 + 2.0) * 3.0 == 9.0) {
println_string("OK");
}
return 0;
}
// EXPECTED
// OK

View File

@ -0,0 +1,15 @@
#include "printlib.h"
int main(){
println_int(3/2+45*(2/1));
println_int(23+19);
println_bool( (false || 3 != 77 ) && (42<=1515) );
println_string("coucou");
return 0;
}
// EXPECTED
// 91
// 42
// 1
// coucou

View File

@ -0,0 +1,9 @@
#include "printlib.h"
int main(){
println_int(42);
return 0;
}
// EXPECTED
// 42

View File

@ -0,0 +1,11 @@
#include "printlib.h"
int main(){
int x;
x="blabla";
return 0;
}
// EXPECTED
// In function main: Line 5 col 2: type mismatch for x: integer and string
// EXITCODE 2

View File

@ -0,0 +1,14 @@
#include "printlib.h"
int main(){
string x,y,z;
x = "ENS";
y = "De Lyon";
z = x + " " + y;
println_string(z);
return 0;
}
// EXPECTED
// ENS De Lyon

View File

@ -0,0 +1,16 @@
#include "printlib.h"
int main(){
string n,m;
n = "foo";
m = "bar";
println_string(n);
println_string(m);
println_string(n + m);
return 0;
}
// EXPECTED
// foo
// bar
// foobar

View File

@ -0,0 +1,13 @@
#include "printlib.h"
int main() {
string s;
println_string(s);
s = s + "Coucou";
println_string(s);
return 0;
}
// EXPECTED
//
// Coucou

View File

@ -0,0 +1,16 @@
#include "printlib.h"
int main() {
bool x;
if (x) {
println_int(1);
}
x = !x;
if (x) {
println_int(2);
}
return 0;
}
// EXPECTED
// 2

View File

@ -0,0 +1,13 @@
#include "printlib.h"
int main() {
float x;
println_float(x);
x = x + 1.0;
println_float(x);
return 0;
}
// EXPECTED
// 0.00
// 1.00

View File

@ -0,0 +1,13 @@
#include "printlib.h"
int main() {
int x;
println_int(x);
x = x + 1;
println_int(x);
return 0;
}
// EXPECTED
// 0
// 1

View File

@ -0,0 +1 @@
Add your own tests in this directory.

20
MiniC/printlib.h Normal file
View File

@ -0,0 +1,20 @@
/**
* Compatibility layer with C (meant to be #included in MiniC source
* files). Defines types, constants and functions that are built-in
* MiniC, to allow compiling MiniC programs with GCC.
*/
typedef char * string;
typedef int bool;
static const int true = 1;
static const int false = 0;
void print_int(int);
void println_int(int);
void println_bool(int);
void print_float(float);
void println_float(float);
void print_string(string);
void println_string(string);

2
MiniC/pyproject.toml Normal file
View File

@ -0,0 +1,2 @@
[tool.pyright]
ignore = ["replace_cfgssa.py", "replace_smart_alloc.py", "replace_target_api.py", "test_replace_mem.py", "MiniCCWrapper.py", "MiniCCWrapperCFGSSA.py", "MiniCCWrapperSmartAlloc.py", "MiniCCWrapperFunctions.py", "MiniCLexer.py", "MiniCParser.py", "MiniCVisitor.py", "*.diff.py", "**/*.diff.py"]

191
MiniC/test_expect_pragma.py Normal file
View File

@ -0,0 +1,191 @@
import collections
import re
import os
import subprocess
import sys
import pytest
testinfo = collections.namedtuple(
'testinfo',
['exitcode', 'execcode', 'output', 'linkargs', 'skip_test_expected'])
default_testinfo = testinfo(
exitcode=0, execcode=0, output='', linkargs=[], skip_test_expected=False)
def cat(filename):
with open(filename, "rb") as f:
for line in f:
sys.stdout.buffer.write(line)
def env_bool_variable(name, globals):
if name not in globals:
globals[name] = False
if name in os.environ:
globals[name] = True
def env_str_variable(name, globals):
if name in os.environ:
globals[name] = os.environ[name]
class TestExpectPragmas(object):
"""Base class for tests that read the expected result as annotations
in test files.
get_expect(file) will parse the file, looking EXPECT and EXITCODE
pragmas.
run_command(command) is a wrapper around subprocess.check_output()
that extracts the output and exit code.
"""
def get_expect(self, filename):
"""Parse "filename" looking for EXPECT and EXITCODE annotations.
Look for a line "EXPECTED" (possibly with whitespaces and
comments). Text after this "EXPECTED" line is the expected
output.
The file may also contain a line like "EXITCODE <n>" where <n>
is an integer, and is the expected exitcode of the command.
The result is cached to avoid re-parsing the file multiple
times.
"""
if filename not in self.__expect:
self.__expect[filename] = self._extract_expect(filename)
return self.__expect[filename]
def remove(self, file):
"""Like os.remove(), but ignore errors, e.g. don't complain if the
file doesn't exist.
"""
try:
os.remove(file)
except OSError:
pass
def run_command(self, cmd, scope="compile"):
"""Run the command cmd (given as [command, arg1, arg2, ...]), and
return testinfo(exitcode=..., output=...) containing the
exit code of the command it its standard output + standard error.
If scope="compile" (resp. "runtime"), then the exitcode (resp.
execcode) is set with the exit status of the command, and the
execcode (resp. exitcode) is set to 0.
"""
try:
output = subprocess.check_output(cmd, timeout=60,
stderr=subprocess.STDOUT)
status = 0
except subprocess.CalledProcessError as e:
output = e.output
status = e.returncode
if scope == "runtime":
return default_testinfo._replace(execcode=status,
output=output.decode())
else:
return default_testinfo._replace(exitcode=status,
output=output.decode())
def skip_if_partial_match(self, actual, expect, ignore_error_message):
if not ignore_error_message:
return False
# TODO: Deal with undefined behavior here?
if expect.exitcode != actual.exitcode:
# Not the same exit code => something's wrong anyway
return False
if actual.exitcode == 3:
# There's a syntax error in both expected and actual,
# but the actual error may slightly differ if we don't
# have the exact same .g4.
return True
# Let the test pass with 'return True' if appropriate.
# Otherwise, continue to the full assertion for a
# complete diagnostic.
if actual.exitcode != 0 and expect.exitcode == actual.exitcode:
if expect.output == '':
# No output expected, but we know there must be an
# error. If there was a particular error message
# expected, we'd have written it in the output,
# hence just ignore the actual message.
return True
# Ignore difference in error message except in the
# line number (ignore the column too, it may
# slightly vary, eg. in "foo" / 4, the error may
# be considered on "foo" or on /):
if re.match(r'^In function [^ :]*: Line [0-9]* col [0-9]*:', actual.output):
out_loc = re.sub(r' col [0-9]*:.*$', '', actual.output)
exp_loc = re.sub(r' col [0-9]*:.*$', '', expect.output)
if out_loc == exp_loc:
return True
if any(x.output and (x.output.endswith('has no value yet!' + os.linesep)
or x.output.endswith(' by 0' + os.linesep))
for x in (actual, expect)):
# Ignore the error message when our compiler
# raises this error (either in actual or expect,
# depending on what we're testing).
return True
return False
__expect = {}
def _extract_expect(self, file):
exitcode = 0
execcode = 0
linkargs = []
inside_expected = False
skip_test_expected = False
expected_lines = []
expected_present = False
with open(file, encoding="utf-8") as f:
for line in f.readlines():
# Ignore non-comments
if not re.match(r'\s*//', line):
continue
# Cleanup comment start and whitespaces
line = re.sub(r'\s*//\s*', '', line)
line = re.sub(r'\s*$', '', line)
if line == 'END EXPECTED':
inside_expected = False
elif line.startswith('EXITCODE'):
words = line.split(' ')
assert len(words) == 2
exitcode = int(words[1])
elif line.startswith('EXECCODE'):
words = line.split(' ')
assert len(words) == 2
execcode = int(words[1])
elif line.startswith('LINKARGS'):
words = line.split(' ')
assert len(words) >= 2
linkargs += [w.replace("$dir", os.path.dirname(file))
for w in words[1:]]
elif line == 'EXPECTED':
inside_expected = True
expected_present = True
elif line == 'SKIP TEST EXPECTED':
skip_test_expected = True
elif inside_expected:
expected_lines.append(line)
if not expected_present:
pytest.fail("Missing EXPECTED directive in test file")
if expected_lines == []:
output = ''
else:
output = os.linesep.join(expected_lines) + os.linesep
return testinfo(exitcode=exitcode, execcode=execcode,
output=output, linkargs=linkargs,
skip_test_expected=skip_test_expected)

57
MiniC/test_interpreter.py Normal file
View File

@ -0,0 +1,57 @@
#! /usr/bin/env python3
import pytest
import glob
import os
import sys
from test_expect_pragma import TestExpectPragmas, cat
HERE = os.path.dirname(os.path.realpath(__file__))
if HERE == os.path.realpath('.'):
HERE = '.'
TEST_DIR = HERE
IMPLEM_DIR = HERE
DISABLE_TYPECHECK = False # True to skip typechecking
ALL_FILES = []
# tests for typing AND evaluation
ALL_FILES += glob.glob(os.path.join(TEST_DIR, 'TP03/tests/provided/**/*.c'), recursive=True)
ALL_FILES += glob.glob(os.path.join(TEST_DIR, 'TP03/tests/students/**/*.c'), recursive=True)
# Path setting
if 'TEST_FILES' in os.environ:
ALL_FILES = glob.glob(os.environ['TEST_FILES'], recursive=True)
MINIC_EVAL = os.path.join(IMPLEM_DIR, 'MiniCC.py')
class TestInterpret(TestExpectPragmas):
def evaluate(self, file):
if not DISABLE_TYPECHECK:
return self.run_command([sys.executable, MINIC_EVAL, "--mode", "eval", file])
else:
return self.run_command([sys.executable, MINIC_EVAL, "--mode", "eval", "--disable-typecheck", file])
# Not in test_expect_pragma to get assertion rewritting
def assert_equal(self, actual, expected):
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 is incorrect"
@pytest.mark.parametrize('filename', ALL_FILES)
def test_eval(self, filename):
cat(filename) # For diagnosis
expect = self.get_expect(filename)
eval = self.evaluate(filename)
if expect:
self.assert_equal(eval, expect)
if __name__ == '__main__':
pytest.main(sys.argv)

View File

@ -43,4 +43,12 @@ _Academic first semester 2022-2023_
- :book: 4th Course session: Friday 23/09/2022, 10:15. Amphi B (Ludovic Henrio) - :book: 4th Course session: Friday 23/09/2022, 10:15. Amphi B (Ludovic Henrio)
* Typing [slides in english](course/CAP_cours04_typing.pdf). * Typing [slides in english](course/CAP_cours04_typing.pdf).
# Week 4:
- :hammer: Lab 3: Wednesday 28/09/2021, 10h15-12h15. Rooms A1 (Nicolas Chappe) & B2 (Rémi Di Guardia)
* Interpreter & Typer [TP03](TP03/tp3.pdf).
* Code in [TP03/](TP03/) and [MiniC/TP03/](MiniC/TP03/).
* Graded lab due on https://etudes.ens-lyon.fr/course/view.php?id=5249 before 2022-10-04 23:59

2
TP03/arith-visitor/.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
/AritListener.py
/AritVisitor.py

View File

@ -0,0 +1,33 @@
grammar Arit;
prog: statement+ EOF #statementList
;
statement
: expr SCOL #exprInstr
| 'set' ID '=' expr SCOL #assignInstr
;
expr: expr multop=(MULT | DIV) expr #multiplicationExpr
| expr addop=(PLUS | MINUS) expr #additiveExpr
| atom #atomExpr
;
atom: INT #numberAtom
| ID #idAtom
| '(' expr ')' #parens
;
SCOL : ';';
PLUS : '+';
MINUS : '-';
MULT : '*';
DIV : '/';
ID: [a-zA-Z_] [a-zA-Z_0-9]*;
INT: [0-9]+;
COMMENT: '#' ~[\r\n]* -> skip;
NEWLINE: '\r'? '\n' -> skip;
WS : (' '|'\t')+ -> skip;

View File

@ -0,0 +1,23 @@
PACKAGE = Arit
MAINFILE = arit
ifndef ANTLR4
abort:
$(error variable ANTLR4 is not set)
endif
all: $(PACKAGE).g4
$(ANTLR4) $^ -Dlanguage=Python3 -visitor
run: $(MAINFILE).py
python3 $^
ex: $(MAINFILE).py
python3 $^ < myexample
test: all
python3 ./test_arith_visitor.py
clean:
find . \( -iname "~" -or -iname "*.cache*" -or -iname "*.diff" -or -iname "log.txt" -or -iname "*.pyc" -or -iname "*.tokens" -or -iname "*.interp" \) -exec rm -rf '{}' \;
rm -rf $(PACKAGE)*.py

View File

@ -0,0 +1,58 @@
from AritVisitor import AritVisitor
from AritParser import AritParser
class UnknownIdentifier(Exception):
pass
class MyAritVisitor(AritVisitor):
"""Visitor that evaluates an expression. Derives and overrides methods
from ArithVisitor (generated by ANTLR4)."""
def __init__(self):
self._memory = dict() # store id -> values
def visitNumberAtom(self, ctx):
try:
value = int(ctx.getText())
return value
except ValueError:
return float(ctx.getText())
def visitIdAtom(self, ctx):
try:
return self._memory[ctx.getText()]
except KeyError:
raise UnknownIdentifier(ctx.getText())
def visitMultiplicationExpr(self, ctx):
# Recursive calls to children. The visitor will choose the
# appropriate method (visitSomething) automatically.
leftval = self.visit(ctx.expr(0))
rightval = self.visit(ctx.expr(1))
# an elegant way to match the token:
if ctx.multop.type == AritParser.MULT:
return leftval * rightval
else:
return leftval / rightval
def visitAdditiveExpr(self, ctx):
leftval = self.visit(ctx.expr(0))
rightval = self.visit(ctx.expr(1))
if ctx.addop.type == AritParser.PLUS:
return leftval + rightval
else:
return leftval - rightval
def visitExprInstr(self, ctx):
val = self.visit(ctx.expr())
print('The value is ' + str(val))
def visitParens(self, ctx):
return self.visit(ctx.expr())
def visitAssignInstr(self, ctx):
val = self.visit(ctx.expr())
name = ctx.ID().getText()
print('now ' + name + ' has value ' + str(val))
self._memory[name] = val

View File

@ -0,0 +1,28 @@
from AritLexer import AritLexer
from AritParser import AritParser
# from AritVisitor import AritVisitor
from MyAritVisitor import MyAritVisitor, UnknownIdentifier
from antlr4 import InputStream, CommonTokenStream
import sys
# example of use of visitors to parse arithmetic expressions.
# stops when the first SyntaxError is launched.
def main():
lexer = AritLexer(InputStream(sys.stdin.read()))
stream = CommonTokenStream(lexer)
parser = AritParser(stream)
tree = parser.prog()
print("Parsing : done.")
visitor = MyAritVisitor()
try:
visitor.visit(tree)
except UnknownIdentifier as exc:
print('Unknown identifier: {}'.format(exc.args[0]))
exit(-1)
if __name__ == '__main__':
main()

View File

@ -0,0 +1,5 @@
1 ;
12 ;
1+2 ;
1+2*3+4;
(1+2)*(3+4);

View File

@ -0,0 +1,36 @@
#!/usr/bin/env python
from AritLexer import AritLexer
from AritParser import AritParser
import pytest
from MyAritVisitor import MyAritVisitor
from antlr4 import InputStream, CommonTokenStream
import sys
@pytest.mark.parametrize("input, expected", [
pytest.param('1+1;', 2),
pytest.param('2-1;', 1),
pytest.param('2*3;', 6),
pytest.param('6/2;', 3),
pytest.param('set x=42; x+1;', 43),
pytest.param('set x=42; set x=12; x+1;', 13)
])
def test_expr(input, expected):
lexer = AritLexer(InputStream(input))
stream = CommonTokenStream(lexer)
parser = AritParser(stream)
tree = parser.prog()
print("Parsing : done.")
visitor = MyAritVisitor()
def patched_visit(self, ctx):
self.last_expr = self.visit(ctx.expr())
visitor.visitExprInstr = patched_visit.__get__(visitor)
visitor.visit(tree)
assert visitor.last_expr == expected
if __name__ == '__main__':
pytest.main(sys.argv)

BIN
TP03/tp3.pdf Normal file

Binary file not shown.

16
TP03/tree/Tree.g4 Normal file
View File

@ -0,0 +1,16 @@
grammar Tree;
int_tree_top : int_tree EOF #top
;
int_tree: INT #leaf
| '(' INT int_tree+ ')' #node
;
INT: [0-9]+;
WS : (' '|'\t'|'\n')+ -> skip;