commit 7badd31e425de38ad0877c5bb6e45881a25900d4 Author: egormanga Date: Wed Aug 28 21:24:54 2019 +0300 Initial commit diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..145b0b9 --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +old.c +**.db +**.log +**.old +**.pyc +**__pycache__ +**_config.py \ No newline at end of file diff --git a/README.md b/README.md new file mode 120000 index 0000000..197480c --- /dev/null +++ b/README.md @@ -0,0 +1 @@ +Slang.md \ No newline at end of file diff --git a/SBC/bytecode.md b/SBC/bytecode.md new file mode 100644 index 0000000..3aeba0f --- /dev/null +++ b/SBC/bytecode.md @@ -0,0 +1,116 @@ +# Slang Bytecode + + +## Standalone + +### NOP +0x00 +> Does nothing. +### RET +0x01 +> Returns `TOS` to caller. + + +## Unary + +### POS +0x10 +> Pushes `abs(TOS)`. +### NEG +0x11 +> Pushes `-TOS`. +### NOT +0x12 +> Pushes `!TOS`. +### INV +0x13 +> Pushes `~TOS`. +### ATOI +0x14 +> Pushes integer of smallest possible width parsed from string `TOS`. +### ITOA +0x15 +> Pushes string representation of integer `TOS`. +### ITOF +0x16 +> Pushes real of smallest possible width equal to integer `TOS`. +### CEIL +0x17 +> Pushes smallest integer of smallest possible width greater or equal to real `TOS`. +### FLR +0x18 +> Pushes largest integer of smallest possible width less or equal to real `TOS`. +### RND +0x19 +> Pushes integer of smallest possible width equal to rounded `TOS`. +### CTOS +0x1a +> Pushes string consisting of char `TOS` and a null byte. + + +## Binary + +### ADD +0x20 +> Pushes `TOS1 + TOS`. +### SUB +0x21 +> Pushes `TOS1 - TOS`. +### MUL +0x22 +> Pushes `TOS1 * TOS`. +### DIV +0x23 +> Pushes `TOS1 / TOS`. +### FLRDIV +0x24 +> Pushes `TOS1 // TOS`. +### MOD +0x25 +> Pushes `TOS1 % TOS`. +### POW +0x26 +> Pushes `TOS1 ** TOS`. +### SHL +0x27 +> Pushes `TOS1 << TOS`. +### SHR +0x28 +> Pushes `TOS1 >> TOS`. +### AND +0x29 +> Pushes `TOS1 & TOS`. +### OR +0x2a +> Pushes `TOS1 | TOS`. +### XOR +0x2b +> Pushes `TOS1 ^ TOS`. + + +## With argument + +### ALLOC*(bytes)* +0xa0 +> Pushes reference to `calloc(1, bytes)`. +### EXTEND*(bytes)* +0xa1 +> Extends integer `TOS` width to `bytes` bytes if narrower. +### CONST*(bytes)* +0xa2 +> Reads next `bytes` bytes of bytecode and pushes a reference to a copy of them. +### JUMPF*(offset)* +0xa3 +> Jumps `offset` bytes of bytecode forward. +### JUMPB*(offset)* +0xa4 +> Jumps `offset` bytes of bytecode backward. +### SCPGET*(cell)* +0xa5 +> Pushes the value of cell `cell` of local scope variables array. +### SCPSET*(cell)* +0xa6 +> Sets the value of cell `cell` of local scope variables array to `TOS`. +### CALL*(nargs)* +0xa7 +> Pops `nargs ^ (1 << 7)` TODO \ No newline at end of file diff --git a/SBC/tests/hw.sbc b/SBC/tests/hw.sbc new file mode 100644 index 0000000..02c7e6e Binary files /dev/null and b/SBC/tests/hw.sbc differ diff --git a/Slang.md b/Slang.md new file mode 100644 index 0000000..2db0867 --- /dev/null +++ b/Slang.md @@ -0,0 +1,170 @@ +# Slang + + +### Example code + +```c +# this is a comment +#| and that is a + multiline one. |# + +const u32 n = 123123 # n is of type const u32 +const i64 m = 10**18 # m is of type const i64 +const int z = 2**128 # z is of type const int (unsized) +const auto q = 2**256 # q is of type const int + +char f(str x) { # f() is of type char, x is of type str + auto c = x[1] # c is of type char + return c +} + +char g(str x) { # g() is of type char, x is of type str + return x[0] # retval is of type char +} + +int h(int x) = x+1 # h() is of type int, x is of type int + +void main() { + print(h(n), \ + f('123asd') + g('32') + 1) #--> «123124 f» + print(q/z/2**96) #--> «4294967296.0» +} + +main() +``` + +## Tokens + +* [Keywords](#Keywords) +* [Identifiers](#Identifiers) +* [Literals](#Literals) +* [Operators](#Operators) +* [Specials](#Specials) + +#### Token resolution order + +1. Special +2. Operator +3. Literal +4. Keyword +5. Identifier + +## Syntax structures + +_Note: `*` after syntax unit means any number of them._ + +### Abstract + +* `{[<\n | ;>]* < <\n | ;>>* [<\n | ;>]*}` — `code` +* `< | >` — `block` + +### Primitive + +* `< | | | | | >` — `value` +* `\[\]` — `itemget` +* `<() | | | >` — `expr` (`f(x+3)` is an instance of `expr`, also `f`, `x+3` and `x` are `expr`s too) + +### Non-final + +* `[modifier]* ` — `typedef` (left-hand type definition) +* `[typedef] [? | + | * | ** | =]` — `argdef` (argument definition) + > `?` — if present then argument value, `none` else.
+ > `+` — tuple with at least one argument.
+ > `*` — tuple with any number of arguments.
+ > `**` — object with keyword arguments.
+ > `=` — default value if argument not specified. +* `([[, ]*]) -> = ` — `lambda` (lambda function) +* `<[, ]*[, *] | *>` — `callargs` +* `<=[, =]*[, **] | **>` — `callkwargs` + +### Final (ordered by resolution order) + +* ` ([[, ]*]) < | = >` — `funcdef` (function definition) +* ` [expr]` — `keywordexpr` (keyword expression) +* ` [= ]` — `vardef` (variable definition) +* ` = ` — `assignment` +* `([ | | , ])` — `funccall` (function call) +* `` — expr evaluation (only in REPL) +* `if () ` — `conditional` +* `for ( in ) ` — `forloop` +* `while () ` — `whileloop` +* `else ` — `elseclause` + +## Keywords + +* `return [expr]` — return from function + +### Modifiers + +* `const` — immutable/constant variable + +### Reserved keywords + +* `def` + +## Identifiers + +Non-empty sequence of alphanumeric characters plus underscore («_»), not starting with a digit character. + +Regex: `[_\w]+[_\w\d]*` + +### Data types + +* `i8`, `i16`, `i32`, `i64`, `i128` — fixed size integer +* `u8`, `u16`, `u32`, `u64`, `u128` — fixed size unsigned integer +* `f8`, `f16`, `f32`, `f64`, `f128` — fixed size IEEE-754 floating point number +* `uf8`, `uf16`, `uf32`, `uf64`, `uf128` — fixed size unsigned floating point number +* `int` — unsized («big») integer +* `uint` — unsized unsigned integer +* `float` — unsized floating point number +* `ufloat` — unsized unsigned floating point +* `bool` — logical (boolean) value +* `byte` — single byte +* `char` — UTF-8 character +* `str` — UTF-8 string +* `void` — nothing (or anything...) + +* `auto` — compile-time type deduction based on value + +## Literals + +_Note: `*` after syntax unit here means any number of them, `+` means at least one._ + +* `<<0> | . | .>` — number +* `<"" | ''>` — string + +## Operators + +* ` ` — unary operators usage +* ` ` — binary operators usage +* ` = ` — in-place operator usage + +### Character operators + +A set of pre-defined character operators: + +* `!$%&*+-:^~` — unary charset +* `%&*+-/<=>^|` — binary charset +* `==`, `**`, `//`, `<<`, `>>` — double-char binary operators + +### Keyword operators + +A set of pre-defined keyword operators: + +* `not` — unary keyword operator +* `and`, `or`, `xor`, `is`, `is not`, `to` — binary keyword operators + +## Specials + +* `#`, `#|`, `|#` — comment specials +* `;` — expr separator special +* `->`, `@.`, `@`, `.`, `:` — attrget optype specials +* `[`, `]` — itemget specials +* `\,?=(){}` — other specials charset + +# Footnotes + +All character class checks are performed in current locale. + +--- +_by Sdore, 2019_ diff --git a/Slang.py b/Slang.py new file mode 100755 index 0000000..8171977 --- /dev/null +++ b/Slang.py @@ -0,0 +1,60 @@ +#!/usr/bin/python3 +# Slang + +from .ast import * +from .repl import * +from .compilers import * +from .compilers.pyssembly import * +from utils.nolog import *; logstart('Slang') + +def debug_compile(src, filename=''): + try: + #print(f"Source: {{\n{S(src).indent()}\n}}\n") + + tl = parse_string(src) + #print(f"Tokens:\n{pformat(tl)}\n") + + ast = build_ast(tl, filename) + #print(f"Code: {repr(ast.code)}\n") + + #print(f"Nodes: {pformat(list(walk_ast_nodes(ast)))}\n") + + optimize_ast(ast, validate_ast(ast)) + #print(f"Optimized: {repr(ast.code)}\n") + + code = PyssemblyCompiler.compile_ast(ast, validate_ast(ast), filename=filename) + #print("Compiled.\n") + + #print("Running.\n") + #exec(code, {}) + #print("\nFinished.\n") + except (SlSyntaxError, SlValidationError, SlCompilationError) as ex: + ex.line = src.split('\n')[ex.lineno-1] + sys.exit(ex) + except pyssembly.PyssemblyError as ex: + print('Error:', ex) + try: code = ex.code.to_code() + except pyssembly.PyssemblyError: pass + else: + print("\nHere is full pyssembly code 'til the errorneous line:\n") + dis.dis(code) + sys.exit(1) + else: return code + +def main(cargs): + if (cargs.o is None and not cargs.file.name.rpartition('.')[0]): + argparser.add_argument('-o', metavar='', required=True) + cargs = argparser.parse_args() + src = cargs.file.read() + filename = cargs.file.name + code = debug_compile(src, filename=filename.join('""')) + open(cargs.o or cargs.file.name.rpartition('.')[0]+'.pyc', 'wb').write(pyssembly.asm(code)) + +if (__name__ == '__main__'): + argparser.add_argument('file', metavar='', type=argparse.FileType('r')) + argparser.add_argument('-o', metavar='') + cargs = argparser.parse_args() + logstarted(); exit(main(cargs)) +else: logimported() + +# by Sdore, 2019 diff --git a/TODO.md b/TODO.md new file mode 100644 index 0000000..49dbb26 --- /dev/null +++ b/TODO.md @@ -0,0 +1 @@ +- arrays \ No newline at end of file diff --git a/__main__.py b/__main__.py new file mode 120000 index 0000000..affcdf4 --- /dev/null +++ b/__main__.py @@ -0,0 +1 @@ +Slang.py \ No newline at end of file diff --git a/ast.py b/ast.py new file mode 100644 index 0000000..ea277c2 --- /dev/null +++ b/ast.py @@ -0,0 +1,1283 @@ +#!/usr/bin/python3 +# Slang AST + +import abc +from .tokens import * +from utils import * + +DEBUG_PRECEDENCE = False + +def warn(class_, msg, node, ns): + if (ns.warnclasses[class_] == False): return + logexception(Warning(f"{msg} \033[8m({class_})\033[0m"), raw=True) + +def literal_type(x): + r = eval(str(x)) + if (isinstance(r, str) and len(r) == 1 and re.match(r"'.+?'", x.strip())): return stdlib.char + return type(r) + +def common_type(l, ns): # TODO + r = set() + for i in l: r.add(Signature.build(i, ns)) + if (not r): return None + if (len(r) > 1): raise TODO(r) + return next(iter(r)) + +class ASTNode(abc.ABC): + __slots__ = ('lineno', 'offset', 'flags') + + @abc.abstractmethod + @init_defaults + def __init__(self, *, lineno, offset, flags: paramset): + self.lineno, self.offset, self.flags = lineno, offset, flags + + def __repr__(self): + return f"<{self.typename} '{self.__str__()}' on line {self.lineno}, offset {self.offset}>" + + @abc.abstractmethod + def __str__(self): + pass + + @abc.abstractclassmethod + def build(cls, tl): + if (not tl): raise SlSyntaxNoToken() + + def validate(self, ns): + for i in self.__slots__: + v = getattr(self, i) + if (isiterable(v) and not isinstance(v, str)): + for jj, j in enumerate(v): + if (hasattr(j, 'validate')): + j.validate(ns) + elif (hasattr(v, 'validate') and i != 'code'): + v.validate(ns) + + def optimize(self, ns): + for i in self.__slots__: + v = getattr(self, i) + if (isiterable(v) and not isinstance(v, str)): + for jj, j in enumerate(v.copy()): + if (hasattr(j, 'optimize')): + r = j.optimize(ns) + if (r is not None): v[jj] = r + if (v[jj].flags.optimized_out): del v[jj] + elif (hasattr(v, 'optimize') and i != 'code'): + r = v.optimize(ns) + if (r is not None): setattr(self, i, r) + if (getattr(self, i).flags.optimized_out): setattr(self, i, None) + + @classproperty + def typename(cls): + return cls.__name__[3:-4] + + @property + def length(self): + return sum(getattr(self, i).length for i in self.__slots__ if hasattr(getattr(self, i), 'length')) + +class ASTRootNode(ASTNode): + __slots__ = ('code',) + + def __init__(self, code, **kwargs): + super().__init__(lineno=None, offset=None, **kwargs) + self.code = code + + def __repr__(self): + return '' + + def __str__(self): + return '' + + @classmethod + def build(cls, name=None): + return cls((yield from ASTCodeNode.build(name))) + +class ASTCodeNode(ASTNode): + __slots__ = ('nodes', 'name') + + def __init__(self, nodes, *, name='', **kwargs): + super().__init__(lineno=None, offset=None, **kwargs) + self.nodes, self.name = nodes, name + + def __repr__(self): + return (S('\n').join(self.nodes).indent().join('\n\n') if (self.nodes) else '').join('{}') + + def __str__(self): + return f"""""" + + @classmethod + def build(cls, name): + yield name + code = cls([], name=name) + while (True): + c = yield + if (c is None): break + code.nodes.append(c) + return code + + def validate(self, ns=None): + if (ns is None): ns = Namespace(self.name) + for i in self.nodes: + i.validate(ns) + return ns + + def optimize(self, ns): + for ii, i in enumerate(self.nodes): + r = i.optimize(ns) + if (r is not None): self.nodes[ii] = r + if (self.nodes[ii].flags.optimized_out): del self.nodes[ii] + +class ASTTokenNode(ASTNode): + @abc.abstractclassmethod + def build(cls, tl): + super().build(tl) + off = int() + for ii, i in enumerate(tl.copy()): + if (i.typename == 'SPECIAL' and (i.token[0] == '#' or i.token == '\\\n')): del tl[ii-off]; off += 1 + if (not tl): raise SlSyntaxEmpty() + + @property + def length(self): + return sum(len(getattr(self, i)) for i in self.__slots__) + +class ASTIdentifierNode(ASTTokenNode): + __slots__ = ('identifier',) + + def __init__(self, identifier, **kwargs): + super().__init__(**kwargs) + self.identifier = identifier + + def __str__(self): + return str(self.identifier) + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + if (tl[0].typename != 'IDENTIFIER'): raise SlSyntaxExpectedError('IDENTIFIER', tl[0]) + identifier = tl.pop(0).token + + return cls(identifier, lineno=lineno, offset=offset) + +class ASTKeywordNode(ASTTokenNode): + __slots__ = ('keyword',) + + def __init__(self, keyword, **kwargs): + super().__init__(**kwargs) + self.keyword = keyword + + def __str__(self): + return str(self.keyword) + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + if (tl[0].typename != 'KEYWORD'): raise SlSyntaxExpectedError('KEYWORD', tl[0]) + keyword = tl.pop(0).token + + return cls(keyword, lineno=lineno, offset=offset) + +class ASTLiteralNode(ASTTokenNode): + __slots__ = ('literal',) + + def __init__(self, literal, **kwargs): + super().__init__(**kwargs) + self.literal = literal + + def __str__(self): + return str(self.literal) + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + if (tl[0].typename != 'LITERAL'): raise SlSyntaxExpectedError('LITERAL', tl[0]) + literal = tl.pop(0).token + + return cls(literal, lineno=lineno, offset=offset) + +class ASTOperatorNode(ASTTokenNode): + __slots__ = ('operator',) + + def __init__(self, operator, **kwargs): + super().__init__(**kwargs) + self.operator = operator + + def __str__(self): + return str(self.operator) + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + if (tl[0].typename != 'OPERATOR'): raise SlSyntaxExpectedError('OPERATOR', tl[0]) + operator = tl.pop(0).token + + return cls(operator, lineno=lineno, offset=offset) + +class ASTSpecialNode(ASTTokenNode): + __slots__ = ('special',) + + def __init__(self, special, **kwargs): + super().__init__(**kwargs) + self.special = special + + def __str__(self): + return str(self.special) + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + if (tl[0].typename != 'SPECIAL'): raise SlSyntaxExpectedError('SPECIAL', tl[0]) + special = tl.pop(0).token + if (special == '\\'): raise SlSyntaxEmpty() + + return cls(special, lineno=lineno, offset=offset) + +class ASTPrimitiveNode(ASTNode): pass + +class ASTValueNode(ASTPrimitiveNode): + __slots__ = ('value',) + + def __init__(self, value, **kwargs): + super().__init__(**kwargs) + self.value = value + + def __str__(self): + return str(self.value) + + @classmethod + def build(cls, tl, *, fcall=False): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + types = [ASTLiteralNode, ASTFunccallNode, ASTAttrgetNode, ASTItemgetNode, ASTIdentifierNode, ASTLambdaNode] + if (fcall): types.remove(ASTFunccallNode); types.remove(ASTLambdaNode) + if (tl[0].typename == 'LITERAL'): value = ASTLiteralNode.build(tl) + else: + for i in types: + tll = tl.copy() + try: value = i.build(tll) + except SlSyntaxException: continue + else: tl[:] = tll; break + else: raise SlSyntaxExpectedError('Value', tl[0]) + + return cls(value, lineno=lineno, offset=offset) + + def validate(self, ns): + super().validate(ns) + if (isinstance(self.value, ASTIdentifierNode)): + if (self.value.identifier not in ns): raise SlValidationNotDefinedError(self.value, scope=ns.scope) + if (self.value.identifier not in ns.values): warn('uninitialized', f"using value of possibly uninitialized variable '{self.value}'", self, ns) + + def optimize(self, ns): + super().optimize(ns) + if (isinstance(self.value, ASTIdentifierNode) and self.value in ns.values): self.value = ASTLiteralNode(repr(ns.values[self.value]), lineno=self.lineno, offset=self.offset) # TODO FIXME in functions + +class ASTItemgetNode(ASTPrimitiveNode): + __slots__ = ('value', 'key') + + def __init__(self, value, key, **kwargs): + super().__init__(**kwargs) + self.value, self.key = value, key + + def __str__(self): + return f"{self.value}[{self.key}]" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + value = ASTIdentifierNode.build(tl) # TODO: value/expr + bracket = ASTSpecialNode.build(tl) + if (bracket.special != '['): raise SlSyntaxExpectedError("'['", bracket) + key = ASTExprNode.build(tl) + bracket = ASTSpecialNode.build(tl) + if (bracket.special != ']'): raise SlSyntaxExpectedError("']'", bracket) + + return cls(value, key, lineno=lineno, offset=offset) + + def validate(self, ns): + super().validate(ns) + valsig = Signature.build(self.value, ns) + keysig = Signature.build(self.key, ns) + if (keysig not in valsig.itemget): raise SlValidationError(f"'{valsig}' does not support itemget by key of type '{keysig}'", self, scope=ns.scope) + +class ASTAttrgetNode(ASTPrimitiveNode): + __slots__ = ('value', 'optype', 'attr') + + def __init__(self, value, optype, attr, **kwargs): + super().__init__(**kwargs) + self.value, self.optype, self.attr = value, optype, attr + + def __str__(self): + return f"{self.value}{self.optype}{self.attr}" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + value = ASTIdentifierNode.build(tl) + optype = ASTSpecialNode.build(tl) + if (optype.special not in attrops): raise SlSyntaxExpectedError(f"one of {attrops}", optype) + attr = ASTIdentifierNode.build(tl) + + return cls(value, optype, attr, lineno=lineno, offset=offset) + + def validate(self, ns): + super().validate(ns) + if (self.optype not in Signature.build(self.value, ns).attrops): raise SlValidationError(f"'{self.value}' does not support attribute operation '{self.optype}'", self, scope=ns.scope) + +class ASTExprNode(ASTPrimitiveNode): + @classmethod + def build(cls, tl, *, fcall=False): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + for ii, p in enumerate(operators[::-1]): + for i in p: + tll = tl.copy() + try: value = ASTBinaryExprNode.build(tll, i) + except SlSyntaxException: continue + else: tl[:] = tll; return value + + tll = tl.copy() + try: value = ASTUnaryExprNode.build(tll) + except SlSyntaxException: pass + else: tl[:] = tll; return value + + tll = tl.copy() + try: value = ASTValueNode.build(tll, fcall=fcall) + except SlSyntaxException: pass + else: tl[:] = tll; return value + + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != '('): raise SlSyntaxExpectedError('Expr', parenthesis) + parenthesized = list() + lvl = 1 + while (tl): + if (tl[0].typename == 'SPECIAL'): lvl += 1 if (tl[0].token == '(') else -1 if (tl[0].token == ')') else 0 + if (lvl == 0): + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != ')'): raise SlSyntaxExpectedError("')'", parenthesis) + break + assert lvl > 0 + parenthesized.append(tl.pop(0)) + value = ASTExprNode.build(parenthesized) + if (parenthesized): raise SlSyntaxExpectedNothingError(parenthesized[0]) + return value + +class ASTUnaryExprNode(ASTExprNode): + __slots__ = ('operator', 'value') + + def __init__(self, operator, value, **kwargs): + super(ASTPrimitiveNode, self).__init__(**kwargs) + self.operator, self.value = operator, value + + def __str__(self): + return f"{self.operator}{' ' if (self.operator.operator.isalpha()) else ''}{str(self.value).join('()') if (DEBUG_PRECEDENCE or isinstance(self.value, ASTBinaryExprNode) and operator_precedence(self.value.operator.operator) >= operator_precedence(self.operator.operator)) else self.value}" + + @classmethod + def build(cls, tl): + ASTPrimitiveNode.build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + operator = ASTOperatorNode.build(tl) + if (operator.operator in bothoperators): operator.operator = UnaryOperator(operator.operator) + elif (not isinstance(operator.operator, UnaryOperator)): raise SlSyntaxExpectedError('UnaryOperator', operator) + value = ASTExprNode.build(tl) + + return cls(operator, value, lineno=lineno, offset=offset) + + def validate(self, ns): + super().validate(ns) + sig = Signature.build(self.value, ns) + op = self.operator.operator + if (op not in sig.operators): raise SlValidationError(f"'{sig}' does not support unary operator '{op}'", self, scope=ns.scope) + + def optimize(self, ns): + super().optimize(ns) + sig = Signature.build(self.value, ns) + if (self.value in ns.values): return ASTValueNode(ASTLiteralNode(eval(f"{self.operator} {ns.values[self.value]}"), lineno=self.lineno, offset=self.offset), lineno=self.lineno, offset=self.offset) + +class ASTBinaryExprNode(ASTExprNode): + __slots__ = ('lvalue', 'operator', 'rvalue') + + def __init__(self, lvalue, operator, rvalue, **kwargs): + super(ASTPrimitiveNode, self).__init__(**kwargs) + self.lvalue, self.operator, self.rvalue = lvalue, operator, rvalue + + def __str__(self): + return f"{str(self.lvalue).join('()') if (DEBUG_PRECEDENCE or isinstance(self.lvalue, ASTBinaryExprNode) and operator_precedence(self.lvalue.operator.operator) > operator_precedence(self.operator.operator)) else self.lvalue}{str(self.operator).join(' ') if (operator_precedence(self.operator.operator) > 0) else self.operator}{str(self.rvalue).join('()') if (DEBUG_PRECEDENCE or isinstance(self.rvalue, ASTBinaryExprNode) and operator_precedence(self.rvalue.operator.operator) > operator_precedence(self.operator.operator)) else self.rvalue}" + + @classmethod + def build(cls, tl, op): + ASTPrimitiveNode.build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + lasti = None + lvl = int() + for ii, i in enumerate(tl): + if (i.typename == 'SPECIAL'): lvl += 1 if (i.token == '(') else -1 if (i.token == ')') else 0 + if (lvl > 0): continue + if (i.typename == 'OPERATOR' and isinstance(i.token, BinaryOperator) and i.token == op): lasti = ii + if (lasti is None): raise SlSyntaxExpectedError('BinaryOperator', tl[0]) + tll, tl[:] = tl[:lasti], tl[lasti:] + lvalue = ASTExprNode.build(tll) + if (tll): raise SlSyntaxExpectedNothingError(tll[0]) + operator = ASTOperatorNode.build(tl) + rvalue = ASTExprNode.build(tl) + + return cls(lvalue, operator, rvalue, lineno=lineno, offset=offset) + + def validate(self, ns): + super().validate(ns) + lsig = Signature.build(self.lvalue, ns) + rsig = Signature.build(self.rvalue, ns) + op = self.operator.operator + if ((op, rsig) not in lsig.operators): raise SlValidationError(f"'{lsig}' does not support operator '{op}' with operand of type '{rsig}'", self, scope=ns.scope) + + def optimize(self, ns): + super().optimize(ns) + if (self.operator.operator == '**' and self.lvalue in ns.values and ns.values[self.lvalue] == 2): self.operator.operator, self.lvalue.value, ns.values[self.lvalue] = BinaryOperator('<<'), ASTLiteralNode('1', lineno=self.lvalue.value.lineno, offset=self.lvalue.value.offset), 1 + if (self.lvalue in ns.values and self.rvalue in ns.values and self.operator.operator != 'to'): return ASTValueNode(ASTLiteralNode(repr(eval(str(self))), lineno=self.lineno, offset=self.offset), lineno=self.lineno, offset=self.offset) + +class ASTNonFinalNode(ASTNode): pass + +class ASTTypedefNode(ASTNonFinalNode): + __slots__ = ('modifiers', 'type') + + def __init__(self, modifiers, type, **kwargs): + super().__init__(**kwargs) + self.modifiers, self.type = modifiers, type + + def __str__(self): + return f"{S(' ').join(self.modifiers)}{' ' if (self.modifiers and self.type) else ''}{self.type or ''}" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + modifiers = list() + while (tl and tl[0].typename == 'KEYWORD'): + if (not isinstance(tl[0].token, Modifier)): raise SlSyntaxExpectedError('Modifier', tl[0]) + modifiers.append(ASTKeywordNode.build(tl)) + type = ASTIdentifierNode.build(tl) + + return cls(modifiers, type, lineno=lineno, offset=offset) + +class ASTArgdefNode(ASTNonFinalNode): + __slots__ = ('type', 'name', 'modifier', 'value') + + def __init__(self, type, name, modifier, value, **kwargs): + super().__init__(**kwargs) + self.type, self.name, self.modifier, self.value = type, name, modifier, value + + def __str__(self): + return f"{f'{self.type} ' if (self.type) else ''}{self.name}{self.modifier or ''}{f'={self.value}' if (self.value is not None) else ''}" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + type = ASTTypedefNode.build(tl) + name = ASTIdentifierNode.build(tl) + modifier = ASTOperatorNode.build(tl) if (tl and tl[0].typename == 'OPERATOR' and tl[0].token in '+**') else ASTSpecialNode.build(tl) if (tl and tl[0].typename == 'SPECIAL' and tl[0].token in '?=') else None + value = ASTValueNode.build(tl) if (modifier == '=') else None + + return cls(type, name, modifier, value, lineno=lineno, offset=offset) + + def validate(self, ns): + super().validate(ns) + if (isinstance(Signature.build(self.type, ns), stdlib.void)): raise SlValidationError(f"Argument cannot have type '{self.type}'", self, scope=ns.scope) + +class ASTCallargsNode(ASTNonFinalNode): + __slots__ = ('callargs', 'starargs') + + def __init__(self, callargs, starargs, **kwargs): + super().__init__(**kwargs) + self.callargs, self.starargs = callargs, starargs + + def __str__(self): + return S(', ').join((*self.callargs, *(f'*{i}' for i in self.starargs))) + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + callargs = list() + starargs = list() + if (tl and not (tl[0].typename == 'SPECIAL' and tl[0].token == ')')): + while (tl): + if (tl[0].typename == 'OPERATOR' and tl[0].token == '*'): + ASTOperatorNode.build(tl) + starargs.append(ASTExprNode.build(tl)) + elif (len(tl) >= 2 and tl[1].typename == 'SPECIAL' and tl[1].token == '='): break + else: callargs.append(ASTExprNode.build(tl)) + if (not tl or tl[0].typename != 'SPECIAL' or tl[0].token == ')'): break + comma = ASTSpecialNode.build(tl) + if (comma.special != ','): raise SlSyntaxExpectedError("','", comma) + + return cls(callargs, starargs, lineno=lineno, offset=offset) + +class ASTCallkwargsNode(ASTNonFinalNode): + __slots__ = ('callkwargs', 'starkwargs') + + def __init__(self, callkwargs, starkwargs, **kwargs): + super().__init__(**kwargs) + self.callkwargs, self.starkwargs = callkwargs, starkwargs + + def __str__(self): + return S(', ').join((*(f'{k}={v}' for k, v in self.callkwargs), *(f'**{i}' for i in self.starkwargs))) + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + callkwargs = list() + starkwargs = list() + if (tl and not (tl[0].typename == 'SPECIAL' and tl[0].token == ')')): + while (tl): + if (tl[0].typename == 'OPERATOR' and tl[0].token == '**'): + ASTOperatorNode.build(tl) + starkwargs.append(ASTExprNode.build(tl)) + else: + key = ASTIdentifierNode.build(tl) + eq = ASTSpecialNode.build(tl) + if (eq.special != '='): raise SlSyntaxExpectedError("'='", eq) + value = ASTExprNode.build(tl) + callkwargs.append((key, value)) + if (not tl or tl[0].typename != 'SPECIAL' or tl[0].token == ')'): break + comma = ASTSpecialNode.build(tl) + if (comma.special != ','): raise SlSyntaxExpectedError("','", comma) + + return cls(callkwargs, starkwargs, lineno=lineno, offset=offset) + +class ASTCallableNode(ASTNode): + def validate(self, ns): + super().validate(ns) + #dlog('-->', self.code.name) + code_ns = ns.derive(self.code.name) + for i in self.argdefs: code_ns.define(i, redefine=True) + self.code.validate(code_ns) + rettype = common_type((i.value for i in self.code.nodes if (isinstance(i, ASTKeywordExprNode) and i.keyword.keyword == 'return')), code_ns) or stdlib.void() + if (self.type.type.identifier == 'auto'): self.type.type.identifier = rettype.typename + else: + expected = Signature.build(self.type, ns) + if (rettype != expected): raise SlValidationError(f"Returning value of type '{rettype}' from function with return type '{expected}'", self, scope=ns.scope) + #dlog('<--', self.code.name) + + def optimize(self, ns): + super().optimize(ns) + #dlog('-->', self.code.name) + code_ns = ns.derive(self.code.name) + for i in self.argdefs: code_ns.define(i, redefine=True) + self.code.optimize(code_ns) + #dlog('<--', self.code.name) + +class ASTLambdaNode(ASTNonFinalNode, ASTCallableNode): + __slots__ = ('argdefs', 'type', 'code') + + def __init__(self, argdefs, type, code, **kwargs): + super().__init__(**kwargs) + self.argdefs, self.type, self.code = argdefs, type, code + + def __fsig__(self): + return f"({S(', ').join(self.argdefs)}) -> {self.type}" + + def __repr__(self): + return f"" + + def __str__(self): + return f"{self.__fsig__()} {f'= {self.code.nodes[0].value}' if (len(self.code.nodes) == 1 and isinstance(self.code.nodes[0], ASTKeywordExprNode) and self.code.nodes[0].keyword.keyword == 'return') else repr(self.code)}" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != '('): raise SlSyntaxExpectedError("'('", parenthesis) + argdefs = list() + while (tl and tl[0].typename != 'SPECIAL'): + argdefs.append(ASTArgdefNode.build(tl)) + if (tl and tl[0].typename == 'SPECIAL' and tl[0].token == ','): ASTSpecialNode.build(tl) + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != ')'): raise SlSyntaxExpectedError("')'", parenthesis) + arrow = ASTOperatorNode.build(tl) + if (arrow.operator != '->'): raise SlSyntaxExpectedError("'->'", arrow) + type = ASTTypedefNode.build(tl) + if (tl and (tl[0].typename != 'SPECIAL' or tl[0].token not in (*'={',))): raise SlSyntaxExpectedError("'=' or '{'", tl[0]) + cdef = ASTSpecialNode.build(tl) + if (cdef.special != '='): raise SlSyntaxExpectedError('=', cdef) + code = ASTCodeNode([ASTKeywordExprNode(ASTKeywordNode('return', lineno=lineno, offset=offset), ASTExprNode.build(tl), lineno=lineno, offset=offset)], name='') + + return cls(argdefs, type, code, lineno=lineno, offset=offset) + +class ASTBlockNode(ASTNonFinalNode): + __slots__ = ('code',) + + def __init__(self, code, **kwargs): + super().__init__(**kwargs) + self.code = code + + def __str__(self): + return repr(self.code) if (len(self.code.nodes) > 1) else repr(self.code)[1:-1].strip() + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + if (tl[0].typename == 'SPECIAL' and tl[0].token == '{'): + ASTSpecialNode.build(tl) + code = (yield from ASTCodeNode.build('')) + else: + yield '' + code = ASTCodeNode([ASTExprNode.build(tl)], name='') + + return cls(code, lineno=lineno, offset=offset) + +class ASTFinalNode(ASTNode): pass + +class ASTDefinitionNode(ASTNode): + def validate(self, ns): + super().validate(ns) + ns.define(self) + +class ASTFuncdefNode(ASTFinalNode, ASTDefinitionNode, ASTCallableNode): + __slots__ = ('type', 'name', 'argdefs', 'code') + + def __init__(self, type, name, argdefs, code, **kwargs): + super().__init__(**kwargs) + self.type, self.name, self.argdefs, self.code = type, name, argdefs, code + + def __fsig__(self): + return f"{self.type or 'def'} {self.name}({S(', ').join(self.argdefs)})" + + def __repr__(self): + return f"" + + def __str__(self): + isexpr = len(self.code.nodes) == 1 and isinstance(self.code.nodes[0], ASTKeywordExprNode) and self.code.nodes[0].keyword.keyword == 'return' + r = f"{self.__fsig__()} {f'= {self.code.nodes[0].value}' if (isexpr) else repr(self.code)}" + return r if (isexpr) else r.join('\n\n') + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + type = ASTTypedefNode.build(tl) + name = ASTIdentifierNode.build(tl) + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != '('): raise SlSyntaxExpectedError("'('", parenthesis) + argdefs = list() + while (tl and tl[0].typename != 'SPECIAL'): + argdef = ASTArgdefNode.build(tl) + if (argdefs and argdef.value is None and argdefs[-1].value is not None): raise SlSyntaxError(f"Non-default argument {argdef} follows default argument {argdefs[-1]}") + argdefs.append(argdef) + if (tl and tl[0].typename == 'SPECIAL' and tl[0].token == ','): ASTSpecialNode.build(tl) + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != ')'): raise SlSyntaxExpectedError("')'", parenthesis) + if (not tl): raise SlSyntaxExpectedError("'=' or '{'", lineno=lineno, offset=-1) + if (tl[0].typename != 'SPECIAL' or tl[0].token not in (*'={',)): raise SlSyntaxExpectedError("'=' or '{'", tl[0]) + cdef = ASTSpecialNode.build(tl) + if (cdef.special == '='): + yield name.identifier + code = ASTCodeNode([ASTKeywordExprNode(ASTKeywordNode('return', lineno=lineno, offset=offset), ASTExprNode.build(tl), lineno=lineno, offset=offset)], name=name.identifier) + elif (cdef.special == '{'): + code = (yield from ASTCodeNode.build(name.identifier)) + + return cls(type, name, argdefs, code, lineno=lineno, offset=offset) + +class ASTKeywordExprNode(ASTFinalNode): + __slots__ = ('keyword', 'value') + + def __init__(self, keyword, value, **kwargs): + super().__init__(**kwargs) + self.keyword, self.value = keyword, value + + def __str__(self): + return f"{self.keyword}{f' {self.value}' if (self.value is not None) else ''}" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + keyword = ASTKeywordNode.build(tl) + if (not isinstance(keyword.keyword, ExprKeyword)): raise SlSyntaxExpectedError('ExprKeyword', keyword) + if (not tl): value = None + elif (keyword.keyword == 'import'): + lineno_, offset_ = tl[0].lineno, tl[0].offset + value = ASTIdentifierNode(str().join(tl.pop(0).token for _ in range(len(tl))), lineno=lineno_, offset=offset_) # TODO Identifier? (or document it) + else: value = ASTExprNode.build(tl) + + return cls(keyword, value, lineno=lineno, offset=offset) + + def validate(self, ns): + if (self.keyword.keyword == 'import'): + ns.define(ASTIdentifierNode(self.value.identifier.partition('::')[2], lineno=self.value.lineno, offset=self.value.offset)) + return + super().validate(ns) + +class ASTAssignvalNode(ASTNode): + def validate(self, ns): + super().validate(ns) + if (self.name.identifier not in ns): raise SlValidationNotDefinedError(self.name, scope=ns.scope) + vartype = ns.signatures[self.name.identifier] + if (self.value is not None): + valtype = Signature.build(self.value, ns) + if (vartype != valtype): raise SlValidationError(f"Assignment of '{self.value}' of type '{valtype}' to '{self.name}' of type '{vartype}'", self, scope=ns.scope) + +class ASTVardefNode(ASTFinalNode, ASTAssignvalNode, ASTDefinitionNode): + __slots__ = ('type', 'name', 'value') + + def __init__(self, type, name, value, **kwargs): + super().__init__(**kwargs) + self.type, self.name, self.value = type, name, value + + def __str__(self): + return f"{self.type} {self.name}{f' = {self.value}' if (self.value is not None) else ''}" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + type = ASTTypedefNode.build(tl) + name = ASTIdentifierNode.build(tl) + assignment = ASTSpecialNode.build(tl) if (tl and tl[0].typename == 'SPECIAL') else None + if (assignment is not None and assignment.special != '='): raise SlSyntaxExpectedError('assignment', assignment) + value = ASTExprNode.build(tl) if (assignment is not None) else None + + return cls(type, name, value, lineno=lineno, offset=offset) + + def validate(self, ns): + if (self.type.type.identifier == 'auto'): self.type.type.identifier = Signature.build(self.value, ns).typename + super().validate(ns) + + def optimize(self, ns): + super().optimize(ns) + #if (ns.signatures[self.name.identifier].modifiers.const): self.flags.optimized_out = True # TODO + +class ASTAssignmentNode(ASTFinalNode, ASTAssignvalNode): + __slots__ = ('name', 'inplace_operator', 'value') + + def __init__(self, name, inplace_operator, value, **kwargs): + super().__init__(**kwargs) + self.name, self.inplace_operator, self.value = name, inplace_operator, value + + def __str__(self): + return f"{self.name} {self.inplace_operator or ''}= {self.value}" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + name = ASTIdentifierNode.build(tl) + inplace_operator = ASTOperatorNode.build(tl) if (tl and tl[0].typename == 'OPERATOR') else None + if (inplace_operator is not None and not isinstance(inplace_operator.operator, BinaryOperator)): raise SlSyntaxExpectedError('BinaryOperator', inplace_operator) + assignment = ASTSpecialNode.build(tl) + if (assignment.special != '='): raise SlSyntaxExpectedError('assignment', assignment) + value = ASTExprNode.build(tl) + + return cls(name, inplace_operator, value, lineno=lineno, offset=offset) + + def validate(self, ns): + super().validate(ns) + vartype = ns.signatures[self.name.identifier] + valtype = Signature.build(self.value, ns) + if (vartype.modifiers.const): raise SlValidationError(f"Assignment to const '{self.name}'", self, scope=ns.scope) + +class ASTFunccallNode(ASTFinalNode): + __slots__ = ('callable', 'callargs', 'callkwargs') + + def __init__(self, callable, callargs, callkwargs, **kwargs): + super().__init__(**kwargs) + self.callable, self.callargs, self.callkwargs = callable, callargs, callkwargs + + def __str__(self): + return f"{str(self.callable).join('()') if (isinstance(self.callable, ASTValueNode) and isinstance(self.callable.value, (ASTFunccallNode, ASTLambdaNode))) else self.callable}({self.callargs}{', ' if (str(self.callargs) and str(self.callkwargs)) else ''}{self.callkwargs})" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + callable = ASTExprNode.build(tl, fcall=True) + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != '('): raise SlSyntaxExpectedError("'('", parenthesis) + callargs = ASTCallargsNode.build(tl) + callkwargs = ASTCallkwargsNode.build(tl) + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != ')'): raise SlSyntaxExpectedError("')'", parenthesis) + + return cls(callable, callargs, callkwargs, lineno=lineno, offset=offset) + + def validate(self, ns): + super().validate(ns) + fsig = Signature.build(self.callable, ns) + if (not isinstance(fsig, Function)): raise SlValidationError(f"'{self.callable}' of type '{fsig}' is not callable", self.callable, scope=ns.scope) + callargssig = CallArguments.build(self, ns) # TODO: starargs + if (callargssig not in fsig.call): raise SlValidationError(f"Parameters '({callargssig})' don't match any of '{self.callable}' signatures:\n{S(fsig.callargssigstr).indent()}\n", self, scope=ns.scope) + +class ASTConditionalNode(ASTFinalNode): + __slots__ = ('condition', 'code') + + def __init__(self, condition, code, **kwargs): + super().__init__(**kwargs) + self.condition, self.code = condition, code + + def __str__(self): + return f"if ({self.condition}) {self.code}" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + if_ = ASTKeywordNode.build(tl) + if (if_.keyword != 'if'): raise SlSyntaxExpectedError("'if'", if_) + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != '('): raise SlSyntaxExpectedError("'('", parenthesis) + condition = ASTExprNode.build(tl) + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != ')'): raise SlSyntaxExpectedError("')'", parenthesis) + code = (yield from ASTBlockNode.build(tl)) + + return cls(condition, code, lineno=lineno, offset=offset) + +class ASTForLoopNode(ASTFinalNode): + __slots__ = ('name', 'iterable', 'code') + + def __init__(self, name, iterable, code, **kwargs): + super().__init__(**kwargs) + self.name, self.iterable, self.code = name, iterable, code + + def __str__(self): + return f"for {self.name} in ({self.iterable}) {self.code}" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + for_ = ASTKeywordNode.build(tl) + if (for_.keyword != 'for'): raise SlSyntaxExpectedError("'for'", for_) + name = ASTIdentifierNode.build(tl) + in_ = ASTKeywordNode.build(tl) + if (in_.keyword != 'in'): raise SlSyntaxExpectedError("'in'", in_) + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != '('): raise SlSyntaxExpectedError("'('", parenthesis) + iterable = ASTExprNode.build(tl) + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != ')'): raise SlSyntaxExpectedError("')'", parenthesis) + code = (yield from ASTBlockNode.build(tl)) + + return cls(name, iterable, code, lineno=lineno, offset=offset) + +class ASTWhileLoopNode(ASTFinalNode): + __slots__ = ('condition', 'code') + + def __init__(self, condition, code, **kwargs): + super().__init__(**kwargs) + self.condition, self.code = condition, code + + def __str__(self): + return f"while ({self.condition}) {self.code}" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + while_ = ASTKeywordNode.build(tl) + if (while_.keyword != 'while'): raise SlSyntaxExpectedError("'while'", while_) + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != '('): raise SlSyntaxExpectedError("'('", parenthesis) + condition = ASTExprNode.build(tl) + parenthesis = ASTSpecialNode.build(tl) + if (parenthesis.special != ')'): raise SlSyntaxExpectedError("')'", parenthesis) + code = (yield from ASTBlockNode.build(tl)) + + return cls(condition, code, lineno=lineno, offset=offset) + +class ASTElseClauseNode(ASTFinalNode): + __slots__ = ('code',) + + def __init__(self, code, **kwargs): + super().__init__(**kwargs) + self.code = code + + def __str__(self): + return f"else {self.code}" + + @classmethod + def build(cls, tl): + super().build(tl) + lineno, offset = tl[0].lineno, tl[0].offset + + else_ = ASTKeywordNode.build(tl) + if (else_.keyword != 'else'): raise SlSyntaxExpectedError("'else'", else_) + code = (yield from ASTBlockNode.build(tl)) + + return cls(code, lineno=lineno, offset=offset) + +def build_ast(code, name=None, *, interactive=False): + code = copy.deepcopy(code) + root = ASTRootNode.build(name) + code_stack = [(root, next(root))] + next(root) + + final_nodes = ASTFinalNode.__subclasses__() + if (interactive): final_nodes += (ASTExprNode,) + for ii, tl in enumerate(code): + if (not tl): continue + lineno, offset = tl[0].lineno, tl[0].offset + + err = set() + for i in final_nodes: + try: + c = tl.copy() + r = i.build(c) + if (inspect.isgenerator(r)): + code_stack.append((r, next(r))) + try: r = next(r) + except StopIteration as ex: code_stack.pop(); r = ex.args[0] + else: + assert r is None + if (c): + if (c[-1].typename == 'SPECIAL' and c[-1].token == '}'): code.insert(ii+1, [c.pop()]) + code.insert(ii+1, c) + err.clear() + break + assert r is not None + if (c): raise SlSyntaxExpectedNothingError(c[0]) + except SlSyntaxEmpty: err.clear(); break + except SlSyntaxNoToken: err.add(SlSyntaxExpectedError(f"More tokens for {i.__name__[3:-4]}", lineno=lineno, offset=offset)) + except SlSyntaxExpectedError as ex: ex.expected += f" at offset {ex.offset if (ex.offset != -1) else ''} (for {i.__name__[3:-4]})"; err.add(ex) + else: code_stack[-1][0].send(r); err.clear(); break + else: + if (len(code_stack) > 1 and tl and tl[0].typename == 'SPECIAL' and tl[0].token == '}'): + if (tl[1:]): code.insert(ii+1, tl[1:]) + try: next(code_stack.pop()[0]) + except StopIteration as ex: code_stack[-1][0].send(ex.args[0]); err.clear() + else: raise WTFException() + elif (not err): raise SlSyntaxError("Unknown structure", lineno=lineno, offset=offset, length=0, scope='.'.join(i[1] for i in code_stack if i[1])) + + if (err): raise SlSyntaxMultiExpectedError(S(sorted(set(map(operator.attrgetter('expected'), err)), reverse=True)).uniquize(), S(sorted(err, key=operator.attrgetter('offset'))).uniquize(), lineno=max(err, key=operator.attrgetter('lineno')).lineno, offset=max(err, key=lambda x: x.offset if (x.offset != -1) else inf).offset, length=min(i.length for i in err if i.length), scope='.'.join(i[1] for i in code_stack if i[1]) if (code_stack[0][1] is not None) else None) + + assert len(code_stack) == 1 + try: next(code_stack.pop()[0]) + except StopIteration as ex: return ex.args[0] + +def walk_ast_nodes(node): + if (isiterable(node) and not isinstance(node, str)): + for i in node: yield from walk_ast_nodes(i) + if (not isinstance(node, ASTNode)): return + yield node + for i in node.__slots__: yield from walk_ast_nodes(getattr(node, i)) + +class _SignatureBase: pass + +class Signature(_SignatureBase): + __slots__ = ('typename', 'modifiers') + + operators = dict() + call = dict() + itemget = dict() + attrops = () + + @init_defaults + @autocast + def __init__(self, *, typename, modifiers: paramset, attrops: tuple): + self.typename, self.modifiers, self.attrops = typename, modifiers, attrops + + @property + def __reprname__(self): + return type(self).__name__ + + def __repr__(self): + return f"<{self.__reprname__} {type(self).__name__}>" + + def __eq__(self, x): + return self.typename == x.typename + + def __hash__(self): + return hash(tuple(getattr(self, i) for i in self.__slots__)) + + @classmethod + @dispatch + def build(cls, x: ASTArgdefNode, ns): # TODO: modifiers + return cls.build(x.type, ns) + + @classmethod + @dispatch + def build(cls, x: ASTVardefNode, ns): + r = cls.build(x.type, ns) + ns.signatures[x.name.identifier] = r + if (x.value is not None): ns.values[x.name] = x.value if (not r.modifiers.volatile and r.modifiers.const) else None + return r + + @classmethod + @dispatch + def build(cls, x: ASTTypedefNode, ns): + if (x.type.identifier not in builtin_names): raise SlValidationNotDefinedError(x.type, scope=ns.scope) + return builtin_names[x.type.identifier](modifiers=x.modifiers) + + @classmethod + @dispatch + def build(cls, x: ASTValueNode, ns): + return cls.build(x.value, ns) + + @classmethod + @dispatch + def build(cls, x: ASTLiteralNode, ns): + return builtin_names[literal_type(x.literal).__name__]() + + @classmethod + @dispatch + def build(cls, x: ASTIdentifierNode, ns): + if (x.identifier in builtin_names): return builtin_names[x.identifier]() + if (x.identifier not in ns): raise SlValidationNotDefinedError(x, scope=ns.scope) + return ns.signatures[x.identifier] + + @classmethod + @dispatch + def build(cls, x: ASTFunccallNode, ns): + return cls.build(x.callable, ns).call[CallArguments.build(x, ns)] + + @classmethod + @dispatch + def build(cls, x: ASTFuncdefNode, ns): + return Function.build(x, ns) + + @classmethod + @dispatch + def build(cls, x: ASTItemgetNode, ns): + return cls.build(x.value, ns).itemget[cls.build(x.key, ns)] + + @classmethod + @dispatch + def build(cls, x: ASTAttrgetNode, ns): + return cls.build(x.value, ns).attrops[x.optype] + + @classmethod + @dispatch + def build(cls, x: ASTUnaryExprNode, ns): + return cls.build(x.value, ns).operators[x.operator.operator] + + @classmethod + @dispatch + def build(cls, x: ASTBinaryExprNode, ns): + return cls.build(x.lvalue, ns).operators[x.operator.operator, cls.build(x.rvalue, ns)] + + @classmethod + @dispatch + def build(cls, x: _SignatureBase, ns): + return x + +class Function(Signature): + __slots__ = ('name', 'call') + + def __init__(self, *, name, **kwargs): + super().__init__(typename='function', **kwargs) + self.name = name + self.call = listmap() + + def __repr__(self): + return f"" + + @property + def callargssigstr(self): + return '\n'.join(f"{self.name}({args})" for args, ret in self.call.items()) + + @classmethod + @dispatch + def build(cls, x: ASTFuncdefNode, ns, *, redefine=False): + name = x.name.identifier + if (name not in ns): ns.signatures[name] = cls(name=name) + callargssig = CallArguments(args=tuple(Signature.build(i, ns) for i in x.argdefs)) + if (not redefine and callargssig in ns.signatures[name].call): raise SlValidationRedefinedError(x, ns.signatures[name].call[callargssig], scope=ns.scope) + ns.signatures[name].call[callargssig] = Signature.build(x.type, ns) + return ns.signatures[name] + +class Object(Signature): + __slots__ = ('attrops',) + +class CallArguments: + __slots__ = ('args', 'starargs', 'kwargs', 'starkwargs') + + @init_defaults + @autocast + def __init__(self, *, args: tuple, starargs: tuple, kwargs: tuple, starkwargs: tuple): + self.args, self.starargs, self.kwargs, self.starkwargs = args, starargs, kwargs, starkwargs + + def __repr__(self): + return S(', ').join((*self.args, *('*'+i for i in self.starargs), *(f"{v} {k}" for k, v in self.kwargs), *('**'+i for i in self.starkwargs))) + + def __eq__(self, x): + return all(getattr(self, i) == getattr(x, i) for i in self.__slots__) + + def __hash__(self): + return hash(tuple(getattr(self, i) for i in self.__slots__)) + + @property + def nargs(self): + return sum(len(getattr(self, i)) for i in self.__slots__) + + @classmethod + @dispatch + def build(cls, x: ASTFunccallNode, ns): # TODO: starargs + return cls(args=tuple(Signature.build(i, ns) for i in x.callargs.callargs), kwargs=tuple((k, Signature.build(v, ns)) for k, v in x.callkwargs.callkwargs)) + +class Namespace: + __slots__ = ('signatures', 'scope', 'values', 'refcount', 'flags', 'warnclasses') + + class _Values: + @init_defaults + def __init__(self, values: dict): + self.values = values + + def __contains__(self, x): + try: self[x] + except (DispatchError, KeyError): return False + else: return True + + @dispatch + def __getitem__(self, x: ASTLiteralNode): + return eval(str(x.literal)) + + @dispatch + def __getitem__(self, x: ASTValueNode): + return self[x.value] + + @dispatch + def __getitem__(self, x: ASTIdentifierNode): + return self.values[x.identifier] + + @dispatch + def __getitem__(self, x: str): + return self.values[x] + + @dispatch + def __setitem__(self, k, v: ASTValueNode): + self[k] = v.value + + @dispatch + def __setitem__(self, x: ASTIdentifierNode, v: ASTLiteralNode): + self.values[x.identifier] = eval(str(v.literal)) + + @dispatch + def __setitem__(self, x: ASTIdentifierNode, v: NoneType): + pass + + @dispatch + def __setitem__(self, k: str, v): + self.values[k] = v + + @dispatch + def __setitem__(self, k, v): + pass + + @dispatch + def __delitem__(self, x: ASTValueNode): + del self[x.value] + + @dispatch + def __delitem__(self, x: ASTIdentifierNode): + del self.values[x.identifier] + + def copy(self): + return self.__class__(values=self.values.copy()) + + @init_defaults + def __init__(self, scope, *, signatures: dict, values: _Values, refcount: lambda: Sdict(int), warnclasses: paramset, **kwargs): + self.scope, self.signatures, self.values, self.refcount, self.warnclasses = scope, signatures, values, refcount, warnclasses + self.flags = paramset(k for k, v in kwargs.items() if v) + #self.derive.clear_cache() + + def __repr__(self): + return f"" + + def __contains__(self, x): + return x in builtin_names or x in self.signatures + + @cachedfunction + def derive(self, scope): + return Namespace(signatures=self.signatures.copy(), values=self.values.copy(), scope=self.scope+'.'+scope) + + @dispatch + def define(self, x: ASTIdentifierNode): + if (x.identifier in self): raise SlValidationRedefinedError(x, self.signatures[x.identifier], scope=self.scope) + self.values[x.identifier] = None + self.signatures[x.identifier] = None + + @dispatch + def define(self, x: ASTFuncdefNode): + return self.define(x, redefine=True) + + @dispatch + def define(self, x, redefine=False): + if (redefine): + try: del self.values[x.name] + except KeyError: pass + elif (x.name.identifier in self): raise SlValidationRedefinedError(x.name, self.signatures[x.name.identifier], scope=self.scope) + self.signatures[x.name.identifier] = Signature.build(x, self) + +from . import stdlib +from .stdlib import builtin_names + +def validate_ast(ast, ns=None): + Namespace.derive.clear_cache() + return ast.code.validate(ns) + +class SlValidationException(Exception): pass + +class SlValidationError(SlValidationException): + __slots__ = ('desc', 'node', 'line', 'scope') + + def __init__(self, desc, node, line='', *, scope=None): + self.desc, self.node, self.line, self.scope = desc, node, line, scope + + def __str__(self): + l, line = lstripcount(self.line.partition('\n')[0].replace('\t', ' '), ' \t') + offset = (self.node.offset-l) if (self.node.offset != -1) else len(line) + return (f'\033[2m(in {self.scope})\033[0m ' if (self.scope is not None) else '')+f"Validation error: {self.desc}{self.at}"+(':\n'+\ + ' \033[1m'+line[:offset]+'\033[91m'+line[offset:]+'\033[0m\n'+\ + ' '+' '*offset+'\033[95m^'+'~'*(self.node.length-1) if (line) else '') + + @property + def at(self): + return f" at line {self.node.lineno}, offset {self.node.offset}" + + @property + def lineno(self): + return self.node.lineno + +class SlValidationNotDefinedError(SlValidationError): + def __init__(self, identifier, **kwargs): + super().__init__(f"'{identifier.identifier}' is not defined", identifier, **kwargs) + +class SlValidationRedefinedError(SlValidationError): + def __init__(self, identifier, definition, **kwargs): + super().__init__(f"'{identifier}' redefined (defined as '{definition}')", identifier, **kwargs)# at lineno {definition.lineno} + +def optimize_ast(ast, ns): return ast.code.optimize(ns) + +# by Sdore, 2019 diff --git a/compilers/__init__.py b/compilers/__init__.py new file mode 100644 index 0000000..06ad96b --- /dev/null +++ b/compilers/__init__.py @@ -0,0 +1,38 @@ +#!/usr/bin/python3 +# Slang compilers + +import abc + +class Compiler(abc.ABC): + @abc.abstractclassmethod + def compile_ast(cls, ast): + pass + +def lstripcount(s, chars): # TODO: commonize + for ii, i in enumerate(s): + if (i not in chars): break + else: ii = 0 + return (ii, s[ii:]) + +class SlCompilationError(Exception): + __slots__ = ('desc', 'node', 'line', 'scope') + + def __init__(self, desc, node, line='', *, scope=None): + self.desc, self.node, self.line, self.scope = desc, node, line, scope + + def __str__(self): + l, line = lstripcount(self.line.partition('\n')[0].replace('\t', ' '), ' \t') + offset = (self.node.offset-l) if (self.node.offset != -1) else len(line) + return (f'\033[2m(in {self.scope})\033[0m ' if (self.scope is not None) else '')+f"Compilation error: {self.desc}{self.at}"+(':\n'+\ + ' \033[1m'+line[:offset]+'\033[91m'+line[offset:]+'\033[0m\n'+\ + ' '+' '*offset+'\033[95m^'+'~'*(self.node.length-1) if (line) else '') + + @property + def at(self): + return f" at line {self.node.lineno}, offset {self.node.offset}" + + @property + def lineno(self): + return self.node.lineno + +# by Sdore, 2019 diff --git a/compilers/pyssembly.py b/compilers/pyssembly.py new file mode 100644 index 0000000..be6bf74 --- /dev/null +++ b/compilers/pyssembly.py @@ -0,0 +1,278 @@ +#!/usr/bin/python3 +# Slang Pyssembly compiler target + +import pyssembly +from . import * +from Slang.ast import * +from utils import * + +class Instrs: + unopmap = { + '+': 'POS', + '-': 'NEG', + '!': 'NOT', + '~': 'INV', + 'not': 'NOT', + } + + binopmap = { + '+': 'ADD', + '-': 'SUB', + '*': 'MUL', + '/': 'DIV', + '//': 'FLOORDIV', + '%': 'MOD', + '**': 'POW', + '<<': 'LSHIFT', + '>>': 'RSHIFT', + '&': 'AND', + '|': 'OR', + '^': 'XOR', + 'and': 'AND', + 'or': 'OR', + } + + @init_defaults + def __init__(self, *, name, ns, filename, argdefs=()): + self.name, self.ns, self.filename = name, ns, filename + self.instrs = list() + self.consts = list() + self.argnames = list() + for i in argdefs: + if (i.modifier is not None): raise SlCompilationError("argument modifiers are not supported yet", i.modifier) + self.argnames.append(i.name.identifier) + self.cellvars = self.argnames.copy() + self.srclnotab = list() + + def compile(self): + return pyssembly.Code('\n'.join(self.instrs), name=self.name, filename=self.filename.strip('"'), srclnotab=self.srclnotab, consts=self.consts, argnames=self.argnames) + + @dispatch + def add(self, x: ASTRootNode): + self.add(x.code) + + @dispatch + def add(self, x: ASTCodeNode): + lastln = int() + for i in x.nodes: + self.srclnotab.append(i.lineno-lastln) + lastln = i.lineno + l = len(self.instrs) + self.add(i) + self.srclnotab += [0]*(len(self.instrs)-l-1) + + @dispatch + def add(self, x: ASTValueNode): + self.add(x.value) + + @dispatch + def add(self, x: ASTVardefNode): + if (x.value is not None): + self.load(x.value) + self.store(x.name) + + @dispatch + def add(self, x: ASTAssignmentNode): + if (x.inplace_operator is not None): self.instrs.append(f"LOAD ({x.name})") + self.load(x.value) + if (x.inplace_operator is not None): self.instrs.append(f"INP_{self.binopmap[x.inplace_operator.operator]}") + self.store(x.name) + + @dispatch + def add(self, x: ASTFunccallNode): + self.load(x) + self.instrs.append("POP") + + @dispatch + def add(self, x: ASTBlockNode): + self.add(x.code) + + @dispatch + def add(self, x: ASTFuncdefNode): + code_ns = self.ns.derive(x.name.identifier) + name = f"{x.name.identifier}__{self.ns.signatures[x.name.identifier].call.index(CallArguments(args=tuple(Signature.build(i, code_ns) for i in x.argdefs)))}" + f_instrs = Instrs(name=f"{self.name}.{name}", ns=code_ns, filename=self.filename, argdefs=x.argdefs) + f_instrs.add(x.code) + #dlog(f"{x.__fsig__()} instrs:\n"+'\n'.join(f_instrs.instrs)+'\n') + self.consts.append(f_instrs.compile().to_code()) + self.instrs += [ + f"LOAD {len(self.consts)-1}", + f"LOAD ('{self.name}.{name}')", + f"MAKE_FUNCTION 0", # TODO: flags + ] + self.store(name) + + @dispatch + def add(self, x: ASTKeywordExprNode): + if (x.keyword.keyword == 'import'): + ns, _, name = x.value.identifier.partition('::') + assert ns == 'py' + self.instrs += [ + "LOAD (0)", # TODO + "LOAD (None)", # TODO + ] + self.instrs.append(f"IMPORT_NAME ({name})") + self.store(name) + elif (x.keyword.keyword == 'return'): + self.load(x.value) + self.instrs.append("RET") + else: raise NotImplementedError(x.keyword) + + @dispatch + def add(self, x: ASTConditionalNode): + self.load(x.condition) + self.instrs.append("JPOPF :else") + self.add(x.code) + self.instrs += [ + "JUMPF :end", + ":else", + ":end", + ] + + @dispatch + def add(self, x: ASTForLoopNode): + self.load(x.iterable) + #self.cellvars.append(x.name.identifier) # TODO FIXME + self.instrs += [ + "SETUP_LOOP :end", + "ITER", + ":for", + "FOR :else", + ] + self.store(x.name) + self.add(x.code) + self.instrs += [ + "JUMPA :for", + ":else", + "POP_BLOCK", + ":end", + ] + + @dispatch + def add(self, x: ASTWhileLoopNode): + self.instrs += [ + "SETUP_LOOP :end", + ":while", + ] + self.load(x.condition) + self.instrs.append("JPOPF :else") + self.add(x.code) + self.instrs += [ + "JUMPA :while", + ":else", + "POP_BLOCK", + ":end", + ] + + @dispatch + def add(self, x: ASTElseClauseNode): + assert (self.instrs[-1] == ":end") + popped = [self.instrs.pop()] + if (self.instrs[-1] == "POP_BLOCK"): popped.append(self.instrs.pop()) + self.add(x.code) + self.instrs += reversed(popped) + + @dispatch + def load(self, x: ASTLiteralNode): + self.instrs.append(f"LOAD ({x.literal})") + + @dispatch + def load(self, x: ASTIdentifierNode): + self.load(x.identifier) + + @dispatch + def load(self, x: ASTValueNode): + self.load(x.value) + + @dispatch + def load(self, x: ASTFunccallNode): + if (isinstance(x.callable, ASTValueNode) and isinstance(x.callable.value, ASTIdentifierNode) and x.callable.value.identifier in self.ns.signatures): + self.load(f"{x.callable.value.identifier}__{self.ns.signatures[x.callable.value.identifier].call.index(CallArguments.build(x, self.ns))}") + else: self.load(x.callable) + n = int() + + for i in x.callargs.callargs: + self.load(i) + n += 1 + if (x.callargs.starargs): + if (n): + self.instrs.append(f"BUILD_TUPLE {n}") + n = 1 + for i in x.callargs.starargs: + self.load(i) + n += 1 + self.instrs.append(f"BUILD_TUPLE_UNPACK_WITH_CALL {n}") + n = 0 + + for i in x.callkwargs.callkwargs: + self.load(f"'{i[0]}'") + self.load(i[1]) + n += 1 + if (n and (x.callargs.starargs or x.callkwargs.starkwargs)): + self.instrs.append(f"BUILD_MAP {n}") + n = 1 + if (x.callkwargs.starkwargs): + for i in x.callkwargs.starkwargs: + self.load(i) + n += 1 + self.instrs.append(f"BUILD_MAP_UNPACK_WITH_CALL {n}") + n = 1 + + self.instrs.append(f"CALL{'EX' if (x.callargs.starargs or x.callkwargs.starkwargs) else 'KW' if (x.callkwargs.callkwargs) else ''} {n}") + + @dispatch + def load(self, x: ASTAttrgetNode): + self.load(x.value) + assert x.optype.special == '.' # TODO + self.instrs.append(f"GETATTR ({x.attr})") + + @dispatch + def load(self, x: ASTUnaryExprNode): + self.load(x.value) + self.instrs.append(self.unopmap[x.operator.operator]) + + @dispatch + def load(self, x: ASTBinaryExprNode): + self.load(x.lvalue) + char = isinstance(Signature.build(x.lvalue, self.ns), stdlib.char) + if (char): self.instrs.append("CALL (ord) 1") + if (x.operator.operator == 'xor'): self.instrs.append("BOOL") + self.load(x.rvalue) + if (char and isinstance(Signature.build(x.rvalue, self.ns), stdlib.char)): self.instrs.append("CALL (ord) 1") + if (x.operator.operator == 'xor'): self.instrs.append("BOOL") + if (x.operator.operator == 'to'): self.instrs.append("CALL (range) 2") + else: self.instrs.append(self.binopmap[x.operator.operator]) + if (x.operator.operator == 'xor'): self.instrs.append("BOOL") + if (char and x.operator.operator not in keyword_operators): self.instrs.append("CALL (chr) 1") + + @dispatch + def load(self, x: ASTItemgetNode): + self.load(x.value) + self.load(x.key) + self.instrs.append("SUBSCR") + + @dispatch + def load(self, x: str): + self.instrs.append(f"LOAD {f'${x}' if (x in self.cellvars) else f'({x})'}") + + @dispatch + def store(self, x: ASTIdentifierNode): + self.store(x.identifier) + + @dispatch + def store(self, x: str): + self.instrs.append(f"STORE {f'${x}' if (x in self.cellvars) else f'({x})'}") + +class PyssemblyCompiler(Compiler): + @classmethod + def compile_ast(cls, ast, ns, *, filename): + instrs = Instrs(name='', ns=ns, filename=filename) + instrs.add(ast) + #dlog("Instrs:\n"+'\n'.join(instrs.instrs)+'\n') + code = instrs.compile().to_code() + #dis.show_code(code) + #dis.dis(code) + #print() + return code + +# by Sdore, 2019 diff --git a/compilers/sbc.py b/compilers/sbc.py new file mode 100644 index 0000000..ec2839b --- /dev/null +++ b/compilers/sbc.py @@ -0,0 +1,288 @@ +#!/usr/bin/python3 +# Slang Bytecode (SBC) compiler target + +from . import * +from Slang.ast import * +from utils import * + +NOP = 0x00 +RET = 0x01 + +POS = 0x10 +NEG = 0x11 +NOT = 0x12 +INV = 0x13 +ATOI = 0x14 +ITOA = 0x15 +ITOF = 0x16 +CEIL = 0x17 +FLR = 0x18 +RND = 0x19 +CTOS = 0x1a + +ADD = 0x20 +SUB = 0x21 +MUL = 0x22 +DIV = 0x23 +FLRDIV = 0x24 +MOD = 0x25 +POW = 0x26 +SHL = 0x27 +SHR = 0x28 +AND = 0x29 +OR = 0x2a +XOR = 0x2b + +ALLOC = 0xa0 +EXTEND = 0xa1 +CONST = 0xa2 +JUMPF = 0xa3 +JUMPB = 0xa4 +SCPGET = 0xa5 +SCPSET = 0xa6 + +HASARG = 0xa0 + +class Instrs: + unops = '+-!~' + binops = (*'+-*/%', '**', '<<', '>>', '&', '|', '^') + + @init_defaults + def __init__(self, *, name, ns, filename, argdefs=()): + self.ns = ns + self.instrs = bytearray() + + def compile(self): + raise TODO + + @dispatch + def add(self, opcode: int, oparg: int = None): + self.instrs.append(opcode) + if (opcode >= HASARG): self.instrs.append(oparg) + else: assert oparg is None + + @dispatch + def add(self, x: ASTRootNode): + self.add(x.code) + + @dispatch + def add(self, x: ASTCodeNode): + lastln = int() + for i in x.nodes: + self.add(i) + + @dispatch + def add(self, x: ASTValueNode): + self.add(x.value) + + @dispatch + def add(self, x: ASTVardefNode): + if (x.value is not None): + self.load(x.value) + self.store(x.name) + + @dispatch + def add(self, x: ASTAssignmentNode): + if (x.inplace_operator is not None): self.instrs.append(f"LOAD ({x.name})") + self.load(x.value) + if (x.inplace_operator is not None): self.instrs.append(f"INP_{self.binopmap[x.inplace_operator.operator]}") + self.store(x.name) + + @dispatch + def add(self, x: ASTFunccallNode): + self.load(x) + self.instrs.append("POP") + + @dispatch + def add(self, x: ASTBlockNode): + self.add(x.code) + + @dispatch + def add(self, x: ASTFuncdefNode): + code_ns = self.ns.derive(x.name.identifier) + name = f"{x.name.identifier}__{self.ns.signatures[x.name.identifier].call.index(CallArguments(args=tuple(Signature.build(i, code_ns) for i in x.argdefs)))}" + f_instrs = Instrs(name=f"{self.name}.{name}", ns=code_ns, filename=self.filename, argdefs=x.argdefs) + f_instrs.add(x.code) + #dlog(f"{x.__fsig__()} instrs:\n"+'\n'.join(f_instrs.instrs)+'\n') + self.consts.append(f_instrs.compile().to_code()) + self.instrs += [ + f"LOAD {len(self.consts)-1}", + f"LOAD ('{self.name}.{name}')", + f"MAKE_FUNCTION 0", # TODO: flags + ] + self.store(name) + + @dispatch + def add(self, x: ASTKeywordExprNode): + if (x.keyword.keyword == 'import'): + ns, _, name = x.value.identifier.partition('::') + assert ns == 'py' + self.instrs += [ + "LOAD (0)", # TODO + "LOAD (None)", # TODO + ] + self.instrs.append(f"IMPORT_NAME ({name})") + self.store(name) + elif (x.keyword.keyword == 'return'): + self.load(x.value) + self.instrs.append("RET") + else: raise NotImplementedError(x.keyword) + + @dispatch + def add(self, x: ASTConditionalNode): + self.load(x.condition) + self.instrs.append("JPOPF :else") + self.add(x.code) + self.instrs += [ + "JUMPF :end", + ":else", + ":end", + ] + + @dispatch + def add(self, x: ASTForLoopNode): + self.load(x.iterable) + #self.cellvars.append(x.name.identifier) # TODO FIXME + self.instrs += [ + "SETUP_LOOP :end", + "ITER", + ":for", + "FOR :else", + ] + self.store(x.name) + self.add(x.code) + self.instrs += [ + "JUMPA :for", + ":else", + "POP_BLOCK", + ":end", + ] + + @dispatch + def add(self, x: ASTWhileLoopNode): + self.instrs += [ + "SETUP_LOOP :end", + ":while", + ] + self.load(x.condition) + self.instrs.append("JPOPF :else") + self.add(x.code) + self.instrs += [ + "JUMPA :while", + ":else", + "POP_BLOCK", + ":end", + ] + + @dispatch + def add(self, x: ASTElseClauseNode): + assert (self.instrs[-1] == ":end") + popped = [self.instrs.pop()] + if (self.instrs[-1] == "POP_BLOCK"): popped.append(self.instrs.pop()) + self.add(x.code) + self.instrs += reversed(popped) + + @dispatch + def load(self, x: ASTLiteralNode): + self.instrs.append(f"LOAD ({x.literal})") + + @dispatch + def load(self, x: ASTIdentifierNode): + self.load(x.identifier) + + @dispatch + def load(self, x: ASTValueNode): + self.load(x.value) + + @dispatch + def load(self, x: ASTFunccallNode): + if (isinstance(x.callable, ASTValueNode) and isinstance(x.callable.value, ASTIdentifierNode) and x.callable.value.identifier in self.ns.signatures): + self.load(f"{x.callable.value.identifier}__{self.ns.signatures[x.callable.value.identifier].call.index(CallArguments.build(x, self.ns))}") + else: self.load(x.callable) + n = int() + + for i in x.callargs.callargs: + self.load(i) + n += 1 + if (x.callargs.starargs): + if (n): + self.instrs.append(f"BUILD_TUPLE {n}") + n = 1 + for i in x.callargs.starargs: + self.load(i) + n += 1 + self.instrs.append(f"BUILD_TUPLE_UNPACK_WITH_CALL {n}") + n = 0 + + for i in x.callkwargs.callkwargs: + self.load(f"'{i[0]}'") + self.load(i[1]) + n += 1 + if (n and (x.callargs.starargs or x.callkwargs.starkwargs)): + self.instrs.append(f"BUILD_MAP {n}") + n = 1 + if (x.callkwargs.starkwargs): + for i in x.callkwargs.starkwargs: + self.load(i) + n += 1 + self.instrs.append(f"BUILD_MAP_UNPACK_WITH_CALL {n}") + n = 1 + + self.instrs.append(f"CALL{'EX' if (x.callargs.starargs or x.callkwargs.starkwargs) else 'KW' if (x.callkwargs.callkwargs) else ''} {n}") + + @dispatch + def load(self, x: ASTAttrgetNode): + self.load(x.value) + assert x.optype.special == '.' # TODO + self.instrs.append(f"GETATTR ({x.attr})") + + @dispatch + def load(self, x: ASTUnaryExprNode): + self.load(x.value) + self.instrs.append(self.unopmap[x.operator.operator]) + + @dispatch + def load(self, x: ASTBinaryExprNode): + self.load(x.lvalue) + char = isinstance(Signature.build(x.lvalue, self.ns), stdlib.char) + if (char): self.instrs.append("CALL (ord) 1") + if (x.operator.operator == 'xor'): self.instrs.append("BOOL") + self.load(x.rvalue) + if (char and isinstance(Signature.build(x.rvalue, self.ns), stdlib.char)): self.instrs.append("CALL (ord) 1") + if (x.operator.operator == 'xor'): self.instrs.append("BOOL") + if (x.operator.operator == 'to'): self.instrs.append("CALL (range) 2") + else: self.instrs.append(self.binopmap[x.operator.operator]) + if (x.operator.operator == 'xor'): self.instrs.append("BOOL") + if (char and x.operator.operator not in keyword_operators): self.instrs.append("CALL (chr) 1") + + @dispatch + def load(self, x: ASTItemgetNode): + self.load(x.value) + self.load(x.key) + self.instrs.append("SUBSCR") + + @dispatch + def load(self, x: str): + self.instrs.append(f"LOAD {f'${x}' if (x in self.cellvars) else f'({x})'}") + + @dispatch + def store(self, x: ASTIdentifierNode): + self.store(x.identifier) + + @dispatch + def store(self, x: str): + self.instrs.append(f"STORE {f'${x}' if (x in self.cellvars) else f'({x})'}") + +class PyssemblyCompiler(Compiler): + @classmethod + def compile_ast(cls, ast, ns, *, filename): + instrs = Instrs(name='', ns=ns, filename=filename) + instrs.add(ast) + #dlog("Instrs:\n"+'\n'.join(instrs.instrs)+'\n') + code = instrs.compile().to_code() + #dis.show_code(code) + #dis.dis(code) + #print() + return code + +# by Sdore, 2019 diff --git a/lexer.py b/lexer.py new file mode 100644 index 0000000..55709f7 --- /dev/null +++ b/lexer.py @@ -0,0 +1,42 @@ +#!/usr/bin/python3 +# Slang lexer + +from .tokens import * + +def read_token(src, *, lineno, offset, lineoff): + (l, src), line = lstripcount(src[offset:], whitespace), src + offset += l + if (src[:1] in '\n;'): return (offset, None) + length = int() + for ii, i in enumerate(Token.types): + r = globals()['find_'+i.casefold()](src) or 0 + if (isinstance(r, int) and r <= 0): length = max(length, -r); continue + n, s = r if (isinstance(r, tuple)) else (r, src[:r]) + return (offset+n, Token(ii, s, lineno=lineno, offset=offset+lineoff)) + else: raise SlSyntaxError("Invalid token", line, lineno=lineno, offset=offset+lineoff, length=length) + +def parse_expr(src, *, lineno=1, lineoff=0): + r = list() + offset = int() + while (True): + offset, tok = read_token(src, lineno=lineno, offset=offset, lineoff=lineoff) + if (tok is None): break + r.append(tok) + return offset, r + +def parse_string(src): + src = src.rstrip() + tl = list() + lines = src.count('\n') + lineoff = int() + while (src): + offset, r = parse_expr(src, lineno=lines-src.count('\n')+1, lineoff=lineoff) + lineoff += offset + if (offset < len(src)): + if (src[offset] == '\n'): lineoff = int() + else: lineoff += 1 + src = src[offset+1:] + tl.append(r) + return tl + +# by Sdore, 2019 diff --git a/repl.py b/repl.py new file mode 100644 index 0000000..1965c54 --- /dev/null +++ b/repl.py @@ -0,0 +1,89 @@ +#!/usr/bin/python3 +# Slang REPL + +import readline +from .ast import * +from .lexer import * +from utils import * + +@dispatch +def execute_node(node: ASTCodeNode, ns): + for i in node.nodes: + r = execute_node(i, ns) + if (r is not None): + ns.values['_'] = r + print(repr(r)) + else: return + return r + +@dispatch +def execute_node(node: ASTVardefNode, ns): + ns.values[node.name] = node.value + +@dispatch +def execute_node(node: ASTFuncdefNode, ns): + ns.values[node.name.identifier] = (node.argdefs, node.code) + +@dispatch +def execute_node(node: ASTAssignmentNode, ns): + ns.values[node.name] = node.value + +@dispatch +def execute_node(node: ASTFunccallNode, ns): + code_ns = ns.derive(node.callable.value.identifier) + argdefs, func = ns.values[node.callable] + for ii, i in enumerate(node.callargs.callargs): + code_ns.values[argdefs[ii].name] = i + return execute_node(func, code_ns) + +@dispatch +def execute_node(node: ASTValueNode, ns): + return execute_node(node.value, ns) + +@dispatch +def execute_node(node: ASTIdentifierNode, ns): + return ns.values[node] + +@dispatch +def execute_node(node: ASTLiteralNode, ns): + return eval(str(node.literal)) + +@dispatch +def execute_node(node: ASTKeywordExprNode, ns): + if (node.keyword.keyword == 'return'): return execute_node(node.value, ns) + raise NotImplementedError(node.keyword) + +@dispatch +def execute_node(node: ASTUnaryExprNode, ns): + return eval(f"{node.operator.operator} {execute_node(node.value, ns)}") + +@dispatch +def execute_node(node: ASTBinaryExprNode, ns): + return eval(f"{execute_node(node.lvalue, ns)} {node.operator.operator} {execute_node(node.rvalue, ns)}") + +def repl(): + ns = Namespace('') + #ns.values['print'] = print + l = list() + tl = list() + while (True): + try: + l.append(input(f"\1\033[1;93m\2{'...' if (tl) else '>>>'}\1\033[0m\2 ")) + tll = parse_string(l[-1]) + if (not tll): continue + tl += tll + if (tl[-1][-1].token == '\\'): continue + if (len(tl) >= 2 and tl[-2][-1].token == '\\'): tl[-1] = tl[-2][:-1]+tl.pop() + if (tl[0][-1].token == '{' and tl[-1][-1].token != '}'): continue + ast = build_ast(tl, interactive=True) + validate_ast(ast, ns) + optimize_ast(ast, ns) + execute_node(ast.code, ns) + except (EOFError, KeyboardInterrupt): break + except (SlSyntaxError, SlValidationError) as ex: + ex.line = l[ex.lineno-1] + print(ex) + tl.clear() + l.clear() + +# by Sdore, 2019 diff --git a/stdlib.py b/stdlib.py new file mode 100644 index 0000000..b004d77 --- /dev/null +++ b/stdlib.py @@ -0,0 +1,110 @@ +#!/usr/bin/python3 +# Slang stdlib + +from .ast import Signature, Function, Object +from .tokens import * +from utils import * + +class Builtin(Signature): + def __init__(self): + pass + + @property + def __reprname__(self): + return type(self).mro()[1].__name__ + + @property + def typename(self): + return type(self).__name__ + +class BuiltinFunction(Builtin, Function): pass + +class BuiltinObject(Builtin, Object): pass + +class BuiltinType(Builtin): + @init_defaults + @autocast + def __init__(self, *, modifiers: paramset): + self.modifiers = modifiers + +class void(BuiltinType): pass + +class bool(BuiltinType): + @staticitemget + @instantiate + def operators(op, valsig=None): + if (valsig is None): + if (op in map(UnaryOperator, '+-~')): return int + if (op in map(UnaryOperator, ('not', '!'))): return bool + raise KeyError() + +class int(BuiltinType): + @staticitemget + @instantiate + def operators(op, valsig=None): + if (valsig is None): + if (op in map(UnaryOperator, '+-~')): return int + if (op in map(UnaryOperator, ('not', '!'))): return bool + if (not isinstance(valsig, (int, float))): raise KeyError() + if (op in map(BinaryOperator, ('**', *'+-*'))): return valsig + if (op in map(BinaryOperator, ('//', '<<', '>>', *'&^|'))): return int + if (op == BinaryOperator('/')): return float + if (op == BinaryOperator('to')): return int + raise KeyError() + +class float(BuiltinType): + @staticitemget + @instantiate + def operators(op, valsig=None): + if (valsig is None): + if (op in map(UnaryOperator, ('not', *'!+-'))): return float + if (not isinstance(valsig, (int, float))): raise KeyError() + if (op in map(BinaryOperator, ('**', *'+-*'))): return float + if (op == BinaryOperator('/')): return float + if (op == BinaryOperator('//')): return int + raise KeyError() + +class str(BuiltinType): + @staticitemget + @instantiate + def operators(op, valsig=None): + if (valsig is None): raise KeyError() + if (isinstance(valsig, str) and op == BinaryOperator('+')): return str + if (isinstance(valsig, int) and op == BinaryOperator('*')): return str + raise KeyError() + + @staticitemget + @instantiate + def itemget(keysig): + if (isinstance(keysig, int)): return char + raise KeyError() + +class char(BuiltinType): + @staticitemget + @instantiate + def operators(op, valsig=None): + if (valsig is None): raise KeyError() + if (isinstance(valsig, str) and op == BinaryOperator('+')): return str + if (isinstance(valsig, int) and op == BinaryOperator('*')): return str + if (isinstance(valsig, (char, int)) and op in map(BinaryOperator, '+-')): return char + raise KeyError() + +i8 = i16 = i32 = i64 = i128 = \ +u8 = u16 = u32 = u64 = u128 = int +f8 = f16 = f32 = f64 = f128 = \ +uf8 = uf16 = uf32 = uf64 = uf128 = float +# TODO: implement these types + +class print(BuiltinFunction): + callargssigstr = "print(...)" + + @staticitemget + @instantiate + def call(callargssig): + if (callargssig.kwargs or callargssig.starkwargs): raise KeyError() + return void + +builtin_names = {j.__name__: globals()[j.__name__] for i in map(operator.methodcaller('__subclasses__'), Builtin.__subclasses__()) for j in i} +builtin_names.update({i: globals()[i] for i in (i+j for j in map(builtins.str, (8, 16, 32, 64, 128)) for i in (*'iuf', 'uf'))}) + +# by Sdore, 2019 diff --git a/tests/example.sl b/tests/example.sl new file mode 100644 index 0000000..160e2ce --- /dev/null +++ b/tests/example.sl @@ -0,0 +1,27 @@ +# this is a comment +#| and that is a + multiline one. |# + +const u32 n = 123123 # n is of type const u32 +const i64 m = 10**18 # m is of type const i64 +const int z = 2**128 # z is of type const int (unsized) +const auto q = 2**256 # q is of type const int + +char f(str x) { # f() is of type char, x is of type str + auto c = x[1] # c is of type char + return c +} + +auto g(str x) { # g() is of type char, x is of type str + return x[0] # retval is of type char +} + +int h(int x) = x+1 # h() is of type int, x is of type int + +void main() { + print(h(n), \ + f('123asd') + g('32') + 1) #--> «123124 f» + print(q/z/2**96) #--> «4294967296.0» +} + +main() \ No newline at end of file diff --git a/tests/funcdef.sl b/tests/funcdef.sl new file mode 100644 index 0000000..471b607 --- /dev/null +++ b/tests/funcdef.sl @@ -0,0 +1,2 @@ +void f() {} +void f(int a?) {} \ No newline at end of file diff --git a/tests/opti.sl b/tests/opti.sl new file mode 100644 index 0000000..548ca1b --- /dev/null +++ b/tests/opti.sl @@ -0,0 +1,2 @@ +int a = 3 +print(2**a) \ No newline at end of file diff --git a/tests/overload.sl b/tests/overload.sl new file mode 100644 index 0000000..1a4c11f --- /dev/null +++ b/tests/overload.sl @@ -0,0 +1,5 @@ +int f(int x) = x+1 +int f(int x, int y) = x+y+1 + +print(f(1)) +print(f(1, 2)) \ No newline at end of file diff --git a/tests/sum.sl b/tests/sum.sl new file mode 100644 index 0000000..12d86e9 --- /dev/null +++ b/tests/sum.sl @@ -0,0 +1,3 @@ +int a = 3 +int b = 5 +print(a+b) \ No newline at end of file diff --git a/tests/test.sl b/tests/test.sl new file mode 100644 index 0000000..28e1fb7 --- /dev/null +++ b/tests/test.sl @@ -0,0 +1,30 @@ +const int a = 3 +int b = 2; int c = 0; int x = 9 + +int sum(int a, int z) = a + z +print(sum(a, b & 3)) +print(1) +print(1, 2) +print('a') +print(2*(3)+-2*(5+1)/2) +print(-2*x**(2+2)*a*b+10*c) +print(*'a'+'b'*2) +print(-2-2-2-2-2-2-2-2) + +void main() { + int z = sum(3, 2) + b = 2**100 + b *= 2 + print(not a) + print(a, b, c) + int test() = 2*2 + print(sum(b, test())) + + for i in (0 to 5) print(i) + else print(0) + + int i = 5 + while (i) {print(i); i -= 1;} else print('ВСЁ!') +} + +main() \ No newline at end of file diff --git a/tests/underscore.sl b/tests/underscore.sl new file mode 100644 index 0000000..7cde44f --- /dev/null +++ b/tests/underscore.sl @@ -0,0 +1 @@ +int _a_b_ \ No newline at end of file diff --git a/tokens.py b/tokens.py new file mode 100644 index 0000000..feb7334 --- /dev/null +++ b/tokens.py @@ -0,0 +1,206 @@ +#!/usr/bin/python3 +# Slang Tokens + +from utils import * + +class Keyword(str): pass +class ExprKeyword(Keyword): pass +class Modifier(Keyword): pass +class ReservedKeyword(Keyword): pass + +class Operator(str): pass +class UnaryOperator(Operator): pass +class BinaryOperator(Operator): pass + +keywords = ( + Keyword('if'), + Keyword('for'), + Keyword('in'), + Keyword('while'), + Keyword('else'), + ExprKeyword('return'), + ExprKeyword('break'), + ExprKeyword('continue'), + ExprKeyword('import'), + Modifier('const'), + Modifier('volatile'), + ReservedKeyword('def'), +) + +operators = (*(tuple(map(*i)) for i in ( # ordered by priority + (UnaryOperator, '!$:~'), + (BinaryOperator, ('**',)), + (BinaryOperator, ('//', *'*/%')), + (BinaryOperator, '+-'), + (BinaryOperator, ('<<', '>>')), + (BinaryOperator, '&'), + (BinaryOperator, '^'), + (BinaryOperator, '|'), + (BinaryOperator, ('<', '<=', '>', '>=', '==', '!=', 'is not', 'is')), + (UnaryOperator, ('not',)), + (BinaryOperator, ('&&', 'and')), + (BinaryOperator, ('^^', 'xor')), + (BinaryOperator, ('||', 'or')), + (BinaryOperator, ('to',)), +)),) +bothoperators = '%&*+-^' +attrops = ('->', '@.', *'@.:') +keyword_operators = ('is not', 'is', 'not', 'and', 'xor', 'or', 'to') + +whitespace = ' \t\r\v\f' +specials = ('..', '->', '@.', *'#@.\\,;?=()[]{}') + +def find_identifier(s): + if (not s or not s[0].isidentifier()): return + i = 1 + for i in range(1, len(s)): + if (not s[i].isalnum() and s[i] != '_'): break + else: i += 1 + if (s[:i].isidentifier()): return i + +def find_keyword(s): + if (not s): return + for i in keywords: + if (s.startswith(i)): + l = len(i) + if (not s[l:l+1] or s[l:l+1].isspace()): return (l, i) + +def find_literal(s): + if (not s): return + if (s[0] in '"\''): + esc = bool() + for i in range(1, len(s)): + if (esc): esc = False; continue + if (s[i] == '\\'): esc = True; continue + if (s[i] == s[0]): return i+1 + if (s[0].isdigit() or s[0] == '.'): + i = int() + digits = '0123456789abcdef' + radix = 10 + digit = True + dp = s[0] == '.' + for i in range(1, len(s)): + if (i == 1 and s[0] == '0'): + if (s[1] not in 'box'): + if (s[1].isalnum()): return + return 1 + else: + radix = (2, 8, 16)['box'.index(s[1])] + digit = False + continue + if (s[i].casefold() not in digits[:radix]): + if (s[i] == '_'): continue + if (s[i] == '.' and not dp): dp = True; continue + if (not digit or s[i].isalpha()): return + return i + digit = True + if (s[i].casefold() in digits[:radix] or s[i] == '.' and not dp): return i+1 + +def find_operator(s): + if (not s): return + for i in sorted(itertools.chain(*operators), key=len, reverse=True): + if (s.startswith(i)): + l = len(i) + if (not (i[-1].isalpha() and s[l:l+1].isalnum())): return (len(i), i) + +def find_special(s): + if (not s): return + if (s[0] == '.' and s[1:2].isdigit()): return + if (s[:2] == '#|'): + l = s.find('|#', 2) + if (l == -1): return -2 + return l+2 + if (s[0] == '#'): + l = s.find('\n', 1) + if (l == -1): return len(s) + return l + if (s[:2] == '\\\n'): return 2 + for i in sorted(specials, key=len, reverse=True): + if (s[:len(i)] == i): + if (i == '=' and s[:len(i)+1] == '=='): break + return len(i) + +def operator_precedence(op): + for ii, i in enumerate(operators): + if (op in i): return ii + #else: return len(operators) + +class Token: + __slots__ = ('type', 'token', 'lineno', 'offset') + types = ('SPECIAL', 'OPERATOR', 'LITERAL', 'KEYWORD', 'IDENTIFIER') # order is also resolution order + + def __init__(self, type, token, *, lineno, offset): + self.type, self.token, self.lineno, self.offset = type, token, lineno, offset + + def __repr__(self): + return f"" + + def __eq__(self, x): + return super() == x or self.token == x + + @property + def typename(self): + return self.types[self.type] + + @property + def length(self): + return len(self.token) + +def lstripcount(s, chars): + for ii, i in enumerate(s): + if (i not in chars): break + else: ii = 0 + return (ii, s[ii:]) + +class SlSyntaxException(Exception): pass +class SlSyntaxNoToken(SlSyntaxException): pass +class SlSyntaxEmpty(SlSyntaxNoToken): pass + +class SlSyntaxError(SlSyntaxException): + __slots__ = ('desc', 'line', 'lineno', 'offset', 'length') + + #@dispatch + def __init__(self, desc='Syntax error', line='', *, lineno, offset, length, scope=None): + self.desc, self.line, self.lineno, self.offset, self.length = (f'\033[2m(in {scope})\033[0m ' if (scope is not None) else '')+desc, line, lineno, offset, length + + #@dispatch + #def __init__(self, desc='Syntax error', *, token): + # self.desc, self.line, self.lineno, self.offset, self.length = desc, '', token.lineno, token.offset, token.length + + def __str__(self): + l, line = lstripcount(self.line.partition('\n')[0].replace('\t', ' '), ' \t') + offset = (self.offset-l) if (self.offset != -1) else len(line) + return f"{self.desc}{self.at}"+(':\n'+\ + ' \033[1m'+line[:offset]+'\033[91m'+line[offset:]+'\033[0m\n'+\ + ' '+' '*offset+'\033[95m^'+'~'*(self.length-1) if (line) else '') + + @property + def at(self): + return f" at line {self.lineno}, offset {self.offset}" + +class SlSyntaxExpectedError(SlSyntaxError): + __slots__ = ('expected', 'found') + + def __init__(self, expected='nothing', found='nothing', *, lineno=None, offset=None, length=0, scope=None): + assert expected != found + if (not isinstance(found, str)): lineno, offset, length, found = found.lineno, found.offset, found.length, found.typename if (hasattr(found, 'typename')) else found + assert lineno is not None and offset is not None + super().__init__(f"Expected {expected.lower()},\n{' '*(len(scope)+6 if (scope is not None) else 3)}found {found.lower()}", lineno=lineno, offset=offset, length=length, scope=scope) + self.expected, self.found = expected, found + +class SlSyntaxExpectedNothingError(SlSyntaxExpectedError): + def __init__(self, found='nothing', **kwargs): + super().__init__(found=found, **kwargs) + +class SlSyntaxMultiExpectedError(SlSyntaxExpectedError): + __slots__ = ('sl',) + + def __init__(self, expected, found, *, scope=None, **kwargs): + self.sl = len(scope)+6 if (scope is not None) else 0 + super().__init__(S(',\n'+' '*(self.sl+9)).join(Stuple(expected).strip('nothing').uniquize(), last=',\n'+' '*(self.sl+6)+'or ') or 'nothing', S(',\n'+' '*(self.sl+6)).join(Stuple(f"{i.found} at offset {i.offset if (i.offset != -1) else ''}" for i in found).strip('nothing').uniquize(), last=',\n'+' '*(self.sl+2)+'and ') or 'nothing', scope=scope, **kwargs) + + @property + def at(self): + return f"\n{' '*self.sl}at line {self.lineno}" + +# by Sdore, 2019