diff --git a/MiniC/.coveragerc b/MiniC/.coveragerc new file mode 100644 index 0000000..eb6325c --- /dev/null +++ b/MiniC/.coveragerc @@ -0,0 +1,11 @@ +[report] +exclude_lines = + pragma: no cover + raise MiniCInternalError + raise MiniCUnsupportedError + if debug: + if debug_graphs: + if self._debug: + if self.debug: + if self._debug_graphs: + if self.debug_graphs: diff --git a/MiniC/.gitignore b/MiniC/.gitignore new file mode 100644 index 0000000..ad8ad89 --- /dev/null +++ b/MiniC/.gitignore @@ -0,0 +1,20 @@ +/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 +log-*.txt +.eval-done.txt +*.py.diff +*.g4.diff 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..bb90ed4 --- /dev/null +++ b/MiniC/Makefile @@ -0,0 +1,123 @@ +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 FILTER='TP03/**/bad*.c' test +ifdef FILTER +export FILTER +endif + +# code generation mode +ifdef MODE +MINICC_OPTS+=--mode $(MODE) +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 + +doc: antlr + sphinx-apidoc -e -f -o doc/api . TP* replace_* *Wrapper* MiniC* conf* test* + make -C doc html + +test: test-interpret + + + +test-pyright: antlr + pyright . + +test-parse: test-pyright antlr + MINICC_OPTS="$(MINICC_OPTS) --mode=parse" python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py -k 'naive' + +test-typecheck: test-pyright antlr + MINICC_OPTS="$(MINICC_OPTS) --mode=typecheck" python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py -k 'naive' + +test-interpret: test-pyright test_interpreter.py main-deps + python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) test_interpreter.py + + +ifndef MODE +# The export must be on the same line as the command (note the ';'), because +# make starts a new shell for each line. +LINEAR=export MINICC_OPTS="${MINICC_OPTS} --mode codegen-linear"; +else +LINEAR= +endif + +# Test for naive allocator (also runs test_expect to check // EXPECTED directives): +test-naive: test-pyright antlr + $(LINEAR) python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py -k 'test_naive or test_expect' + +test-mem: test-pyright antlr + $(LINEAR) python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py -k 'test_alloc_mem' + +test-hybrid: test-pyright antlr + $(LINEAR) python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py -k 'test_alloc_hybrid' + +# Test for all but the smart allocator, i.e. everything that lab4 should pass: +test-lab4: test-pyright antlr + $(LINEAR) python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py -k 'not test_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 'test_smart' + +# Complete testsuite (should pass for lab5): +test-codegen: test-pyright antlr + python3 -m pytest $(PYTEST_BASE_OPTS) $(PYTEST_OPTS) ./test_codegen.py + +NAME_CLEAN = ${shell printf '%s' "$(MYNAME)" | tr -cd '[0-9a-zA-Z]'} +BASEDIR=$$(basename "$$PWD") +tar: clean + dir=$$(basename "$$PWD") && cd .. && \ + tar cvfz "$$dir"-$(NAME_CLEAN).tgz --exclude=".git" --exclude=".pytest_cache" \ + --exclude="htmlcov" "$$dir" + @echo "Created ../$$(basename "$$PWD")-$(NAME_CLEAN).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", "hybrid")): + try: + os.remove(s) + print("Removed {}".format(s)) + except OSError: + pass +endef +export CLEAN +clean-tests: + @python3 -c "$$CLEAN" + find . -iname "*.riscv" -print0 | xargs -0 rm -rf \; + +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 + +.PHONY: install-deps +install-deps: + python3 -m pip install antlr4-python3-runtime==4.13.1 pytest pytest-cov pytest-xdist coverage graphviz networkx pygraphviz + +.PHONY: test test-interpret test-codegen clean clean-tests tar antlr +# multiple invocations of pytest in parallel would share temporary file names +# and sometimes break. Parallelism can be achieved within pytest with +# pytest-xdist, which is efficient and safe. +.NOTPARALLEL: diff --git a/MiniC/MiniC.g4 b/MiniC/MiniC.g4 new file mode 100644 index 0000000..0fb24e9 --- /dev/null +++ b/MiniC/MiniC.g4 @@ -0,0 +1,138 @@ +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' (actually a 'return INT' because we don't have a ZERO +// lexical token). +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 100755 index 0000000..6d72ca7 --- /dev/null +++ b/MiniC/MiniCC.py @@ -0,0 +1,333 @@ +#! /usr/bin/python3 +""" +Evaluation and code generation labs, main file. +Usage: + python3 MiniCC.py --mode + python3 MiniCC.py --help +""" + +from __future__ import annotations + +from typing import cast +from enum import Enum + +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) + +import antlr4 +from antlr4.error.ErrorListener import ErrorListener + +from argparse import ArgumentParser +from traceback import print_exc +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 is_ssa(self) -> bool: + return self.value >= Mode.SSA.value + + +def valid_modes(): + modes = ['parse', 'typecheck', 'eval'] + + try: + import TP04.MiniCCodeGen3AVisitor # pyright: ignore # noqa: F401, type: ignore + modes.append('codegen-linear') + except ModuleNotFoundError: + return modes + + try: + import Lib.CFG # pyright: ignore # noqa: F401, type: ignore + modes.append('codegen-cfg') + except ModuleNotFoundError: + return modes + + try: + import TP05.EnterSSA # pyright: ignore # noqa: F401, type: ignore + modes.append('codegen-ssa') + except ModuleNotFoundError: + return modes + + try: + import TPoptim.OptimSSA # pyright: ignore # noqa: F401, type: ignore + modes.append('codegen-optim') + except ModuleNotFoundError: + 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, offendingSymbol, 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, dom_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 = antlr4.FileStream(inputname, encoding='utf-8') + lexer = MiniCLexer(input_s) + counter = CountErrorListener() + lexer._listeners.append(counter) + stream = antlr4.CommonTokenStream(lexer) + parser = MiniCParser(stream) + parser._listeners.append(counter) + tree = parser.prog() + if counter.count > 0: + exit(3) # Syntax or lexicography errors occurred, 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] + code = build_cfg(function) + if debug_graphs: + s = "{}.{}.dot".format(basename, code.fdata.get_name()) + print("CFG:", s) + code.print_dot(s, view=True) + if mode.is_ssa(): + from TP05.EnterSSA import enter_ssa # type: ignore[import] + from Lib.CFG import CFG # type: ignore[import] + enter_ssa(cast(CFG, code), dom_graphs, basename) + if ssa_graphs: + s = "{}.{}.enterssa.dot".format(basename, code.fdata.get_name()) + print("SSA:", s) + code.print_dot(s, view=True) + if mode == Mode.OPTIM: + from TPoptim.OptimSSA import OptimSSA # type: ignore[import] + OptimSSA(cast(CFG, code), debug=debug) + if ssa_graphs: + s = "{}.{}.optimssa.dot".format(basename, code.fdata.get_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 == "hybrid": + from TP04.HybridNaiveAllocator import HybridNaiveAllocator # type: ignore[import] + allocator = HybridNaiveAllocator(fdata) + comment = "hybrid, naive allocation" + elif reg_alloc == "smart": + liveness = None + if mode.is_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.is_ssa(): + from Lib.CFG import CFG # type: ignore[import] + from TP05.ExitSSA import exit_ssa # type: ignore[import] + exit_ssa(cast(CFG, code), reg_alloc == 'smart') + comment += " with SSA" + if allocator: + allocator.rewriteCode(code) + if mode.is_ssa() and ssa_graphs: + s = "{}.{}.exitssa.dot".format(basename, code.fdata.get_name()) + print("CFG after SSA:", s) + code.print_dot(s, view=True) + from Lib.LinearCode import LinearCode # type: ignore[import] + output.write(f"# Code generated by {os.path.realpath(sys.argv[0])}\n") + for v in os.environ: + if v.startswith("REPLACE_"): + output.write(f"# {v}={os.environ[v]}\n") + output.write(f"# Options: {' '.join(sys.argv[1:])}\n") + if isinstance(code, LinearCode): + code.print_code(output, comment=comment) + else: + from Lib.CFG import CFG # type: ignore[import] + from TP04.LinearizeCFG import linearize # type: ignore[import] + assert (isinstance(code, CFG)) + code.print_code(output, linearize=linearize, comment=comment) + if debug: + visitor3.printSymbolTable() + + +# command line management +if __name__ == '__main__': + + modes = valid_modes() + + parser = ArgumentParser(description='CAP/MIF08 MiniCC compiler') + + parser.add_argument('filename', type=str, + help='Source file.') + parser.add_argument('--mode', type=str, + choices=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', 'hybrid', '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 the CFG at SSA entry and exit.') + parser.add_argument('--dom-graphs', action='store_true', + default=False, + help='Display dominance-related 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 + dom_graphs = args.dom_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, dom_graphs) + except MiniCUnsupportedError as e: + print(e) + exit(5) + except (MiniCInternalError, AllocationError): + print_exc() + exit(4) diff --git a/MiniC/MiniCLexer.pyi b/MiniC/MiniCLexer.pyi new file mode 100644 index 0000000..6451f39 --- /dev/null +++ b/MiniC/MiniCLexer.pyi @@ -0,0 +1,16 @@ +from antlr4.error.ErrorListener import ErrorListener +from antlr4.Lexer import Lexer +from typing import Callable, Any, List + +class MiniCLexer(Lexer): + _listeners: List[ErrorListener] + EQ: int + LT: int + LTEQ: int + GT: int + GTEQ: int + PLUS: int + MINUS: int + MULT: int + DIV: int + MOD: int diff --git a/MiniC/MiniCParser.pyi b/MiniC/MiniCParser.pyi new file mode 100644 index 0000000..bc36f65 --- /dev/null +++ b/MiniC/MiniCParser.pyi @@ -0,0 +1,22 @@ +from antlr4.error.ErrorListener import ErrorListener +from antlr4.Parser import Parser +from typing import Callable, Any, List + +class MiniCParser(Parser): + _listeners: List[ErrorListener] + prog: Callable[[], Any] + EQ: int + LT: int + LTEQ: int + GT: int + GTEQ: int + PLUS: int + MINUS: int + MULT: int + DIV: int + MOD: int + NEQ: int + INTTYPE: int + FLOATTYPE: int + BOOLTYPE: int + STRINGTYPE: int diff --git a/MiniC/README-interpreter.md b/MiniC/README-interpreter.md new file mode 100644 index 0000000..3d6f6c5 --- /dev/null +++ b/MiniC/README-interpreter.md @@ -0,0 +1,32 @@ +# MiniC interpreter and typer +LAB3, MIF08 / CAP / CS444 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..c9716f6 --- /dev/null +++ b/MiniC/TP03/MiniCInterpretVisitor.py @@ -0,0 +1,185 @@ +# Visitor to *interpret* MiniC files +from typing import ( + Dict, + List) +from MiniCVisitor import MiniCVisitor +from MiniCParser import MiniCParser +from Lib.Errors import MiniCRuntimeError, MiniCInternalError, MiniCUnsupportedError + +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(f"Initialization for type {type_str}") + + 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( + f"Unknown comparison operator '{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( + f"Unknown additive operator '{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( + f"Unknown multiplicative operator '{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 = f"{val:.2f}" + 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()) + else: + raise MiniCUnsupportedError("Functions are not supported in evaluation mode") + + def visitFuncCall(self, ctx) -> None: # pragma: no cover + raise MiniCUnsupportedError("Functions are not supported in evaluation mode") diff --git a/MiniC/TP03/MiniCTypingVisitor.py b/MiniC/TP03/MiniCTypingVisitor.py new file mode 100644 index 0000000..f9c5fcd --- /dev/null +++ b/MiniC/TP03/MiniCTypingVisitor.py @@ -0,0 +1,148 @@ +# Visitor to *typecheck* MiniC files +from typing import List, NoReturn +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) -> NoReturn: + 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): + assert ctx.myop is not None + 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_bool 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..7ad9582 --- /dev/null +++ b/MiniC/TP03/tests/provided/examples-types/bad_type_bool_bool.c @@ -0,0 +1,9 @@ +#include "printlib.h" + +int main(){ + println_bool(true+true); + return 0; +} +// EXITCODE 2 +// EXPECTED +// In function main: Line 4 col 15: 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..3c3695a --- /dev/null +++ b/MiniC/TP03/tests/provided/examples/bad_main.c @@ -0,0 +1,12 @@ +#include "printlib.h" + +int toto(){ + println_int(42); + return 0; +} + +// SKIP TEST EXPECTED +// EXITCODE 5 +// EXPECTED +// Functions are not supported in evaluation mode + diff --git a/MiniC/TP03/tests/provided/examples/no_main.c b/MiniC/TP03/tests/provided/examples/no_main.c new file mode 100644 index 0000000..a57eeda --- /dev/null +++ b/MiniC/TP03/tests/provided/examples/no_main.c @@ -0,0 +1,7 @@ +#include "printlib.h" + +// SKIP TEST EXPECTED +// EXECCODE 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_basic_bool.c b/MiniC/TP03/tests/provided/examples/test_basic_bool.c new file mode 100644 index 0000000..c754dc8 --- /dev/null +++ b/MiniC/TP03/tests/provided/examples/test_basic_bool.c @@ -0,0 +1,33 @@ +#include "printlib.h" + +int main() { + println_bool(1 == 1); + println_bool(1 == 2); + println_bool(1 != 1); + println_bool(1 != 2); + println_bool(1 < 2); + println_bool(1 < 0); + println_bool(1 <= 2); + println_bool(1 <= 0); + println_bool(1 > 2); + println_bool(1 > 0); + println_bool(1 >= 2); + println_bool(1 >= 0); + println_bool(!(1 < 2)); + return 0; +} + +// EXPECTED +// 1 +// 0 +// 0 +// 1 +// 1 +// 0 +// 1 +// 0 +// 0 +// 1 +// 0 +// 1 +// 0 diff --git a/MiniC/TP03/tests/provided/examples/test_basic_expr.c b/MiniC/TP03/tests/provided/examples/test_basic_expr.c new file mode 100644 index 0000000..12d43c2 --- /dev/null +++ b/MiniC/TP03/tests/provided/examples/test_basic_expr.c @@ -0,0 +1,23 @@ +#include "printlib.h" + +int main() { + println_int(42); + println_int(3 - 2); + println_int(-42); + println_int(- -42); + println_int(10 / 3); + println_int(9 / 3); + println_int(8 / 3); + println_float(42.3); + return 0; +} + +// EXPECTED +// 42 +// 1 +// -42 +// 42 +// 3 +// 3 +// 2 +// 42.30 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..b01fbad --- /dev/null +++ b/MiniC/TP03/tests/provided/strcat/test_string01.c @@ -0,0 +1,15 @@ +#include "printlib.h" + +int main(){ + string x,y,z; + x = "ENS"; + y = "De Lyon"; + z = x + " " + y; + println_string(z); + return 0; +} + +// SKIP TEST EXPECTED +// 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..7ea28a7 --- /dev/null +++ b/MiniC/TP03/tests/provided/strcat/test_string02.c @@ -0,0 +1,17 @@ +#include "printlib.h" + +int main(){ + string n,m; + n = "foo"; + m = "bar"; + println_string(n); + println_string(m); + println_string(n + m); // non-standard C + return 0; +} + +// SKIP TEST EXPECTED +// 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..e1ceea4 --- /dev/null +++ b/MiniC/TP03/tests/provided/strcat/unititialized_str.c @@ -0,0 +1,14 @@ +#include "printlib.h" + +int main() { + string s; + println_string(s); + s = s + "Coucou"; + println_string(s); + return 0; +} + +// SKIP TEST EXPECTED +// EXPECTED +// +// Coucou diff --git a/MiniC/TP03/tests/provided/uninitialised/bool.c b/MiniC/TP03/tests/provided/uninitialised/bool.c new file mode 100644 index 0000000..6a80cec --- /dev/null +++ b/MiniC/TP03/tests/provided/uninitialised/bool.c @@ -0,0 +1,17 @@ +#include "printlib.h" + +int main() { + bool x; + if (x) { + println_int(1); + } + x = !x; + if (x) { + println_int(2); + } + return 0; +} + +// SKIP TEST EXPECTED +// 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/TP03/tests/students/ext-for-c/README.md b/MiniC/TP03/tests/students/ext-for-c/README.md new file mode 100644 index 0000000..f2a3430 --- /dev/null +++ b/MiniC/TP03/tests/students/ext-for-c/README.md @@ -0,0 +1 @@ +Add your own tests for the C-like 'for' loop in this directory (tests for the interpreter). diff --git a/MiniC/doc/Makefile b/MiniC/doc/Makefile new file mode 100644 index 0000000..d4bb2cb --- /dev/null +++ b/MiniC/doc/Makefile @@ -0,0 +1,20 @@ +# Minimal makefile for Sphinx documentation +# + +# You can set these variables from the command line, and also +# from the environment for the first two. +SPHINXOPTS ?= +SPHINXBUILD ?= sphinx-build +SOURCEDIR = . +BUILDDIR = _build + +# Put it first so that "make" without argument is like "make help". +help: + @$(SPHINXBUILD) -M help "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) + +.PHONY: help Makefile + +# Catch-all target: route all unknown targets to Sphinx using the new +# "make mode" option. $(O) is meant as a shortcut for $(SPHINXOPTS). +%: Makefile + @$(SPHINXBUILD) -M $@ "$(SOURCEDIR)" "$(BUILDDIR)" $(SPHINXOPTS) $(O) diff --git a/MiniC/doc/conf.py b/MiniC/doc/conf.py new file mode 100644 index 0000000..0c9f373 --- /dev/null +++ b/MiniC/doc/conf.py @@ -0,0 +1,61 @@ +# Configuration file for the Sphinx documentation builder. +# +# This file only contains a selection of the most common options. For a full +# list see the documentation: +# https://www.sphinx-doc.org/en/master/usage/configuration.html + +# -- Path setup -------------------------------------------------------------- + +# If extensions (or modules to document with autodoc) are in another directory, +# add these directories to sys.path here. If the directory is relative to the +# documentation root, use os.path.abspath to make it absolute, like shown here. +# +import os +import sys +sys.path.insert(0, os.path.abspath('..')) + + +# -- Project information ----------------------------------------------------- + +project = 'MiniC' +copyright = '2023, compil-lyon' +author = 'compil-lyon' + + +# -- General configuration --------------------------------------------------- + +# Add any Sphinx extension module names here, as strings. They can be +# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom +# ones. +extensions = [ + 'sphinx.ext.autodoc', + 'sphinx.ext.viewcode', +] + +autodoc_default_options = { + 'member-order': 'bysource', + 'members': True, + 'undoc-members': True, + 'ignore-module-all': True, +} + +# Add any paths that contain templates here, relative to this directory. +templates_path = ['_templates'] + +# List of patterns, relative to source directory, that match files and +# directories to ignore when looking for source files. +# This pattern also affects html_static_path and html_extra_path. +exclude_patterns = ['_build', 'Thumbs.db', '.DS_Store'] + + +# -- Options for HTML output ------------------------------------------------- + +# The theme to use for HTML and HTML Help pages. See the documentation for +# a list of builtin themes. +# +html_theme = 'sphinx_rtd_theme' + +# Add any paths that contain custom static files (such as style sheets) here, +# relative to this directory. They are copied after the builtin static files, +# so a file named "default.css" will overwrite the builtin "default.css". +html_static_path = ['_static'] diff --git a/MiniC/doc/index.rst b/MiniC/doc/index.rst new file mode 100644 index 0000000..ebe38e6 --- /dev/null +++ b/MiniC/doc/index.rst @@ -0,0 +1,77 @@ +.. MiniC documentation master file, created by + sphinx-quickstart on Thu Feb 3 16:47:38 2022. + You can adapt this file completely to your liking, but it should at least + contain the root `toctree` directive. + +Welcome to MiniC's documentation! +================================= + +.. toctree:: + :maxdepth: 4 + :caption: Contents: + + Base library - Errors + Base library - Statement + Base library - RISC-V instructions + Base library - Operands + Base library - Function data + Base library - Graphs + Linear intermediate representation + Temporary allocation + Control Flow Graph - CFG and Basic blocks + Control Flow Graph - Terminators + SSA form - Dominance frontier + SSA form - Phi Nodes + +These pages document the various Python sources in the Lib/ +folder of MiniC. You should not have to edit them *at all*. + +Base library +------------ + +The :doc:`api/Lib.Statement` defines various classes that represent +RISC-V assembly statements, such as labels or 3-address instructions. + +We won't create objects of these classes directly very often. +Instead, to easily create such statements for standard RISC-V assembly instructions +and pseudo-instructions, we give you the :doc:`api/Lib.RiscV`. + +RISC-V instructions take arguments of various kinds, +as defined in the :doc:`api/Lib.Operands`. + +At some point, we will need some basic functions about oriented and non oriented graphs, +those are present in :doc:`api/Lib.Graphes`. + +Linear Intermediate representation +---------------------------------- + +The :doc:`api/Lib.LinearCode` allows to work with assembly source code +modeled as a list of statements. + +Temporary allocation +-------------------- + +Before implementing the all-in-memory allocator of lab 4a, +you should understand the naive allocator in the :doc:`api/Lib.Allocator`. + +Control Flow Graph Intermediate representation +---------------------------------------------- + +The classes for the CFG and its basic blocks are in the :doc:`api/Lib.CFG`. +Each block ends with a terminator, as documented in the :doc:`api/Lib.Terminator`. + +SSA form +-------- + +The translation of the CFG into SSA form makes use of dominance frontiers. +Functions to work with dominance are defined in the :doc:`api/Lib.Dominators`. + +Phi nodes, a special kind of statement that appears in CFGs in SSA form, +are defined in the :doc:`api/Lib.PhiNode`. + +Indices and tables +================== + +* :ref:`genindex` +* :ref:`modindex` +* :ref:`search` diff --git a/MiniC/libprint.s b/MiniC/libprint.s new file mode 100644 index 0000000..01845bd --- /dev/null +++ b/MiniC/libprint.s @@ -0,0 +1,251 @@ + .globl println_int +println_int: + .globl println_bool +println_bool: + addi sp,sp,-8 + sd ra, 0(sp) + call print_int + call newline + ld ra, 0(sp) + addi sp,sp,8 + ret + + .globl println_char +println_char: + addi sp,sp,-8 + sd ra, 0(sp) + call print_char + call newline + ld ra, 0(sp) + addi sp,sp,8 + ret + + .text + .align 1 + .globl println_string + .type println_string, @function +println_string: #address stored in a0 + addi sp,sp,-152 + sd ra,24(sp) + sd s0,16(sp) + addi s0,sp,32 + sd t0,32(sp) + sd t1,40(sp) + sd t2,48(sp) + sd t3,56(sp) + sd t4,64(sp) + sd t5,72(sp) + sd t6,80(sp) + + sd a0,88(sp) + sd a1,96(sp) + sd a2,104(sp) + sd a3,112(sp) + sd a4,120(sp) + sd a5,128(sp) + sd a6,136(sp) + sd a7,144(sp) + + ## Argument is already in a0, just forward it to puts + call puts + + ld ra,24(sp) + ld s0,16(sp) + + ld t0,32(sp) + ld t1,40(sp) + ld t2,48(sp) + ld t3,56(sp) + ld t4,64(sp) + ld t5,72(sp) + ld t6,80(sp) + + ld a0,88(sp) + ld a1,96(sp) + ld a2,104(sp) + ld a3,112(sp) + ld a4,120(sp) + ld a5,128(sp) + ld a6,136(sp) + ld a7,144(sp) + + addi sp,sp,152 + jr ra + .size println_string, .-println_string + .section .rodata + .align 3 +fmt_int: + .string "%ld" +str_empty: + .string "" + .text + .align 1 + .globl print_int + .type print_int, @function +print_int: # print int stored in a0, saves/restores all scratch registers (except ft which we don't use) + addi sp,sp,-152 + + sd ra,24(sp) + sd s0,16(sp) + + sd t0,32(sp) + sd t1,40(sp) + sd t2,48(sp) + sd t3,56(sp) + sd t4,64(sp) + sd t5,72(sp) + sd t6,80(sp) + + sd a0,88(sp) + sd a1,96(sp) + sd a2,104(sp) + sd a3,112(sp) + sd a4,120(sp) + sd a5,128(sp) + sd a6,136(sp) + sd a7,144(sp) + + ## first parameter of print_int is second parameter of printf + mv a1,a0 + ## first parameter of printf is the format string + la a0,fmt_int + call printf + + ld ra,24(sp) + ld s0,16(sp) + + ld t0,32(sp) + ld t1,40(sp) + ld t2,48(sp) + ld t3,56(sp) + ld t4,64(sp) + ld t5,72(sp) + ld t6,80(sp) + + ld a0,88(sp) + ld a1,96(sp) + ld a2,104(sp) + ld a3,112(sp) + ld a4,120(sp) + ld a5,128(sp) + ld a6,136(sp) + ld a7,144(sp) + + addi sp,sp,152 + jr ra + .size print_int, .-print_int + .align 1 + .globl newline + .type newline, @function +newline: # print int stored in a0, saves/restores all scratch registers (except ft which we don't use) + addi sp,sp,-152 + + sd ra,24(sp) + sd s0,16(sp) + + sd t0,32(sp) + sd t1,40(sp) + sd t2,48(sp) + sd t3,56(sp) + sd t4,64(sp) + sd t5,72(sp) + sd t6,80(sp) + + sd a0,88(sp) + sd a1,96(sp) + sd a2,104(sp) + sd a3,112(sp) + sd a4,120(sp) + sd a5,128(sp) + sd a6,136(sp) + sd a7,144(sp) + + ## first parameter of printf is the format string + la a0,str_empty + call puts + + ld ra,24(sp) + ld s0,16(sp) + + ld t0,32(sp) + ld t1,40(sp) + ld t2,48(sp) + ld t3,56(sp) + ld t4,64(sp) + ld t5,72(sp) + ld t6,80(sp) + + ld a0,88(sp) + ld a1,96(sp) + ld a2,104(sp) + ld a3,112(sp) + ld a4,120(sp) + ld a5,128(sp) + ld a6,136(sp) + ld a7,144(sp) + + addi sp,sp,152 + jr ra + .size newline, .-newline + .align 1 + .globl print_char + .type print_char, @function +print_char: # print char stored in a0 (ascii code) + addi sp,sp,-152 + sd ra,24(sp) + sd s0,16(sp) + + addi s0,sp,32 + + + sd t0,32(sp) + sd t1,40(sp) + sd t2,48(sp) + sd t3,56(sp) + sd t4,64(sp) + sd t5,72(sp) + sd t6,80(sp) + + sd a0,88(sp) + sd a1,96(sp) + sd a2,104(sp) + sd a3,112(sp) + sd a4,120(sp) + sd a5,128(sp) + sd a6,136(sp) + sd a7,144(sp) + + # call to putchar + mv a5,a0 + sb a5,-17(s0) + lbu a5,-17(s0) + sext.w a5,a5 + mv a0,a5 + call putchar + + #restore registers + ld ra,24(sp) + ld s0,16(sp) + + ld t0,32(sp) + ld t1,40(sp) + ld t2,48(sp) + ld t3,56(sp) + ld t4,64(sp) + ld t5,72(sp) + ld t6,80(sp) + + ld a0,88(sp) + ld a1,96(sp) + ld a2,104(sp) + ld a3,112(sp) + ld a4,120(sp) + ld a5,128(sp) + ld a6,136(sp) + ld a7,144(sp) + + addi sp,sp,152 + jr ra + .size print_char, .-print_char + + diff --git a/MiniC/printlib.h b/MiniC/printlib.h new file mode 100644 index 0000000..ea43937 --- /dev/null +++ b/MiniC/printlib.h @@ -0,0 +1,21 @@ +#include +/** + * 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); + +#define print_float(f) do { printf("%.2f", f); } while(0) +#define println_float(f) do { printf("%.2f\n", f); } while(0) + +void print_string(string); +void println_string(string); diff --git a/MiniC/pyproject.toml b/MiniC/pyproject.toml new file mode 100644 index 0000000..883b26b --- /dev/null +++ b/MiniC/pyproject.toml @@ -0,0 +1,2 @@ +[tool.pyright] +ignore = ["replace_cfgssa.py", "replace_smart_alloc.py", "replace_sequentialize_moves.py", "replace_target_api.py", "test_replace_mem.py", "MiniCCWrapper.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..bc5aca9 --- /dev/null +++ b/MiniC/test_expect_pragma.py @@ -0,0 +1,317 @@ +import collections +from pathlib import PurePath +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) + +ASM = 'riscv64-unknown-elf-gcc' +SIMU = 'spike' + +LIBPRINT = 'libprint.s' +if 'LIBPRINT' in os.environ: + LIBPRINT = os.environ['LIBPRINT'] + + +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] + + +def filter_pathnames(pathlist, pattern): + return tuple(path for path in pathlist if PurePath(path).match(pattern)) + + +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 skip_if_partial_match(self, actual, expect, ignore_error_message): + if not ignore_error_message: + return False + + 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) + + +class TestCompiler(object): + DISABLE_CODEGEN: bool + SKIP_NOT_IMPLEMENTED: bool + MINIC_COMPILE: str + MINICC_OPTS: list[str] + + 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 run_with_gcc(self, file, info): + return self.compile_and_simulate(file, info, reg_alloc='gcc', use_gcc=True) + + def compile_with_gcc(self, file, output_name): + print("Compiling with GCC...") + result = self.run_command( + [ASM, '-S', '-I./', + '--output=' + output_name, + '-Werror', + '-Wno-div-by-zero', # We need to accept 1/0 at compile-time + file]) + print(result.output) + print("Compiling with GCC... DONE") + return result + + def compile_and_simulate(self, file, info, reg_alloc, + use_gcc=False): + basename, _ = os.path.splitext(file) + output_name = basename + '-' + reg_alloc + '.s' + if use_gcc: + result = self.compile_with_gcc(file, output_name) + if result.exitcode != 0: + # We don't consider the exact exitcode, and ignore the + # output (our error messages may be different from + # GCC's) + return result._replace(exitcode=1, + output=None) + else: + result = self.compile_with_ours(file, output_name, reg_alloc) + if (self.DISABLE_CODEGEN or + reg_alloc == 'none' or + info.exitcode != 0 or result.exitcode != 0): + # Either the result is meaningless, or we already failed + # and don't need to go any further: + return result + # Only executable code past this point. + exec_name = basename + '-' + reg_alloc + '.riscv' + return self.link_and_run(output_name, exec_name, info) + + def link_and_run(self, output_name, exec_name, info): + self.remove(exec_name) + cmd = [ + ASM, output_name, LIBPRINT, + '-o', exec_name + ] + info.linkargs + print(info) + print("Assembling and linking " + output_name + ": " + ' '.join(cmd)) + try: + subprocess.check_output(cmd, timeout=60, stderr=subprocess.STDOUT) + except subprocess.CalledProcessError as e: + print("Assembling failed:\n") + print(e.output.decode()) + print("Assembler code below:\n") + cat(output_name) + pytest.fail() + assert (os.path.isfile(exec_name)) + sys.stdout.write("Assembling and linking ... OK\n") + try: + result = self.run_command( + [SIMU, + '-m100', # Limit memory usage to 100MB, more than enough and + # avoids crashing on a VM with <= 2GB RAM for example. + 'pk', + exec_name], + scope="runtime") + output = re.sub(r'bbl loader\r?\n', '', result.output) + print("Output of the program:") + print(output) + return testinfo(execcode=result.execcode, + exitcode=result.exitcode, + output=output, + linkargs=[], + skip_test_expected=False) + except subprocess.TimeoutExpired: + pytest.fail("Timeout executing program. Infinite loop in generated code?") + + def compile_with_ours(self, file, output_name, reg_alloc): + print("Compiling ...") + self.remove(output_name) + alloc_opt = '--reg-alloc=' + reg_alloc + out_opt = '--output=' + output_name + cmd = [sys.executable, self.MINIC_COMPILE] + if not self.DISABLE_CODEGEN: + cmd += [out_opt, alloc_opt] + cmd += self.MINICC_OPTS + cmd += [file] + result = self.run_command(cmd) + print(' '.join(cmd)) + print("Exited with status:", result.exitcode) + print(result.output) + if result.exitcode == 4: + if "AllocationError" in result.output: + if reg_alloc == 'naive': + pytest.skip("Too big for the naive allocator") + else: + pytest.skip("Offsets too big to be manipulated") + elif ("NotImplementedError" in result.output and + self.SKIP_NOT_IMPLEMENTED): + pytest.skip("Feature not implemented in this compiler") + if result.exitcode != 0: + # May either be a failing test or a test with expected + # compilation failure (bad type, ...). Let the caller + # do the assertion and decide: + return result + if not self.DISABLE_CODEGEN: + assert os.path.isfile(output_name) + print("Compiling ... OK") + return result diff --git a/MiniC/test_interpreter.py b/MiniC/test_interpreter.py new file mode 100755 index 0000000..1da3fe2 --- /dev/null +++ b/MiniC/test_interpreter.py @@ -0,0 +1,100 @@ +#! /usr/bin/env python3 +import pytest +import glob +import os +import sys +from test_expect_pragma import ( + TestExpectPragmas, cat, + TestCompiler, filter_pathnames + ) + +HERE = os.path.dirname(os.path.realpath(__file__)) +if HERE == os.path.realpath('.'): + HERE = '.' +TEST_DIR = HERE +IMPLEM_DIR = HERE + +SKIP_EXPECT = False +if 'SKIP_EXPECT' in os.environ: + SKIP_EXPECT = True + +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') + +if 'FILTER' in os.environ: + ALL_FILES = filter_pathnames(ALL_FILES, os.environ['FILTER']) + + +class TestInterpret(TestExpectPragmas, TestCompiler): + DISABLE_CODEGEN = False + + def evaluate(self, file): + if not DISABLE_TYPECHECK: + res = self.run_command([sys.executable, MINIC_EVAL, + "--mode", "eval", file]) + else: + res = self.run_command([sys.executable, MINIC_EVAL, + "--mode", "eval", + "--disable-typecheck", file]) + if res.exitcode == 1: + # Execution can't distinguish exit code at runtime and static + # rejection of the program. But we know that an exit code of 1 is + # reserved for runtime errors, hence convert this exitcode + # into an execcode. + res = res._replace(exitcode=0, execcode=1) + return res + + # Not in test_expect_pragma to get assertion rewritting + def assert_equal(self, actual, expected, compiler): + if expected.output is not None and actual.output is not None: + assert actual.output == expected.output, \ + f"Output of the program is incorrect (using {compiler})." + assert actual.exitcode == expected.exitcode, \ + f"Exit code of the compiler ({compiler}) is incorrect" + assert actual.execcode == expected.execcode, \ + f"Exit code of the execution is incorrect (using {compiler})" + + @pytest.mark.parametrize('filename', ALL_FILES) + def test_expect(self, filename): + """Test the EXPECTED annotations in test files by launching the + program with GCC.""" + if SKIP_EXPECT: + pytest.skip("Skipping all test_expect " + "because $SKIP_EXPECT is set.") + cat(filename) # For diagnosis + expect = self.get_expect(filename) + if expect.skip_test_expected: + pytest.skip("Skipping test_expect with GCC because " + "the test contains SKIP TEST EXPECTED") + if expect.exitcode != 0: + # GCC is more permissive than us, so trying to compile an + # incorrect program would bring us no information (it may + # compile, or fail with a different message...) + pytest.skip("Not testing the expected value for " + "tests expecting exitcode!=0") + gcc_result = self.run_with_gcc(filename, expect) + self.assert_equal(gcc_result, expect, "gcc") + + @pytest.mark.parametrize('filename', ALL_FILES) + def test_eval(self, filename): + cat(filename) # For diagnosis + expect = self.get_expect(filename) + actual = self.evaluate(filename) + if expect: + self.assert_equal(actual, expect, "MiniCC") + + +if __name__ == '__main__': + pytest.main(sys.argv) 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 100755 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..84a6c46 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; + + +