diff --git a/MiniC/.coveragerc b/MiniC/.coveragerc new file mode 100644 index 0000000..f1fcf13 --- /dev/null +++ b/MiniC/.coveragerc @@ -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: diff --git a/MiniC/.gitignore b/MiniC/.gitignore new file mode 100644 index 0000000..01c7c6d --- /dev/null +++ b/MiniC/.gitignore @@ -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 diff --git a/MiniC/Lib/Errors.py b/MiniC/Lib/Errors.py new file mode 100644 index 0000000..e466aa4 --- /dev/null +++ b/MiniC/Lib/Errors.py @@ -0,0 +1,18 @@ +class MiniCRuntimeError(Exception): + pass + + +class MiniCInternalError(Exception): + pass + + +class MiniCUnsupportedError(Exception): + pass + + +class MiniCTypeError(Exception): + pass + + +class AllocationError(Exception): + pass diff --git a/MiniC/Makefile b/MiniC/Makefile new file mode 100644 index 0000000..ee9b977 --- /dev/null +++ b/MiniC/Makefile @@ -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 diff --git a/MiniC/MiniC.g4 b/MiniC/MiniC.g4 new file mode 100644 index 0000000..a1dad31 --- /dev/null +++ b/MiniC/MiniC.g4 @@ -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 + ; + diff --git a/MiniC/MiniCC.py b/MiniC/MiniCC.py new file mode 100644 index 0000000..877364c --- /dev/null +++ b/MiniC/MiniCC.py @@ -0,0 +1,310 @@ +#! /usr/bin/env python3 +""" +Code generation lab, main file. Code Generation with Smart IRs. +Usage: + python3 MiniCC.py + 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) diff --git a/MiniC/README-interpreter.md b/MiniC/README-interpreter.md new file mode 100644 index 0000000..cf39f47 --- /dev/null +++ b/MiniC/README-interpreter.md @@ -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? diff --git a/MiniC/TP03/MiniCInterpretVisitor.py b/MiniC/TP03/MiniCInterpretVisitor.py new file mode 100644 index 0000000..49d10c3 --- /dev/null +++ b/MiniC/TP03/MiniCInterpretVisitor.py @@ -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()) diff --git a/MiniC/TP03/MiniCTypingVisitor.py b/MiniC/TP03/MiniCTypingVisitor.py new file mode 100644 index 0000000..f9b29f5 --- /dev/null +++ b/MiniC/TP03/MiniCTypingVisitor.py @@ -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() diff --git a/MiniC/TP03/tests/provided/examples-types/bad_def01.c b/MiniC/TP03/tests/provided/examples-types/bad_def01.c new file mode 100644 index 0000000..366bc74 --- /dev/null +++ b/MiniC/TP03/tests/provided/examples-types/bad_def01.c @@ -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 diff --git a/MiniC/TP03/tests/provided/examples-types/bad_type00.c b/MiniC/TP03/tests/provided/examples-types/bad_type00.c new file mode 100644 index 0000000..9d3580e --- /dev/null +++ b/MiniC/TP03/tests/provided/examples-types/bad_type00.c @@ -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 diff --git a/MiniC/TP03/tests/provided/examples-types/bad_type01.c b/MiniC/TP03/tests/provided/examples-types/bad_type01.c new file mode 100644 index 0000000..1a84239 --- /dev/null +++ b/MiniC/TP03/tests/provided/examples-types/bad_type01.c @@ -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 diff --git a/MiniC/TP03/tests/provided/examples-types/bad_type02.c b/MiniC/TP03/tests/provided/examples-types/bad_type02.c new file mode 100644 index 0000000..1a9796e --- /dev/null +++ b/MiniC/TP03/tests/provided/examples-types/bad_type02.c @@ -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 diff --git a/MiniC/TP03/tests/provided/examples-types/bad_type03.c b/MiniC/TP03/tests/provided/examples-types/bad_type03.c new file mode 100644 index 0000000..1dd7199 --- /dev/null +++ b/MiniC/TP03/tests/provided/examples-types/bad_type03.c @@ -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 diff --git a/MiniC/TP03/tests/provided/examples-types/bad_type04.c b/MiniC/TP03/tests/provided/examples-types/bad_type04.c new file mode 100644 index 0000000..7f71583 --- /dev/null +++ b/MiniC/TP03/tests/provided/examples-types/bad_type04.c @@ -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 + diff --git a/MiniC/TP03/tests/provided/examples-types/bad_type_bool_bool.c b/MiniC/TP03/tests/provided/examples-types/bad_type_bool_bool.c new file mode 100644 index 0000000..be2d14e --- /dev/null +++ b/MiniC/TP03/tests/provided/examples-types/bad_type_bool_bool.c @@ -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 diff --git a/MiniC/TP03/tests/provided/examples-types/double_decl00.c b/MiniC/TP03/tests/provided/examples-types/double_decl00.c new file mode 100644 index 0000000..5a4bbbd --- /dev/null +++ b/MiniC/TP03/tests/provided/examples-types/double_decl00.c @@ -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 diff --git a/MiniC/TP03/tests/provided/examples/bad_main.c b/MiniC/TP03/tests/provided/examples/bad_main.c new file mode 100644 index 0000000..a48a988 --- /dev/null +++ b/MiniC/TP03/tests/provided/examples/bad_main.c @@ -0,0 +1,11 @@ +#include "printlib.h" + +int toto(){ + println_int(42); + return 0; +} + +// EXITCODE 1 +// EXPECTED +// No main function in file + diff --git a/MiniC/TP03/tests/provided/examples/test_assign.c b/MiniC/TP03/tests/provided/examples/test_assign.c new file mode 100644 index 0000000..c7b4bee --- /dev/null +++ b/MiniC/TP03/tests/provided/examples/test_assign.c @@ -0,0 +1,13 @@ +#include "printlib.h" + +int main() +{ + int n,u; + n=17; + u=n; + println_int(n); + return 0; +} + +// EXPECTED +// 17 diff --git a/MiniC/TP03/tests/provided/examples/test_compare.c b/MiniC/TP03/tests/provided/examples/test_compare.c new file mode 100644 index 0000000..d5f9481 --- /dev/null +++ b/MiniC/TP03/tests/provided/examples/test_compare.c @@ -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 diff --git a/MiniC/TP03/tests/provided/examples/test_expr.c b/MiniC/TP03/tests/provided/examples/test_expr.c new file mode 100644 index 0000000..faed46f --- /dev/null +++ b/MiniC/TP03/tests/provided/examples/test_expr.c @@ -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 diff --git a/MiniC/TP03/tests/provided/examples/test_print.c b/MiniC/TP03/tests/provided/examples/test_print.c new file mode 100644 index 0000000..4ad499b --- /dev/null +++ b/MiniC/TP03/tests/provided/examples/test_print.c @@ -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 diff --git a/MiniC/TP03/tests/provided/examples/test_print_int.c b/MiniC/TP03/tests/provided/examples/test_print_int.c new file mode 100644 index 0000000..21015d6 --- /dev/null +++ b/MiniC/TP03/tests/provided/examples/test_print_int.c @@ -0,0 +1,9 @@ +#include "printlib.h" + +int main(){ + println_int(42); + return 0; +} + +// EXPECTED +// 42 diff --git a/MiniC/TP03/tests/provided/examples/test_str_assign.c b/MiniC/TP03/tests/provided/examples/test_str_assign.c new file mode 100644 index 0000000..d0eda85 --- /dev/null +++ b/MiniC/TP03/tests/provided/examples/test_str_assign.c @@ -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 diff --git a/MiniC/TP03/tests/provided/strcat/test_string01.c b/MiniC/TP03/tests/provided/strcat/test_string01.c new file mode 100644 index 0000000..5f5e1fd --- /dev/null +++ b/MiniC/TP03/tests/provided/strcat/test_string01.c @@ -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 + diff --git a/MiniC/TP03/tests/provided/strcat/test_string02.c b/MiniC/TP03/tests/provided/strcat/test_string02.c new file mode 100644 index 0000000..a90884a --- /dev/null +++ b/MiniC/TP03/tests/provided/strcat/test_string02.c @@ -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 diff --git a/MiniC/TP03/tests/provided/strcat/unititialized_str.c b/MiniC/TP03/tests/provided/strcat/unititialized_str.c new file mode 100644 index 0000000..beae5d5 --- /dev/null +++ b/MiniC/TP03/tests/provided/strcat/unititialized_str.c @@ -0,0 +1,13 @@ +#include "printlib.h" + +int main() { + string s; + println_string(s); + s = s + "Coucou"; + println_string(s); + return 0; +} + +// EXPECTED +// +// Coucou \ No newline at end of file diff --git a/MiniC/TP03/tests/provided/uninitialised/bool.c b/MiniC/TP03/tests/provided/uninitialised/bool.c new file mode 100644 index 0000000..b7c2168 --- /dev/null +++ b/MiniC/TP03/tests/provided/uninitialised/bool.c @@ -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 diff --git a/MiniC/TP03/tests/provided/uninitialised/float.c b/MiniC/TP03/tests/provided/uninitialised/float.c new file mode 100644 index 0000000..df033a3 --- /dev/null +++ b/MiniC/TP03/tests/provided/uninitialised/float.c @@ -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 \ No newline at end of file diff --git a/MiniC/TP03/tests/provided/uninitialised/int.c b/MiniC/TP03/tests/provided/uninitialised/int.c new file mode 100644 index 0000000..b3afe0c --- /dev/null +++ b/MiniC/TP03/tests/provided/uninitialised/int.c @@ -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 \ No newline at end of file diff --git a/MiniC/TP03/tests/students/README.md b/MiniC/TP03/tests/students/README.md new file mode 100644 index 0000000..f7c87d9 --- /dev/null +++ b/MiniC/TP03/tests/students/README.md @@ -0,0 +1 @@ +Add your own tests in this directory. \ No newline at end of file diff --git a/MiniC/printlib.h b/MiniC/printlib.h new file mode 100644 index 0000000..99d031c --- /dev/null +++ b/MiniC/printlib.h @@ -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); diff --git a/MiniC/pyproject.toml b/MiniC/pyproject.toml new file mode 100644 index 0000000..21ba6a3 --- /dev/null +++ b/MiniC/pyproject.toml @@ -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"] diff --git a/MiniC/test_expect_pragma.py b/MiniC/test_expect_pragma.py new file mode 100644 index 0000000..4480971 --- /dev/null +++ b/MiniC/test_expect_pragma.py @@ -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 " where + 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) diff --git a/MiniC/test_interpreter.py b/MiniC/test_interpreter.py new file mode 100644 index 0000000..bfee619 --- /dev/null +++ b/MiniC/test_interpreter.py @@ -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) diff --git a/PLANNING.md b/PLANNING.md index 6fb0175..72ed4aa 100644 --- a/PLANNING.md +++ b/PLANNING.md @@ -43,4 +43,12 @@ _Academic first semester 2022-2023_ - :book: 4th Course session: Friday 23/09/2022, 10:15. Amphi B (Ludovic Henrio) - * Typing [slides in english](course/CAP_cours04_typing.pdf). \ No newline at end of file + * 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 diff --git a/TP03/arith-visitor/.gitignore b/TP03/arith-visitor/.gitignore new file mode 100644 index 0000000..cfd98dc --- /dev/null +++ b/TP03/arith-visitor/.gitignore @@ -0,0 +1,2 @@ +/AritListener.py +/AritVisitor.py diff --git a/TP03/arith-visitor/Arit.g4 b/TP03/arith-visitor/Arit.g4 new file mode 100644 index 0000000..47c322b --- /dev/null +++ b/TP03/arith-visitor/Arit.g4 @@ -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; diff --git a/TP03/arith-visitor/Makefile b/TP03/arith-visitor/Makefile new file mode 100644 index 0000000..78daa82 --- /dev/null +++ b/TP03/arith-visitor/Makefile @@ -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 diff --git a/TP03/arith-visitor/MyAritVisitor.py b/TP03/arith-visitor/MyAritVisitor.py new file mode 100644 index 0000000..10b4590 --- /dev/null +++ b/TP03/arith-visitor/MyAritVisitor.py @@ -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 diff --git a/TP03/arith-visitor/arit.py b/TP03/arith-visitor/arit.py new file mode 100644 index 0000000..d488343 --- /dev/null +++ b/TP03/arith-visitor/arit.py @@ -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() diff --git a/TP03/arith-visitor/myexample b/TP03/arith-visitor/myexample new file mode 100644 index 0000000..53c1d21 --- /dev/null +++ b/TP03/arith-visitor/myexample @@ -0,0 +1,5 @@ +1 ; +12 ; +1+2 ; +1+2*3+4; +(1+2)*(3+4); diff --git a/TP03/arith-visitor/test_arith_visitor.py b/TP03/arith-visitor/test_arith_visitor.py new file mode 100644 index 0000000..47e4d4d --- /dev/null +++ b/TP03/arith-visitor/test_arith_visitor.py @@ -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) diff --git a/TP03/tp3.pdf b/TP03/tp3.pdf new file mode 100644 index 0000000..c5d590a Binary files /dev/null and b/TP03/tp3.pdf differ diff --git a/TP03/tree/Tree.g4 b/TP03/tree/Tree.g4 new file mode 100644 index 0000000..5698625 --- /dev/null +++ b/TP03/tree/Tree.g4 @@ -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; + + +