diff --git a/.circleci/config.yml b/.circleci/config.yml index c745b59..dbea0cf 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -2,11 +2,13 @@ version: 2 jobs: build: docker: - - image: circleci/python:3.7.1 + - image: circleci/python:3.7.2 steps: - checkout - run: | pip install --user -r requirements.txt - sudo pip install nose + sudo pip install pytest + sudo pip install pycodestyle - run: | - nosetests \ No newline at end of file + pytest -vv -p no:cacheprovider + pycodestyle src/* --ignore=E501 \ No newline at end of file diff --git a/.gitignore b/.gitignore index aa50240..41f1a37 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,7 @@ __pycache__/ out/ \.vscode/ +\.pytest_cache/ site/ build/ diff --git a/.hooks/pre-commit.sh b/.hooks/pre-commit.sh new file mode 100755 index 0000000..6c1953a --- /dev/null +++ b/.hooks/pre-commit.sh @@ -0,0 +1,29 @@ +#!/bin/sh + +# Pytest +echo "Running pre-commit hooks" +pytest -vv -p no:cacheprovider + +if [ $? -ne 0 ]; then + echo "Tests must pass before committing" + exit 1 +fi +#### + +# Pycodestyle +pycodestyle src/* --ignore=E501 + +if [ $? -ne 0 ]; then + echo "Tests must pass before committing" + exit 1 +fi +#### + +# Prevent master commit +branch="$(git rev-parse --abbrev-ref HEAD)" + +if [ "$branch" = "master" ]; then + echo "You can't commit directly to master branch" + exit 1 +fi +#### \ No newline at end of file diff --git a/README.md b/README.md index e5fb73e..128f2fc 100644 --- a/README.md +++ b/README.md @@ -4,7 +4,7 @@ ___ [![License: GPL v3](https://img.shields.io/badge/license-GPL%20v3-blue.svg)](https://www.gnu.org/licenses/gpl-3.0) -[![Version](https://img.shields.io/badge/version-0.2.0-brightgreen.svg)](https://github.com/hassanalinali/Lesma/blob/master/LICENSE.md) +[![Version](https://img.shields.io/badge/version-0.2.1-brightgreen.svg)](https://github.com/hassanalinali/Lesma/blob/master/LICENSE.md) [![CircleCI](https://circleci.com/gh/hassanalinali/Lesma/tree/master.svg?style=shield)](https://circleci.com/gh/hassanalinali/Lesma/tree/master) [![Codacy Badge](https://api.codacy.com/project/badge/Grade/90fcc06be70d4dd98f54f1bb2713d70c)](https://www.codacy.com/app/hassanalinali/Lesma?utm_source=github.com&utm_medium=referral&utm_content=hassanalinali/Lesma&utm_campaign=Badge_Grade) @@ -15,7 +15,7 @@ Currently an early Work in Progress, and **many thanks** to [Ayehavgunne](https: ## Features - **it's fast**, because it should be so, but it won't ever oblige you to make an extra effort for the sake of performance - **it's compiled**, so you can finally distribute your projects without dependencies, and because binary size also matters, a Hello World example would be around 8kb -- **it's statically typed** so you don't need to guess the type of the variable if your coworker didn't spend the time to use meaningful names +- **it's statically typed** so you don't need to guess the type of the variable if your coworker didn't spend the time to use meaningful names and you can make use of compile-time checks, autocomplete and more - **it's simple and expressive** because the code should be easily readable and it shouldn't make you guess what it does ## Influences @@ -54,9 +54,9 @@ python les.py run test.les python les.py compile test.les ``` -Or run the unit tests using nosetests +Or install pytest and run the unit tests yourself ```bash -nosetests +pytest ``` For advanced usage or help, consult the CLI help menu diff --git a/docs/TODO.md b/docs/TODO.md index e356208..40aa8aa 100644 --- a/docs/TODO.md +++ b/docs/TODO.md @@ -1,40 +1,33 @@ # TODO ## Fixes -- [x] Fix **break** and **continue** not branching if the parent block is not the loop -- [x] Fix constant declaration not allowing types - [ ] Fix Type declaration not expecting square brackets (for lists) - [ ] Fix input function -- [x] Should not allow declaring the type to an existing variable - [ ] Fix not being able to return user-defined structs and classes - [ ] Fix not being able to overload operators on user-defined structs and classes -- [x] Fix Python error on empty input - [ ] Unicode doesn't work on Windows platforms - [ ] Fix string and list type declaration not working +- [ ] Fix base unary operators being applied before user defined ones ## Improvements - [ ] Allow any type for lists/tuples (currently only int) -- [ ] Allow any type for range (currently only int) -- [x] Allow any type for casting -- [x] Change casting syntax - [ ] Allow more operators on different types such as strings - [ ] Improve warning messages - [ ] Add indentation related errors -- [ ] Add docs for as and is +- [x] Add docs for as and is - [ ] Remove clang as a dependency -- [x] Change from anonymous structs to identified (to allow proper struct types) - [ ] Move error messages from source files to typechecker +- [ ] Fix array types not working and empty lists +- [ ] Catch struct/class used parameters that are not initialized +- [ ] Add support for functions with same name but different parameters ## Features -- [ ] Implement Null (maybe) +- [ ] Implement Null (maybe someday) - [ ] Implement Tuples - [ ] Implement Dictionary -- [ ] Implement Empty lists - [ ] Implement 'in' as a boolean result - [ ] Implement anonymous functions -- [x] Implement alias - [ ] Implement Closure - [ ] Implement string interpolation - [ ] Implement Enums -- [x] Implement unsigned numbers -- [x] Implement operator overloading \ No newline at end of file +- [ ] Implement defer keyword \ No newline at end of file diff --git a/docs/examples.md b/docs/examples.md index 238c58e..c7e0d62 100644 --- a/docs/examples.md +++ b/docs/examples.md @@ -144,7 +144,7 @@ print(start_at_5(4)) print(start_at_27(15)) # User input -Int age = input('How old are you?') +age: int = input('How old are you?') # String Interpolation print('Wow! You are {age} years old?!') @@ -157,17 +157,19 @@ def operator - (x: int) -> int # One parameters overloads binary operations return 0 - x + 1 # Extern functions (FFI) -def extern abs(x: int32) -> int32 # from C's stdlib +def extern abs(x: int) -> int # from C's stdlib -print(abs(-5 as int32)) # ints are int64 by default in Lesma, they're int32 in C +print(abs(-5.0 as int)) # ints are int64 by default in Lesma, they're int32 in C # or you can just let Lesma convert between "compatible" types such as numbers -print(abs(-5)) +print(abs(-5.0)) # Named parameters and defaults def optional_params(x: int, y: int32 = 5, z: double = 9) -> int # Lesma takes care of casting the return type between "compatible" types - return x + z + return x + z + +optional_params(5, z=11) # Enums enum Colors @@ -182,7 +184,7 @@ struct Circle x: int y: int -cir: Circle = {radius=5, x=2, y=4} +cir: Circle = Circle(radius=5, x=2, y=4) print(cir.radius) diff --git a/docs/features/keywords.md b/docs/features/keywords.md index ce7b554..8ad71ec 100644 --- a/docs/features/keywords.md +++ b/docs/features/keywords.md @@ -8,7 +8,7 @@ For reference, here are all the keywords categorized by type. | if | else | for | while | | switch | case | default | def | | const | break | continue | pass -| void | alias | +| void | alias | extern | operator | ## Operator keywords diff --git a/docs/features/operators.md b/docs/features/operators.md index 4f4e251..d60d72f 100644 --- a/docs/features/operators.md +++ b/docs/features/operators.md @@ -24,9 +24,9 @@ Lesma offers the standard mathematical and boolean operators |And| `and`| And| |Or| `or`| Or| |Xor| `xor`| Exclusive Or| -|Is| `is` | Is type operator| +|Is| `is` | Type checking operator| |As| `as` | Casting operator| -|Not| `not` | Not bool operator| +|Not| `not` | Not boolean operator| |Equals| `==` | Value equality| |Not Equals| `!=` | Value inequality| |Less Than| `<` | Less than value| diff --git a/docs/features/syntax.md b/docs/features/syntax.md index 5a6f183..811f142 100644 --- a/docs/features/syntax.md +++ b/docs/features/syntax.md @@ -1,5 +1,5 @@ -- Whitespace is signifiant, indentation uses either Tabs or exactly 4 spaces -- Flow control statements, structs, classes and functions require an indentation +- Whitespace is signifiant, indentation uses either tabs or exactly 4 spaces +- Flow control statements, structs, classes and functions require indentation - Lesma's Checker will report any invalid syntax or unrecommended behaviour, such incompatible types for operations, or unused variables. - `_` variable name is used as an ignored result, and is treated differently by the compiler (similar to golang) diff --git a/docs/features/types.md b/docs/features/types.md index 73f4132..60dcb3e 100644 --- a/docs/features/types.md +++ b/docs/features/types.md @@ -2,24 +2,16 @@ Types are optional in Lesma, you can choose whether you specify them or not. Uns Operations between different types will either be casted to the larger type if the two types are compatible, or give an error otherwise. Two types are compatible if they are different sizes of the same group type (such as ints or floating points). +The type must be either a user-defined alias, a struct or class, or a built-in type. + !!! warning Types that are not specified are inferred, this is fundamentally different to dynamic types! -The type must be either a user-defined alias, struct or class, or a built-in type such as: - -- `Any` -- `Int` -- `Double` -- `Float` -- `Str` -- `Bool` -- `List` -- `Tuple` -- `Dict` -- `Range` +--- +## Built-in Types -## Any +### Any Any types can receive any kind of value, so it can receive any kind of value at any moment. @@ -31,7 +23,7 @@ x = "Hey there" !!! warning Any not implemented yet! -## Int +### Int There are multiple types of ints available based on width and signed/unsigned. They can get a minimum of 8 bits width and a maximum of 128. If the width is not specified, it's by default 64. - Signed: `int`, `int8`, `int16`, `int32`, `int64`, `int128` - Unsigned: `uint`, `uint8`, `uint16`, `uint32`, `uint64`, `uint128` @@ -45,21 +37,21 @@ large_num: int128 = 5 !!! info In Lesma, int and uint are by default 64 bits wide, and they will enlarge by themselves to 128 bits wide if needed. This is not the default behaviour if you specify the size yourself! -## Float +### Float Float is floating-point real number. ```py x: float = 0.5 ``` -## Double +### Double Double is double-precision floating-point real number. ```py x: double = 172312.41923 ``` -## Str +### Str Str is Lesma's implementation of strings/char lists. All Lesma strings support UTF-8. ```py @@ -68,13 +60,13 @@ x = '🍌' x = '夜のコンサートは最高でした。' ``` -## Bool +### Bool Bools occupy only 1 bit, and can be either `true` or `false`. ```py x: bool = true ``` -## List +### List Lists are mutable by default, are declared using square paranthesis, have dynamic size, start from 0, and the members are accessed using the their index around square paranthesis. ```py @@ -85,7 +77,7 @@ print(x[2]) !!! warning Lists currently only support integers, no other types! -## Tuple +### Tuple Tuples are like lists, but immutable, and declared using round paranthesis. ```py @@ -96,7 +88,7 @@ print(x[0]) !!! warning Tuples are not yet implemented! -## Dict +### Dict Dictionaries are lists of key-value pairs (similar to hashmaps), and they're mutable ```py @@ -108,7 +100,7 @@ print(x['first_name']) !!! warning Dicts are not yet implemented! -## Range +### Range Ranges are similar to Python's ranges, defined using `start..end` kind of syntax, and they're especially used for loops ```py @@ -117,4 +109,24 @@ for x in 0..100 ``` !!! warning - Ranges can not currently be assigned to variables \ No newline at end of file + Ranges can not currently be assigned to variables + +---- + +## Type Operations + +### Is +`Is` binary operator checks if the left operand's type matches the right operand and returns a bool as a result + +```py +x: int = 5 +print(x is int) +``` + +### As +`As` binary operator casts the left operand to the right operand type and returns the casted value + +```py +x: float = 5.5 +print(x as int) # Should print 5 +``` diff --git a/docs/index.md b/docs/index.md index f73d22d..b95d841 100644 --- a/docs/index.md +++ b/docs/index.md @@ -10,7 +10,7 @@ Currently an early Work in Progress, and many thanks to [Ayehavgunne](https://gi ## Features - **it's fast**, because it should be so, but it won't ever oblige you to make an extra effort for the sake of performance - **it's compiled**, so you can finally distribute your projects without dependencies, and because binary size also matters, a Hello World example would be around 8kb -- **it's statically typed** so you don't need to guess the type of the variable if your coworker didn't spend the time to use meaningful names +- **it's statically typed** so you don't need to guess the type of the variable if your coworker didn't spend the time to use meaningful names and you can make use of compile-time checks, autocomplete and more - **it's simple and expressive** because the code should be easily readable and it shouldn't make you guess what it does ## Influences diff --git a/requirements.txt b/requirements.txt index 1c73099..cbb7eeb 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,3 +1,3 @@ halo==0.0.22 -llvmlite==0.26.0 +llvmlite==0.27.0 docopt==0.6.2 \ No newline at end of file diff --git a/setup_env.sh b/setup_env.sh new file mode 100755 index 0000000..be7b466 --- /dev/null +++ b/setup_env.sh @@ -0,0 +1,5 @@ +#!/bin/sh + +# Setup Git Hooks +cd .git/hooks/ +ln -s -f ../../.hooks/pre-commit.sh ./pre-commit diff --git a/src/les.py b/src/les.py index 6759803..33b1a43 100644 --- a/src/les.py +++ b/src/les.py @@ -32,7 +32,7 @@ def _run(arg_list): error(les_file + " is not a valid file") return - code = open(les_file).read() + code = open(les_file, encoding="utf8").read() lexer = Lexer(code, les_file) parser = Parser(lexer) t = parser.parse() @@ -55,7 +55,7 @@ def _compile(arg_list): return les_file = os.path.abspath(les_file) - code = open(les_file).read() + code = open(les_file, encoding="utf8").read() lexer = Lexer(code, les_file) parser = Parser(lexer) t = parser.parse() @@ -68,7 +68,7 @@ def _compile(arg_list): if __name__ == "__main__": - args = docopt(__doc__, version='0.2.0') + args = docopt(__doc__, version='0.2.1') if args['compile']: _compile(args) diff --git a/src/lesma/ast.py b/src/lesma/ast.py index 66ac647..1f0b2b9 100644 --- a/src/lesma/ast.py +++ b/src/lesma/ast.py @@ -72,6 +72,7 @@ def __init__(self, name, return_type, parameters, line_num, varargs=None): self.varargs = varargs self.line_num = line_num + class AnonymousFunc(AST): def __init__(self, return_type, parameters, body, line_num, parameter_defaults=None, varargs=None): self.return_type = return_type @@ -117,12 +118,6 @@ def __init__(self, name, fields, line_num): self.line_num = line_num -class StructLiteral(AST): - def __init__(self, fields, line_num): - self.fields = fields - self.line_num = line_num - - class ClassDeclaration(AST): def __init__(self, name, base=None, constructor=None, methods=None, class_fields=None, instance_fields=None): self.name = name diff --git a/src/lesma/compiler/code_generator.py b/src/lesma/compiler/code_generator.py index 2bc548d..f80c516 100644 --- a/src/lesma/compiler/code_generator.py +++ b/src/lesma/compiler/code_generator.py @@ -6,7 +6,7 @@ import subprocess from llvmlite import ir from lesma.grammar import * -from lesma.ast import CollectionAccess, DotAccess, Input, StructLiteral, VarDecl, Str +from lesma.ast import CollectionAccess, DotAccess, Input, VarDecl, Str from lesma.compiler import RET_VAR, type_map from lesma.compiler.operations import unary_op, binary_op, cast_ops from lesma.compiler.builtins import define_builtins @@ -35,6 +35,7 @@ def __init__(self, file_name): self.loop_test_blocks = [] self.loop_end_blocks = [] self.is_break = False + self.in_class = False llvm.initialize() llvm.initialize_native_target() llvm.initialize_native_asmprinter() @@ -78,6 +79,10 @@ def visit_externfuncdecl(self, node): self.externfuncdecl(node.name, node) def externfuncdecl(self, name, node): + for func in self.module.functions: + if func.name == name: + self.define(name, func, 1) + return return_type = node.return_type parameters = node.parameters varargs = node.varargs @@ -114,9 +119,11 @@ def visit_funccall(self, node): func_type = func_type.type.pointee name = self.search_scopes(node.name) name = name.name + elif isinstance(func_type, ir.IdentifiedStructType): + return self.struct_assign(node) else: name = node.name - + if len(node.arguments) < len(func_type.args): args = [] args_supplied = [] @@ -138,7 +145,7 @@ def visit_funccall(self, node): else: if set(node.named_arguments.keys()) & set(args_supplied): raise TypeError('got multiple values for argument(s) {}'.format(set(node.named_arguments.keys()) & set(args_supplied))) - + args.append(self.comp_cast( self.visit(func_type.parameter_defaults[arg_names[x]]), self.visit(func_type.parameters[arg_names[x]]), @@ -156,7 +163,7 @@ def visit_funccall(self, node): def comp_cast(self, arg, typ, node): if types_compatible(str(arg.type), typ): return cast_ops(self, arg, typ, node) - + return arg def visit_compound(self, node): @@ -170,10 +177,7 @@ def visit_compound(self, node): def visit_structdeclaration(self, node): fields = [] for field in node.fields.values(): - if field.value == STR: - raise NotImplementedError - else: - fields.append(type_map[field.value]) + fields.append(type_map[field.value]) struct = self.module.context.get_identified_type(node.name) struct.fields = [field for field in node.fields.keys()] @@ -181,9 +185,25 @@ def visit_structdeclaration(self, node): struct.set_body([field for field in fields]) self.define(node.name, struct) + def visit_classdeclaration(self, node): + self.in_class = True + + fields = [] + for field in node.class_fields.values(): + fields.append(type_map[field.value]) + + classdecl = self.module.context.get_identified_type(node.name) + classdecl.fields = [field for field in node.class_fields.keys()] + classdecl.name = 'class.' + node.name + classdecl.set_body([field for field in fields]) + + # self.funcdecl('class.{}.new'.format(node.name), node.constructor) + self.in_class = False + self.define(node.name, classdecl) + def visit_typedeclaration(self, node): raise NotImplementedError - + def visit_incrementassign(self, node): collection_access = None key = None @@ -199,7 +219,7 @@ def visit_incrementassign(self, node): pointee = self.search_scopes(var_name).type.pointee op = node.op temp = ir.Constant(var.type, 1) - + if isinstance(pointee, ir.IntType): if op == PLUS_PLUS: res = self.builder.add(var, temp) @@ -218,7 +238,6 @@ def visit_incrementassign(self, node): else: self.store(res, var_name) - def visit_aliasdeclaration(self, node): type_map[node.name] = type_map[node.collection.value] return ALIAS @@ -389,8 +408,8 @@ def visit_range(self, node): return array_ptr def visit_assign(self, node): # TODO: Simplify this, it just keeps getting worse - if isinstance(node.right, StructLiteral): - self.struct_assign(node) + if hasattr(node.right, 'name') and isinstance(self.search_scopes(node.right.name), ir.IdentifiedStructType): + self.define(node.left.value.value, self.visit(node.right)) elif hasattr(node.right, 'value') and isinstance(self.search_scopes(node.right.value), ir.Function): self.define(node.left.value, self.search_scopes(node.right.value)) else: @@ -407,10 +426,14 @@ def visit_assign(self, node): # TODO: Simplify this, it just keeps getting wors elif isinstance(node.left, DotAccess): obj = self.search_scopes(node.left.obj) obj_type = self.search_scopes(obj.struct_name) - new_obj = self.builder.insert_value(self.load(obj.name), self.visit(node.right), obj_type.fields.index(node.left.field)) - struct_ptr = self.alloc_and_store(new_obj, obj_type, name=obj.name) - struct_ptr.struct_name = obj.struct_name - self.define(obj.name, struct_ptr) + idx = -1 + for i, v in enumerate(obj_type.fields): + if v == node.left.field: + idx = i + break + + elem = self.builder.gep(obj, [self.const(0, width=INT32), self.const(idx, width=INT32)], inbounds=True) + self.builder.store(self.visit(node.right), elem) elif isinstance(node.left, CollectionAccess): right = self.visit(node.right) self.call('dyn_array_set', [self.search_scopes(node.left.collection.value), self.const(node.left.key.value), right]) @@ -432,18 +455,17 @@ def visit_fieldassignment(self, node): return self.builder.extract_value(self.load(node.obj), obj_type.fields.index(node.field)) def struct_assign(self, node): - struct_type = self.search_scopes(node.left.type.value) - name = node.left.value.value - struct = self.builder.alloca(struct_type, name=name) + struct_type = self.search_scopes(node.name) + struct = self.builder.alloca(struct_type) fields = [] - for field in node.right.fields.values(): + for field in node.named_arguments.values(): fields.append(self.visit(field)) - elem = self.builder.gep(struct, [self.const(0, width=INT32), self.const(len(fields)-1, width=INT32)], inbounds=True) - self.builder.store(fields[len(fields)-1], elem) - - struct.struct_name = node.left.type.value - self.define(name, struct) + elem = self.builder.gep(struct, [self.const(0, width=INT32), self.const(len(fields) - 1, width=INT32)], inbounds=True) + self.builder.store(fields[len(fields) - 1], elem) + + struct.struct_name = node.name + return struct def visit_dotaccess(self, node): obj = self.search_scopes(node.obj) @@ -490,9 +512,9 @@ def visit_opassign(self, node): res = self.builder.srem(var, right) elif op == POWER_ASSIGN: if not isinstance(node.right.value, int): - error('Cannot use non-integers for power coeficient') + error('Cannot use non-integers for power coeficient') # TODO: Send me to typechecker and check for binop as well - + right = cast_ops(self, right, var.type, node) temp = self.alloc_and_store(var, type_map[INT]) for _ in range(node.right.value - 1): @@ -514,6 +536,8 @@ def visit_opassign(self, node): elif op == FLOORDIV_ASSIGN: right = cast_ops(self, right, var.type, node) res = self.builder.fdiv(var, right) + temp = cast_ops(self, res, ir.IntType(64), node) + res = cast_ops(self, temp, res.type, node) elif op == DIV_ASSIGN: right = cast_ops(self, right, var.type, node) res = self.builder.fdiv(var, right) @@ -603,13 +627,14 @@ def visit_print(self, node): self.call('putchar', [ir.Constant(type_map[INT32], 10)]) return if isinstance(val.type, ir.IntType): - # noinspection PyUnresolvedReferences if val.type.width == 1: array = self.create_array(BOOL) self.call('bool_to_str', [array, val]) val = array else: - if val.type.signed: + if int(str(val.type).split("i")[1]) == 8: + self.print_num("%c", val) + elif val.type.signed: if int(str(val.type).split("i")[1]) < 32: val = self.builder.sext(val, type_map[INT32]) self.print_num("%d", val) @@ -624,6 +649,8 @@ def visit_print(self, node): self.print_num("%llu", val) return elif isinstance(val.type, (ir.FloatType, ir.DoubleType)): + if isinstance(val.type, ir.FloatType): + val = cast_ops(self, val, ir.DoubleType(), node) self.print_num("%g", val) return self.call('print', [val]) @@ -799,7 +826,7 @@ def _add_builtins(self): scanf_ty = ir.FunctionType(type_map[INT], [type_map[INT8].as_pointer(), type_map[INT].as_pointer()], var_arg=True) ir.Function(self.module, scanf_ty, 'scanf') - getchar_ty = ir.FunctionType(ir.IntType(4), []) + getchar_ty = ir.FunctionType(ir.IntType(8), []) ir.Function(self.module, getchar_ty, 'getchar') puts_ty = ir.FunctionType(type_map[INT], [type_map[INT].as_pointer()]) @@ -818,11 +845,27 @@ def stringz(string): def generate_code(self, node): return self.visit(node) + def add_debug_info(self, optimize, filename): + di_file = self.module.add_debug_info("DIFile", { + "filename": os.path.basename(os.path.abspath(filename)), + "directory": os.path.dirname(os.path.abspath(filename)), + }) + di_module = self.module.add_debug_info("DICompileUnit", { + "language": ir.DIToken("DW_LANG_Python"), + "file": di_file, + "producer": "Lesma v0.2.1", + "runtimeVersion": 1, + "isOptimized": optimize, + }, is_distinct=True) + # self.module.add_global(di_module) + self.module.add_named_metadata('llvm.dbg.cu', [di_file, di_module]) + def evaluate(self, optimize=True, ir_dump=False, timer=False): if ir_dump and not optimize: for func in self.module.functions: if func.name == "main": print(func) + llvmmod = llvm.parse_assembly(str(self.module)) if optimize: pmb = llvm.create_pass_manager_builder() @@ -846,6 +889,8 @@ def compile(self, filename, optimize=True, output=None, emit_llvm=False): spinner = Spinner() spinner.startSpinner("Compiling") compile_time = time() + + self.add_debug_info(optimize, filename) program_string = llvm.parse_assembly(str(self.module)) prog_str = str(program_string) diff --git a/src/lesma/compiler/llvmlite_custom.py b/src/lesma/compiler/llvmlite_custom.py index 7ad6eb6..2b4e749 100644 --- a/src/lesma/compiler/llvmlite_custom.py +++ b/src/lesma/compiler/llvmlite_custom.py @@ -1,7 +1,8 @@ from llvmlite import ir # Vectors - + + def extract_element(self, vector, idx, name=''): """ Returns the value at position idx. @@ -9,7 +10,8 @@ def extract_element(self, vector, idx, name=''): instr = ir.instructions.ExtractElement(self.block, vector, idx, name=name) self._insert(instr) return instr - + + def insert_element(self, vector, value, idx, name=''): """ Returns vector with vector[idx] replaced by value. @@ -19,6 +21,7 @@ def insert_element(self, vector, value, idx, name=''): self._insert(instr) return instr + def shuffle_vector(self, vector1, vector2, mask, name=''): """ Concatenate vectors and extract elements into new vector. @@ -38,13 +41,13 @@ def __init__(self, parent, vector, index, name=''): raise TypeError("index needs to be of IntType.") typ = vector.type.elementtype super(ExtractElement, self).__init__(parent, typ, "extractelement", - [vector, index], name=name) + [vector, index], name=name) def descr(self, buf): operands = ", ".join("{0} {1}".format( - op.type, op.get_reference()) for op in self.operands) + op.type, op.get_reference()) for op in self.operands) buf.append("{opname} {operands}\n".format( - opname = self.opname, operands = operands)) + opname=self.opname, operands=operands)) class InsertElement(ir.instructions.Instruction): @@ -58,13 +61,14 @@ def __init__(self, parent, vector, value, index, name=''): raise TypeError("index needs to be of IntType.") typ = vector.type super(InsertElement, self).__init__(parent, typ, "insertelement", - [vector, value, index], name=name) + [vector, value, index], name=name) def descr(self, buf): operands = ", ".join("{0} {1}".format( - op.type, op.get_reference()) for op in self.operands) + op.type, op.get_reference()) for op in self.operands) buf.append("{opname} {operands}\n".format( - opname = self.opname, operands = operands)) + opname=self.opname, operands=operands)) + class ShuffleVector(ir.instructions.Instruction): def __init__(self, parent, vector1, vector2, mask, name=''): @@ -72,25 +76,24 @@ def __init__(self, parent, vector1, vector2, mask, name=''): raise TypeError("vector1 needs to be of VectorType.") if vector2 != ir.Undefined: if vector2.type != vector1.type: - raise TypeError("vector2 needs to be " + - "Undefined or of the same type as vector1.") - if (not isinstance(mask, ir.Constant) or - not isinstance(mask.type, ir.types.VectorType) or - mask.type.elementtype != ir.types.IntType(32)): + raise TypeError("vector2 needs to be Undefined or of the same type as vector1.") + if (not isinstance(mask, ir.Constant) or not + isinstance(mask.type, ir.types.VectorType) or not + mask.type.elementtype != ir.types.IntType(32)): raise TypeError("mask needs to be a constant i32 vector.") typ = ir.types.VectorType(vector1.type.elementtype, mask.type.count) index_range = range(vector1.type.count if vector2 == ir.Undefined else 2 * vector1.type.count) if not all(ii.constant in index_range for ii in mask.constant): raise IndexError("mask values need to be in {0}".format(index_range)) super(ShuffleVector, self).__init__(parent, typ, "shufflevector", - [vector1, vector2, mask], name=name) + [vector1, vector2, mask], name=name) def descr(self, buf): buf.append("shufflevector {0} {1}\n".format( - ", ".join("{0} {1}".format(op.type, op.get_reference()) - for op in self.operands), - self._stringify_metadata(leading_comma=True), - )) + ", ".join("{0} {1}".format(op.type, op.get_reference()) + for op in self.operands), + self._stringify_metadata(leading_comma=True), + )) class VectorType(ir.Type): @@ -140,7 +143,7 @@ def wrap_constant_value(self, values): return (ir.Constant(self.elementtype, values), ) * self.count if len(values) != len(self): raise ValueError("wrong constant size for %s: got %d elements" - % (self, len(values))) + % (self, len(values))) return [ir.Constant(ty, val) if not isinstance(val, ir.Value) else val for ty, val in zip(self.elements, values)] @@ -199,6 +202,7 @@ def __new(cls, bits, signed): Old_IdentifiedStructType = ir.IdentifiedStructType + class _IdentifiedStructType(Old_IdentifiedStructType): def gep(self, i): """ @@ -208,7 +212,7 @@ def gep(self, i): """ if not isinstance(i.type, ir.IntType): raise TypeError(i.type) - + return self.elements[i.constant] def set_body(self, *elems): @@ -224,4 +228,4 @@ def set_body(self, *elems): ir.types.IdentifiedStructType = _IdentifiedStructType -ir.IdentifiedStructType = _IdentifiedStructType \ No newline at end of file +ir.IdentifiedStructType = _IdentifiedStructType diff --git a/src/lesma/compiler/operations.py b/src/lesma/compiler/operations.py index a472526..1da9291 100644 --- a/src/lesma/compiler/operations.py +++ b/src/lesma/compiler/operations.py @@ -18,6 +18,7 @@ def hasFunction(compiler, func_name): if func.name == func_name: return True + def userdef_unary_str(op, expr): return 'operator' + '.' + op + '.' + str(expr.type) @@ -63,6 +64,10 @@ def binary_op(compiler, node): elif isinstance(left.type, ir.IntType) and isinstance(right.type, ir.IntType): return int_ops(compiler, op, left, right, node) elif type(left.type) in NUM_TYPES and type(right.type) in NUM_TYPES: + if isinstance(left.type, ir.IntType): + left = cast_ops(compiler, left, right.type, node) + elif isinstance(right.type, ir.IntType): + left = cast_ops(compiler, right, left.type, node) return float_ops(compiler, op, left, right, node) diff --git a/src/lesma/parser.py b/src/lesma/parser.py index 7878445..0969b6a 100644 --- a/src/lesma/parser.py +++ b/src/lesma/parser.py @@ -4,6 +4,7 @@ from lesma.compiler.__init__ import type_map from lesma.utils import error + class Parser(object): def __init__(self, lexer): self.lexer = lexer @@ -38,6 +39,11 @@ def eat_value(self, *token_value): def preview(self, num=1): return self.lexer.preview_token(num) + def keep_indent(self): + while self.current_token.type == NEWLINE: + self.eat_type(NEWLINE) + return self.current_token.indent_level == self.indent_level + def program(self): root = Compound() while self.current_token.type != EOF: @@ -65,22 +71,33 @@ def class_declaration(self): base = None constructor = None methods = None - class_fields = None + class_fields = OrderedDict() instance_fields = None self.in_class = True self.next_token() class_name = self.current_token + self.user_types.append(class_name.value) self.eat_type(NAME) if self.current_token.value == LPAREN: pass # TODO impliment multiple inheritance self.eat_type(NEWLINE) self.indent_level += 1 - while self.current_token.indent_level == self.indent_level: + while self.keep_indent(): + if self.current_token.type == NEWLINE: + self.eat_type(NEWLINE) + continue + if self.current_token.type == NAME and self.preview().value == COLON: + field = self.current_token.value + self.eat_type(NAME) + self.eat_value(COLON) + field_type = self.type_spec() + class_fields[field] = field_type + self.eat_type(NEWLINE) if self.current_token.value == NEW: constructor = self.constructor_declaration(class_name) self.indent_level -= 1 self.in_class = False - return ClassDeclaration(class_name.value, base=base, constructor=constructor, methods=methods, class_fields=class_fields, instance_fields=instance_fields) + return ClassDeclaration(class_name.value, base, constructor, methods, class_fields, instance_fields) def variable_declaration(self): var_node = Var(self.current_token.value, self.line_num) @@ -172,7 +189,7 @@ def function_declaration(self): if op_func: if len(params) not in (1, 2): # TODO: move this to type checker error("Operators can either be unary or binary, and the number of parameters do not match") - + name.value = 'operator' + '.' + name.value for param in params: name.value += '.' + str(type_map[str(params[param].value)]) @@ -186,14 +203,15 @@ def constructor_declaration(self, class_name): param_defaults = {} vararg = None while self.current_token.value != RPAREN: - if self.current_token.type == NAME: - param_type = self.variable(self.current_token) - self.eat_type(NAME) - else: - param_type = self.type_spec() - params[self.current_token.value] = param_type param_name = self.current_token.value self.eat_type(NAME) + if self.current_token.value == COLON: + self.eat_value(COLON) + param_type = self.type_spec() + else: + param_type = self.variable(self.current_token) + + params[self.current_token.value] = param_type if self.current_token.value != RPAREN: if self.current_token.value == ASSIGN: self.eat_value(ASSIGN) @@ -282,7 +300,7 @@ def statement_list(self): if isinstance(node, Return): return [node] results = [node] - while self.current_token.indent_level == self.indent_level: + while self.keep_indent(): results.append(self.statement()) if self.current_token.type == NEWLINE: self.next_token() @@ -328,8 +346,8 @@ def statement(self): elif self.current_token.type == TYPE: if self.current_token.value == STRUCT: node = self.struct_declaration() - elif self.current_token.value == CLASS: - node = self.class_declaration() + elif self.current_token.value == CLASS: + node = self.class_declaration() elif self.current_token.value == EOF: return else: @@ -389,27 +407,18 @@ def slice_expression(self, token): pass def curly_bracket_expression(self, token): - hash_or_struct = None if token.value == LCURLYBRACKET: pairs = OrderedDict() while self.current_token.value != RCURLYBRACKET: key = self.expr() - if self.current_token.value == COLON: - hash_or_struct = 'hash' - self.eat_value(COLON) - else: - hash_or_struct = 'struct' - self.eat_value(ASSIGN) + self.eat_value(ASSIGN) pairs[key.value] = self.expr() if self.current_token.value == COMMA: self.next_token() else: break self.eat_value(RCURLYBRACKET) - if hash_or_struct == 'hash': - return HashMap(pairs, self.line_num) - elif hash_or_struct == 'struct': - return StructLiteral(pairs, self.line_num) + return HashMap(pairs, self.line_num) else: raise SyntaxError('Wait... what?') @@ -557,7 +566,7 @@ def switch_statement(self): switch = Switch(value, [], self.line_num) if self.current_token.type == NEWLINE: self.next_token() - while self.current_token.indent_level == self.indent_level: + while self.keep_indent(): switch.cases.append(self.case_statement()) if self.current_token.type == NEWLINE: self.next_token() diff --git a/src/lesma/type_checker.py b/src/lesma/type_checker.py index c010c8c..02a3c36 100644 --- a/src/lesma/type_checker.py +++ b/src/lesma/type_checker.py @@ -1,14 +1,6 @@ -from lesma.ast import Collection -from lesma.ast import Var -from lesma.ast import VarDecl -from lesma.ast import DotAccess -from lesma.ast import CollectionAccess +from lesma.ast import Collection, Var, VarDecl, DotAccess, CollectionAccess from lesma.grammar import * -from lesma.visitor import AliasSymbol -from lesma.visitor import CollectionSymbol -from lesma.visitor import FuncSymbol -from lesma.visitor import NodeVisitor, StructSymbol -from lesma.visitor import VarSymbol +from lesma.visitor import AliasSymbol, CollectionSymbol, FuncSymbol, NodeVisitor, StructSymbol, ClassSymbol, VarSymbol from lesma.utils import warning, error @@ -134,6 +126,10 @@ def visit_assign(self, node): # TODO clean up this mess of a function var_name = node.left.value.value value = self.infer_type(node.left.type) value.accessed = True + elif hasattr(node.right, 'name') and isinstance(self.search_scopes(node.right.name), (StructSymbol, ClassSymbol)): + var_name = node.left.value + value = self.infer_type(self.search_scopes(node.right.name)) + value.accessed = True elif isinstance(node.right, Collection): var_name = node.left.value value, collection_type = self.visit(node.right) @@ -169,7 +165,6 @@ def visit_assign(self, node): # TODO clean up this mess of a function if isinstance(var, FuncSymbol): self.define(var_name, var) else: - # noinspection PyUnresolvedReferences val_info = self.search_scopes(node.right.value) func_sym = FuncSymbol(var_name, val_info.type.return_type, val_info.parameters, val_info.body, val_info.parameter_defaults) self.define(var_name, func_sym) @@ -229,7 +224,6 @@ def visit_incrementassign(self, node): else: error('file={} line={}: Things that should not be happening ARE happening (fix this message)'.format(self.file_name, node.line_num)) - def visit_fieldassignment(self, node): obj = self.search_scopes(node.obj) return self.visit(obj.type.fields[node.field]) @@ -297,14 +291,18 @@ def visit_typedeclaration(self, node): typs = tuple(typs) typ = AliasSymbol(node.name.value, typs) self.define(typ.name, typ) - + def visit_aliasdeclaration(self, node): typ = AliasSymbol(node.name, node.collection.value) self.define(typ.name, typ) - + def visit_externfuncdecl(self, node): func_name = node.name func_type = self.search_scopes(node.return_type.value) + + if self.search_scopes(func_name) is not None: + error('file={} line={}: Cannot redefine a declared function: {}'.format(self.file_name, node.line_num, func_name)) + if func_type and func_type.name == FUNC: func_type.return_type = self.visit(node.return_type.func_ret_type) self.define(func_name, FuncSymbol(func_name, func_type, node.parameters, None)) @@ -334,10 +332,13 @@ def visit_externfuncdecl(self, node): self.define(func_name, func_symbol, 1) self.drop_top_scope() - def visit_funcdecl(self, node): func_name = node.name func_type = self.search_scopes(node.return_type.value) + + if self.search_scopes(func_name) is not None: + error('file={} line={}: Cannot redefine a declared function: {}'.format(self.file_name, node.line_num, func_name)) + if func_type and func_type.name == FUNC: func_type.return_type = self.visit(node.return_type.func_ret_type) self.define(func_name, FuncSymbol(func_name, func_type, node.parameters, node.body, node.parameter_defaults)) @@ -399,15 +400,25 @@ def visit_anonymousfunc(self, node): def visit_funccall(self, node): func_name = node.name func = self.search_scopes(func_name) - for x, param in enumerate(func.parameters.values()): + parameters = None + parameter_defaults = None + if isinstance(func, (StructSymbol, ClassSymbol)): + parameters = func.fields + parameter_defaults = func.fields + else: + parameters = func.parameters + parameter_defaults = func.parameter_defaults + + for x, param in enumerate(parameters.values()): if x < len(node.arguments): var = self.visit(node.arguments[x]) param_ss = self.search_scopes(param.value) - if not types_compatible(var, param_ss) and (param_ss != self.search_scopes(ANY) and param.value != var.name and param.value != var.type.name): + if var.type is not None and not types_compatible(var, param_ss) and \ + (param_ss != self.search_scopes(ANY) and param.value != var.name and param.value != var.type.name): raise TypeError # TODO: Make this an actual error else: - func_param_keys = list(func.parameters.keys()) - if func_param_keys[x] not in node.named_arguments.keys() and func_param_keys[x] not in func.parameter_defaults.keys(): + func_param_keys = list(parameters.keys()) + if func_param_keys[x] not in node.named_arguments.keys() and func_param_keys[x] not in parameter_defaults.keys(): error('file={} line={}: Missing arguments to function: {}'.format(self.file_name, node.line_num, repr(func_name))) else: if func_param_keys[x] in node.named_arguments.keys(): @@ -446,6 +457,10 @@ def visit_structdeclaration(self, node): sym = StructSymbol(node.name, node.fields) self.define(sym.name, sym) + def visit_classdeclaration(self, node): + sym = ClassSymbol(node.name, node.class_fields) + self.define(sym.name, sym) + def visit_return(self, node): res = self.visit(node.value) self.return_flag = True @@ -476,6 +491,9 @@ def visit_collection(self, node): def visit_dotaccess(self, node): obj = self.search_scopes(node.obj) obj.accessed = True + if node.field not in obj.type.fields: + error('file={} line={}: Invalid property {} of variable {}'.format( + self.file_name, node.line_num, node.field, node.obj)) return self.visit(obj.type.fields[node.field]) def visit_hashmap(self, node): diff --git a/src/lesma/visitor.py b/src/lesma/visitor.py index e6639ba..91cd090 100644 --- a/src/lesma/visitor.py +++ b/src/lesma/visitor.py @@ -73,6 +73,14 @@ def __init__(self, name, fields): self.val_assigned = False +class ClassSymbol(Symbol): + def __init__(self, name, class_fields): + super().__init__(name) + self.class_fields = class_fields + self.accessed = False + self.val_assigned = False + + class CollectionSymbol(Symbol): def __init__(self, name, var_type, item_types): super().__init__(name, var_type) @@ -204,8 +212,8 @@ def items(self): @property def unvisited_symbols(self): return [sym_name for sym_name, sym_val in self.items if - not isinstance(sym_val, (BuiltinTypeSymbol, BuiltinFuncSymbol)) and - not sym_val.accessed and sym_name != '_'] + not isinstance(sym_val, (BuiltinTypeSymbol, BuiltinFuncSymbol)) and not + sym_val.accessed and sym_name != '_'] def infer_type(self, value): if isinstance(value, BuiltinTypeSymbol): @@ -226,6 +234,10 @@ def infer_type(self, value): return self.search_scopes(COMPLEX) if isinstance(value, str): return self.search_scopes(STR) + if isinstance(value, StructSymbol): + return self.search_scopes(STRUCT) + if isinstance(value, ClassSymbol): + return self.search_scopes(CLASS) if isinstance(value, bool): return self.search_scopes(BOOL) if isinstance(value, list): diff --git a/tests/io/ffi.les b/tests/io/ffi.les new file mode 100644 index 0000000..e9d5dd0 --- /dev/null +++ b/tests/io/ffi.les @@ -0,0 +1,7 @@ +# Extern functions (FFI) +def extern abs(x: int) -> int # from C's stdlib +def extern pow(x: double, y: double) -> double + + +print(abs(-5)) +print(pow(2,4)) \ No newline at end of file diff --git a/tests/io/ffi.output b/tests/io/ffi.output new file mode 100644 index 0000000..8304884 --- /dev/null +++ b/tests/io/ffi.output @@ -0,0 +1,2 @@ +5 +16 \ No newline at end of file diff --git a/tests/io/function.les b/tests/io/function.les new file mode 100644 index 0000000..6ca2856 --- /dev/null +++ b/tests/io/function.les @@ -0,0 +1,20 @@ +def fib(n: int) -> int + a = 0 + b = 1 + for _ in 0..n + prev_a = a + a = b + b = prev_a + b + return a + +def fib_rec(n: int) -> int + if n == 0 + return 0 + if n == 1 + return 1 + return fib_rec(n - 1) + fib_rec(n - 2) + +def factorial(n: int = 5) -> int + if n <= 1 + return 1 + return n * factorial(n - 1) \ No newline at end of file diff --git a/tests/io/function.output b/tests/io/function.output new file mode 100644 index 0000000..69dc5ca --- /dev/null +++ b/tests/io/function.output @@ -0,0 +1,4 @@ +55 +55 +120 +16 \ No newline at end of file diff --git a/tests/io/list.les b/tests/io/list.les new file mode 100644 index 0000000..8ce4c6f --- /dev/null +++ b/tests/io/list.les @@ -0,0 +1,5 @@ +things = [1, 2, 3] # List, homogeneous +print(things[0]) +print(things[1*1+1]) +things[2] = 1002 +print(things[2]) \ No newline at end of file diff --git a/tests/io/list.output b/tests/io/list.output new file mode 100644 index 0000000..5efd464 --- /dev/null +++ b/tests/io/list.output @@ -0,0 +1,3 @@ +1 +3 +1002 \ No newline at end of file diff --git a/tests/io/opassign.les b/tests/io/opassign.les new file mode 100644 index 0000000..a31d0e5 --- /dev/null +++ b/tests/io/opassign.les @@ -0,0 +1,33 @@ +number: int = 0 +print(number) +number += 5 +print(number) +number -= 1 +print(number) +number /= 2 +print(number) +number *= 10 +print(number) +number //= 7 +print(number) +number ^= 3 +print(number) +number %= 3 +print(number) + +number2: double = 0 +print(number2) +number2 += 5 +print(number2) +number2 -= 1 +print(number2) +number2 /= 2 +print(number2) +number2 *= 10 +print(number2) +number2 //= 7 +print(number2) +number2 ^= 3 +print(number2) +number2 %= 3 +print(number2) \ No newline at end of file diff --git a/tests/io/opassign.output b/tests/io/opassign.output new file mode 100644 index 0000000..a297552 --- /dev/null +++ b/tests/io/opassign.output @@ -0,0 +1,16 @@ +0 +5 +4 +2 +20 +2 +8 +2 +0 +5 +4 +2 +20 +2 +8 +2 \ No newline at end of file diff --git a/tests/io/operators.les b/tests/io/operators.les index 4308f10..dc860e5 100644 --- a/tests/io/operators.les +++ b/tests/io/operators.les @@ -6,6 +6,8 @@ print(5//2) print(5%3) print(2^8) print(-11) +print(1 << 3) +print(8 >> 2) print(true and false) print(true or false) print(true xor true) diff --git a/tests/io/operators.output b/tests/io/operators.output index aa31c21..1aec396 100644 --- a/tests/io/operators.output +++ b/tests/io/operators.output @@ -6,6 +6,8 @@ 2 256 -11 +8 +2 false true false diff --git a/tests/io/opoverload.les b/tests/io/opoverload.les new file mode 100644 index 0000000..3953a4e --- /dev/null +++ b/tests/io/opoverload.les @@ -0,0 +1,8 @@ +def operator - (x: int, y:int) -> int + return x + 3 + +def operator + (x: int) -> int + return 5 + +print(3 - 1) +print( +2 ) \ No newline at end of file diff --git a/tests/io/opoverload.output b/tests/io/opoverload.output new file mode 100644 index 0000000..ec385ae --- /dev/null +++ b/tests/io/opoverload.output @@ -0,0 +1,2 @@ +6 +5 \ No newline at end of file diff --git a/tests/io/range.les b/tests/io/range.les new file mode 100644 index 0000000..7bf65b3 --- /dev/null +++ b/tests/io/range.les @@ -0,0 +1,2 @@ +for i in 0..11 + print(i) \ No newline at end of file diff --git a/tests/io/range.output b/tests/io/range.output new file mode 100644 index 0000000..7bd5250 --- /dev/null +++ b/tests/io/range.output @@ -0,0 +1,11 @@ +0 +1 +2 +3 +4 +5 +6 +7 +8 +9 +10 \ No newline at end of file diff --git a/tests/io/struct.les b/tests/io/struct.les new file mode 100644 index 0000000..3b44d89 --- /dev/null +++ b/tests/io/struct.les @@ -0,0 +1,10 @@ +struct Circle + radius: int + x: int + y: int + +cir: Circle = Circle(radius=5, x=2, y=4) + +print(cir.radius + cir.x) +cir.x = 20 +print(cir.x) \ No newline at end of file diff --git a/tests/io/struct.output b/tests/io/struct.output new file mode 100644 index 0000000..1845bb0 --- /dev/null +++ b/tests/io/struct.output @@ -0,0 +1,2 @@ +7 +20 \ No newline at end of file diff --git a/tests/io/types.les b/tests/io/types.les new file mode 100644 index 0000000..1974fdb --- /dev/null +++ b/tests/io/types.les @@ -0,0 +1,4 @@ +x: int32 = 5 +print(x as float) +print(x is int) +print(x is int32) \ No newline at end of file diff --git a/tests/io/types.output b/tests/io/types.output new file mode 100644 index 0000000..1c0a98d --- /dev/null +++ b/tests/io/types.output @@ -0,0 +1,3 @@ +5 +false +true \ No newline at end of file diff --git a/tests/io/unicode.les b/tests/io/unicode.les new file mode 100644 index 0000000..a948e42 --- /dev/null +++ b/tests/io/unicode.les @@ -0,0 +1,3 @@ +π: float = 3.14 +print('🍌') +print('夜のコンサートは最高でした。') diff --git a/tests/io/unicode.output b/tests/io/unicode.output new file mode 100644 index 0000000..3d60a79 --- /dev/null +++ b/tests/io/unicode.output @@ -0,0 +1,2 @@ +🍌 +夜のコンサートは最高でした。 \ No newline at end of file diff --git a/tests/io/vardecl.les b/tests/io/vardecl.les index 0a60651..a3182bc 100644 --- a/tests/io/vardecl.les +++ b/tests/io/vardecl.les @@ -7,6 +7,11 @@ a6: int128 = 6 a7: double = 6.5 a8: float = 7.69 a10: bool = true +# a11: str = "Hey there" +a12 = [1,2,3,4] +# a13 = [1.5,3.5,9.0] +# a14 = ["Hey", "There"] +# a15 = [true, false, false] x: int = 5 y = 7.5 diff --git a/tests/test_all.py b/tests/test_all.py new file mode 100644 index 0000000..3a31508 --- /dev/null +++ b/tests/test_all.py @@ -0,0 +1,31 @@ +import os +import pytest +from subprocess import Popen, PIPE + + +def get_tests(): + tests = [] + for file in os.listdir("./tests/io"): + if file.endswith(".les"): + tests.append(os.path.basename(file).split('.')[0]) + + return tests + + +# Base test for all files +@pytest.mark.parametrize("test_name", get_tests()) +def test_base(test_name): + proc = Popen(["python3", "src/les.py", "run", f'tests/io/{test_name}.les'], stdout=PIPE, stderr=PIPE) + out, err = proc.communicate() + output = out.decode('utf-8').strip() + error = err.decode('utf-8').strip() + rc = proc.returncode + + assert 'Error:' not in error + assert rc == 0 + + if output: + with open(f'tests/io/{test_name}.output') as expected: + exp_str = "".join(expected.readlines()) + assert output == exp_str + expected.close() diff --git a/tests/test_operators.py b/tests/test_operators.py deleted file mode 100644 index cf83b66..0000000 --- a/tests/test_operators.py +++ /dev/null @@ -1,19 +0,0 @@ -import unittest -from subprocess import Popen, PIPE - - -class TestArithmetic(unittest.TestCase): - def test_arithmetic(self): - proc = Popen(["python3", "src/les.py", "run", 'tests/io/operators.les'], stdout=PIPE, stderr=PIPE) - out, err = proc.communicate() - output = out.decode('utf-8').strip() - error = err.decode('utf-8').strip() - rc = proc.returncode - with open('tests/io/operators.output') as expected: - self.assertTrue('Error:' not in error) - self.assertEqual(rc, 0) - expected.close() - - -if __name__ == '__main__': - unittest.main() diff --git a/tests/test_vardecl.py b/tests/test_vardecl.py deleted file mode 100644 index 8c5be9b..0000000 --- a/tests/test_vardecl.py +++ /dev/null @@ -1,19 +0,0 @@ -import unittest -from subprocess import Popen, PIPE - - -class TestVariableDeclaration(unittest.TestCase): - def test_vardecl(self): - proc = Popen(["python3", "src/les.py", "run", 'tests/io/vardecl.les'], stdout=PIPE, stderr=PIPE) - out, err = proc.communicate() - output = out.decode('utf-8').strip() - error = err.decode('utf-8').strip() - rc = proc.returncode - with open('tests/io/vardecl.output') as expected: - self.assertTrue('Error:' not in error) - self.assertEqual(rc, 0) - expected.close() - - -if __name__ == '__main__': - unittest.main()