diff --git a/compiler/language_models/expression.py b/compiler/language_models/expression.py index dd8f8e0..8f25f02 100644 --- a/compiler/language_models/expression.py +++ b/compiler/language_models/expression.py @@ -7,7 +7,7 @@ import attr import immutabledict -from . import declarable, argument_list, namespace as namespace_module +from . import declarable, argument_list, namespace as namespace_module, string from ..libs import parser as parser_module # pylint: disable=fixme @@ -130,7 +130,7 @@ def execute(self, namespace): # TODO: local variable formatting if self.is_binary: return b''.join(bytes(value.value) for value in self.values) - return ''.join(str(value.value) for value in self.values) + return string.String(''.join(str(value.value) for value in self.values)) @classmethod def from_string(cls, value): @@ -287,6 +287,11 @@ def execute(self, namespace): try: return namespace.lookup(self.name) except KeyError as exc: + if self.annotation is not None: + # If there's an annotation, this is a type declaration. + # Put None as the value as a placeholder for now. + self.assign(namespace, None) + return None raise NameError(f'name {self.name!r} is not defined') from exc @classmethod @@ -677,7 +682,10 @@ def execute(self, namespace): key: arg.execute(namespace) for key, arg in self.keyword_arguments.items() } - return callable_value(*positional_arguments, **keyword_arguments) # noqa + try: + return callable_value(*positional_arguments, **keyword_arguments) # noqa + except TypeError as exc: + raise TypeError(f'problem with {callable_value!r}(*{positional_arguments!r}, **{keyword_arguments!r})') from exc @property def expressions(self): diff --git a/compiler/language_models/module.py b/compiler/language_models/module.py index 7ced3a0..7d20ff4 100644 --- a/compiler/language_models/module.py +++ b/compiler/language_models/module.py @@ -7,20 +7,11 @@ from . import ( statement as statement_module, - namespace as namespace_module, + namespace as namespace_module, test, string, ) from ..libs import parser as parser_module -class _String(str): - is_alpha = str.isalpha - is_digit = str.isdigit - is_numeric = str.isnumeric - is_decimal = str.isdecimal - is_alphanumeric = str.isalnum - is_space = str.isspace - - builtin_namespace = namespace_module.Namespace() builtin_namespace.declare('True', True) builtin_namespace.declare('False', False) @@ -31,8 +22,8 @@ class _String(str): builtin_namespace.declare('Exception', Exception) builtin_namespace.declare('print', print) builtin_namespace.declare('Boolean', bool) -builtin_namespace.declare('String', _String) -builtin_namespace.declare('Character', str) +builtin_namespace.declare('String', string.String) +builtin_namespace.declare('Character', string.String) builtin_namespace.declare('Integer', int) builtin_namespace.declare('List', list) builtin_namespace.declare('FrozenList', tuple) @@ -49,6 +40,7 @@ class _String(str): builtin_namespace.declare('staticmethod', staticmethod) builtin_namespace.declare('classmethod', classmethod) builtin_namespace.declare('abstract', lambda x: x) # don't bother implementing yet +builtin_namespace.declare('test', test.test) @attr.s(frozen=True, slots=True) @@ -56,8 +48,8 @@ class Module(parser_module.Symbol): statements: typing.Sequence[statement_module.Statement] = attr.ib(converter=tuple) @classmethod - def from_string(cls, code: str) -> Module: - cursor = parser_module.Cursor(code.splitlines()) + def from_string(cls, path: typing.Sequence[str], code: str) -> Module: + cursor = parser_module.Cursor(code.splitlines(), path=path) return cls.parse(cursor).last_symbol # noqa @@ -101,7 +93,11 @@ def execute(self, for statement in self.statements: statement.execute(global_namespace).get_value() - get_outcome().get_value() # reraise any exception, with module added to traceback + outcome = get_outcome() + + if isinstance(outcome, statement_module.Raise.Outcome): + print(outcome) + outcome.get_value() # reraise any exception, with module added to traceback return global_namespace.as_object() diff --git a/compiler/language_models/program.py b/compiler/language_models/program.py index 6abdd65..f829533 100644 --- a/compiler/language_models/program.py +++ b/compiler/language_models/program.py @@ -20,7 +20,7 @@ def register_module(self, path: typing.Sequence[str], module: module_module.Modu def register_module_from_string(self, path: typing.Sequence[str], module_string: str): self.register_module( path, - module_module.Module.from_string(module_string), + module_module.Module.from_string(path, module_string), ) def register_package(self, path: typing.Sequence[str], root_path: str): @@ -36,6 +36,10 @@ def register_package(self, path: typing.Sequence[str], root_path: str): path + file_path.relative_to(root_path).parts ] = file_path.read_bytes() + # Initialize all the modules. + for module_path in self.modules: + self.get_module(module_path) + def get_module(self, path: typing.Sequence[str]): path = tuple(path) @@ -43,7 +47,12 @@ def get_module(self, path: typing.Sequence[str]): if path not in self.modules: raise ImportError(f'no such module {path}') - self.initialized_modules[path] = self.modules[path].execute(self, path) + module = self.modules[path] + + if isinstance(module, module_module.Module): + module = module.execute(self, path) + + self.initialized_modules[path] = module return self.initialized_modules[path] diff --git a/compiler/language_models/string.py b/compiler/language_models/string.py index d1abde1..87f8c28 100644 --- a/compiler/language_models/string.py +++ b/compiler/language_models/string.py @@ -3,6 +3,17 @@ import regex +class String(str): + is_alpha = str.isalpha + is_digit = str.isdigit + is_numeric = str.isnumeric + is_decimal = str.isdecimal + is_alphanumeric = str.isalnum + is_space = str.isspace + + length = str.__len__ + + def unescape_text(escaped: str) -> str: return _ESCAPE_TEXT_REGEX.sub( _unescape_text, diff --git a/compiler/language_models/test.py b/compiler/language_models/test.py new file mode 100644 index 0000000..ab18567 --- /dev/null +++ b/compiler/language_models/test.py @@ -0,0 +1,23 @@ +import traceback +import typing + + +def test(test_class): + print(f'testing {test_class}') + instance = test_class() + + for name in dir(test_class): + method = getattr(instance, name) + if name.startswith('test_') and isinstance(method, typing.Callable): + print(f' {name}:') + + try: + method() + except AssertionError: + print(' FAILURE') + traceback.print_exc() + except Exception: # noqa, pylint: disable=broad-except + print(' ERROR') + traceback.print_exc() + else: + print(' SUCCESS') diff --git a/compiler/libs/parser.py b/compiler/libs/parser.py index 08fec1f..1a27f1c 100644 --- a/compiler/libs/parser.py +++ b/compiler/libs/parser.py @@ -28,6 +28,7 @@ class Cursor: column: int = attr.ib(default=0) last_symbol: typing.Optional[Symbol] = attr.ib(default=None) block_depth: int = attr.ib(default=0) + path: str = attr.ib(default=('?',)) def line_text(self, line=None): line = self.line if line is None else line @@ -62,6 +63,7 @@ def new_from_symbol(self, symbol: Symbol): column=column, last_symbol=symbol, block_depth=block_depth, + path=self.path, ) def parse_one_symbol(self, one_of: typing.Sequence[typing.Type[Symbol]], fail=False) -> Cursor: @@ -99,7 +101,7 @@ def parse_one_symbol(self, one_of: typing.Sequence[typing.Type[Symbol]], fail=Fa ) def __str__(self): - heading = f'{self.line + 1}, {self.column + 1}: ' + heading = f'{".".join(self.path)}:{self.line + 1}:{self.column + 1}: ' line_text = self.line_text() pointer_indent = ' ' * (len(heading) + self.column) return f'{heading}{line_text}\n{pointer_indent}^' diff --git a/compiler/meta/generic.py b/compiler/meta/generic.py index 77599c5..4427127 100644 --- a/compiler/meta/generic.py +++ b/compiler/meta/generic.py @@ -1,6 +1,7 @@ import typing import attr +import immutabledict @attr.s(frozen=True, slots=True) @@ -8,11 +9,16 @@ class Generic: class_factory: typing.Callable = attr.ib() _cache: typing.MutableMapping = attr.ib(factory=dict) - def __getitem__(self, item): - if not isinstance(item, tuple): - item = (item,) + def __getitem__(self, *args, **kwargs): + if len(args) == 1 and isinstance(args[0], tuple): + args = args[0] + kwargs = immutabledict.immutabledict(kwargs) + item = (args, kwargs) - if item not in self._cache: - self._cache[item] = self.class_factory(*item) + try: + if item not in self._cache: + self._cache[item] = self.class_factory(*args, **kwargs) + except TypeError as exc: + raise TypeError(f'problem with {item!r}') from exc return self._cache[item] diff --git a/src/sibilance/compiler/parsing/ast.sib b/src/sibilance/compiler/parsing/ast.sib index 32d55b1..4ab15ef 100644 --- a/src/sibilance/compiler/parsing/ast.sib +++ b/src/sibilance/compiler/parsing/ast.sib @@ -77,15 +77,6 @@ class Import(Node): path: FrozenList[String] -class Statement(Or[ - Import, - ClassDefinition, - FunctionDefinition, - Expression, -]): - pass - - class ArgumentsDefinition(Node): positional_arguments: FrozenList[VariableDefinition] keyword_arguments: FrozenMap[String, VariableDefinition] @@ -103,6 +94,15 @@ class ClassDefinition(Node): statements: FrozenList[Statement] +class Statement(Or[ + Import, + ClassDefinition, + FunctionDefinition, + Expression, +]): + pass + + class Module(Node): statements: FrozenList[Statement] diff --git a/src/sibilance/compiler/parsing/ast_test.sib b/src/sibilance/compiler/parsing/ast_test.sib new file mode 100644 index 0000000..0a3b16f --- /dev/null +++ b/src/sibilance/compiler/parsing/ast_test.sib @@ -0,0 +1,29 @@ +import .ast + + +@test +class CursorTest: + def test_advance_line(self): + cursor = ast.Cursor() + assert cursor.line == 1 + + cursor = cursor.advance("hello\nworld") + assert cursor.line == 2 + cursor = cursor.advance("\n\n") + assert cursor.line == 4 + + def test_advance_column(self): + cursor = ast.Cursor() + assert cursor.column == 1 + + cursor = cursor.advance("he") + assert cursor.column == 3 + + cursor = cursor.advance("llo") + assert cursor.column == 6 + + cursor = cursor.advance("\nwo") + assert cursor.column == 3 + + cursor = cursor.advance("rld\n!") + assert cursor.column == 2