604 lines
22 KiB
Python
604 lines
22 KiB
Python
"""
|
|
Parses the source code from the plain text into a syntax tree
|
|
"""
|
|
from typing import Any, Dict, NoReturn, Union
|
|
|
|
import ast
|
|
|
|
from .typing import (
|
|
TypeBase,
|
|
TypeUInt8,
|
|
TypeUInt32,
|
|
TypeUInt64,
|
|
TypeInt32,
|
|
TypeInt64,
|
|
TypeFloat32,
|
|
TypeFloat64,
|
|
TypeBytes,
|
|
TypeStruct,
|
|
TypeStructMember,
|
|
TypeTuple,
|
|
TypeTupleMember,
|
|
)
|
|
|
|
from . import codestyle
|
|
from .exceptions import StaticError
|
|
from .ourlang import (
|
|
WEBASSEMBLY_BUILDIN_FLOAT_OPS,
|
|
|
|
Module,
|
|
Function,
|
|
|
|
Expression,
|
|
AccessBytesIndex, AccessStructMember, AccessTupleMember,
|
|
BinaryOp,
|
|
ConstantFloat32, ConstantFloat64, ConstantInt32, ConstantInt64,
|
|
ConstantUInt8, ConstantUInt32, ConstantUInt64,
|
|
FunctionCall,
|
|
StructConstructor, TupleConstructor,
|
|
UnaryOp, VariableReference,
|
|
|
|
Statement,
|
|
StatementIf, StatementPass, StatementReturn,
|
|
)
|
|
|
|
def phasm_parse(source: str) -> Module:
|
|
"""
|
|
Public method for parsing Phasm code into a Phasm Module
|
|
"""
|
|
res = ast.parse(source, '')
|
|
|
|
our_visitor = OurVisitor()
|
|
return our_visitor.visit_Module(res)
|
|
|
|
OurLocals = Dict[str, TypeBase]
|
|
|
|
class OurVisitor:
|
|
"""
|
|
Class to visit a Python syntax tree and create an ourlang syntax tree
|
|
|
|
We're (ab)using the Python AST parser to give us a leg up
|
|
|
|
At some point, we may deviate from Python syntax. If nothing else,
|
|
we probably won't keep up with the Python syntax changes.
|
|
"""
|
|
|
|
# pylint: disable=C0103,C0116,C0301,R0201,R0912
|
|
|
|
def __init__(self) -> None:
|
|
pass
|
|
|
|
def visit_Module(self, node: ast.Module) -> Module:
|
|
module = Module()
|
|
|
|
_not_implemented(not node.type_ignores, 'Module.type_ignores')
|
|
|
|
# Second pass for the types
|
|
|
|
for stmt in node.body:
|
|
res = self.pre_visit_Module_stmt(module, stmt)
|
|
|
|
if isinstance(res, Function):
|
|
if res.name in module.functions:
|
|
raise StaticError(
|
|
f'{res.name} already defined on line {module.functions[res.name].lineno}'
|
|
)
|
|
|
|
module.functions[res.name] = res
|
|
|
|
if isinstance(res, TypeStruct):
|
|
if res.name in module.structs:
|
|
raise StaticError(
|
|
f'{res.name} already defined on line {module.structs[res.name].lineno}'
|
|
)
|
|
|
|
module.structs[res.name] = res
|
|
constructor = StructConstructor(res)
|
|
module.functions[constructor.name] = constructor
|
|
|
|
# Second pass for the function bodies
|
|
|
|
for stmt in node.body:
|
|
self.visit_Module_stmt(module, stmt)
|
|
|
|
return module
|
|
|
|
def pre_visit_Module_stmt(self, module: Module, node: ast.stmt) -> Union[Function, TypeStruct]:
|
|
if isinstance(node, ast.FunctionDef):
|
|
return self.pre_visit_Module_FunctionDef(module, node)
|
|
|
|
if isinstance(node, ast.ClassDef):
|
|
return self.pre_visit_Module_ClassDef(module, node)
|
|
|
|
raise NotImplementedError(f'{node} on Module')
|
|
|
|
def pre_visit_Module_FunctionDef(self, module: Module, node: ast.FunctionDef) -> Function:
|
|
function = Function(node.name, node.lineno)
|
|
|
|
_not_implemented(not node.args.posonlyargs, 'FunctionDef.args.posonlyargs')
|
|
|
|
for arg in node.args.args:
|
|
if not arg.annotation:
|
|
_raise_static_error(node, 'Type is required')
|
|
|
|
function.posonlyargs.append((
|
|
arg.arg,
|
|
self.visit_type(module, arg.annotation),
|
|
))
|
|
|
|
_not_implemented(not node.args.vararg, 'FunctionDef.args.vararg')
|
|
_not_implemented(not node.args.kwonlyargs, 'FunctionDef.args.kwonlyargs')
|
|
_not_implemented(not node.args.kw_defaults, 'FunctionDef.args.kw_defaults')
|
|
_not_implemented(not node.args.kwarg, 'FunctionDef.args.kwarg')
|
|
_not_implemented(not node.args.defaults, 'FunctionDef.args.defaults')
|
|
|
|
# Do stmts at the end so we have the return value
|
|
|
|
for decorator in node.decorator_list:
|
|
if not isinstance(decorator, ast.Name):
|
|
_raise_static_error(decorator, 'Function decorators must be string')
|
|
if not isinstance(decorator.ctx, ast.Load):
|
|
_raise_static_error(decorator, 'Must be load context')
|
|
_not_implemented(decorator.id in ('exported', 'imported'), 'Custom decorators')
|
|
|
|
if decorator.id == 'exported':
|
|
function.exported = True
|
|
else:
|
|
function.imported = True
|
|
|
|
if node.returns:
|
|
function.returns = self.visit_type(module, node.returns)
|
|
|
|
_not_implemented(not node.type_comment, 'FunctionDef.type_comment')
|
|
|
|
return function
|
|
|
|
def pre_visit_Module_ClassDef(self, module: Module, node: ast.ClassDef) -> TypeStruct:
|
|
struct = TypeStruct(node.name, node.lineno)
|
|
|
|
_not_implemented(not node.bases, 'ClassDef.bases')
|
|
_not_implemented(not node.keywords, 'ClassDef.keywords')
|
|
_not_implemented(not node.decorator_list, 'ClassDef.decorator_list')
|
|
|
|
offset = 0
|
|
|
|
for stmt in node.body:
|
|
if not isinstance(stmt, ast.AnnAssign):
|
|
raise NotImplementedError(f'Class with {stmt} nodes')
|
|
|
|
if not isinstance(stmt.target, ast.Name):
|
|
raise NotImplementedError('Class with default values')
|
|
|
|
if not stmt.value is None:
|
|
raise NotImplementedError('Class with default values')
|
|
|
|
if stmt.simple != 1:
|
|
raise NotImplementedError('Class with non-simple arguments')
|
|
|
|
member = TypeStructMember(stmt.target.id, self.visit_type(module, stmt.annotation), offset)
|
|
|
|
struct.members.append(member)
|
|
offset += member.type.alloc_size()
|
|
|
|
return struct
|
|
|
|
def visit_Module_stmt(self, module: Module, node: ast.stmt) -> None:
|
|
if isinstance(node, ast.FunctionDef):
|
|
self.visit_Module_FunctionDef(module, node)
|
|
return
|
|
|
|
if isinstance(node, ast.ClassDef):
|
|
return
|
|
|
|
raise NotImplementedError(f'{node} on Module')
|
|
|
|
def visit_Module_FunctionDef(self, module: Module, node: ast.FunctionDef) -> None:
|
|
function = module.functions[node.name]
|
|
|
|
our_locals = dict(function.posonlyargs)
|
|
|
|
for stmt in node.body:
|
|
function.statements.append(
|
|
self.visit_Module_FunctionDef_stmt(module, function, our_locals, stmt)
|
|
)
|
|
|
|
def visit_Module_FunctionDef_stmt(self, module: Module, function: Function, our_locals: OurLocals, node: ast.stmt) -> Statement:
|
|
if isinstance(node, ast.Return):
|
|
if node.value is None:
|
|
# TODO: Implement methods without return values
|
|
_raise_static_error(node, 'Return must have an argument')
|
|
|
|
return StatementReturn(
|
|
self.visit_Module_FunctionDef_expr(module, function, our_locals, function.returns, node.value)
|
|
)
|
|
|
|
if isinstance(node, ast.If):
|
|
result = StatementIf(
|
|
self.visit_Module_FunctionDef_expr(module, function, our_locals, function.returns, node.test)
|
|
)
|
|
|
|
for stmt in node.body:
|
|
result.statements.append(
|
|
self.visit_Module_FunctionDef_stmt(module, function, our_locals, stmt)
|
|
)
|
|
|
|
for stmt in node.orelse:
|
|
result.else_statements.append(
|
|
self.visit_Module_FunctionDef_stmt(module, function, our_locals, stmt)
|
|
)
|
|
|
|
return result
|
|
|
|
if isinstance(node, ast.Pass):
|
|
return StatementPass()
|
|
|
|
raise NotImplementedError(f'{node} as stmt in FunctionDef')
|
|
|
|
def visit_Module_FunctionDef_expr(self, module: Module, function: Function, our_locals: OurLocals, exp_type: TypeBase, node: ast.expr) -> Expression:
|
|
if isinstance(node, ast.BinOp):
|
|
if isinstance(node.op, ast.Add):
|
|
operator = '+'
|
|
elif isinstance(node.op, ast.Sub):
|
|
operator = '-'
|
|
elif isinstance(node.op, ast.Mult):
|
|
operator = '*'
|
|
elif isinstance(node.op, ast.BitXor):
|
|
operator = '^'
|
|
else:
|
|
raise NotImplementedError(f'Operator {node.op}')
|
|
|
|
# Assume the type doesn't change when descending into a binary operator
|
|
# e.g. you can do `"hello" * 3` with the code below (yet)
|
|
|
|
return BinaryOp(
|
|
exp_type,
|
|
operator,
|
|
self.visit_Module_FunctionDef_expr(module, function, our_locals, exp_type, node.left),
|
|
self.visit_Module_FunctionDef_expr(module, function, our_locals, exp_type, node.right),
|
|
)
|
|
|
|
if isinstance(node, ast.UnaryOp):
|
|
if isinstance(node.op, ast.UAdd):
|
|
operator = '+'
|
|
elif isinstance(node.op, ast.USub):
|
|
operator = '-'
|
|
else:
|
|
raise NotImplementedError(f'Operator {node.op}')
|
|
|
|
return UnaryOp(
|
|
exp_type,
|
|
operator,
|
|
self.visit_Module_FunctionDef_expr(module, function, our_locals, exp_type, node.operand),
|
|
)
|
|
|
|
if isinstance(node, ast.Compare):
|
|
if 1 < len(node.ops):
|
|
raise NotImplementedError('Multiple operators')
|
|
|
|
if isinstance(node.ops[0], ast.Gt):
|
|
operator = '>'
|
|
elif isinstance(node.ops[0], ast.Eq):
|
|
operator = '=='
|
|
elif isinstance(node.ops[0], ast.Lt):
|
|
operator = '<'
|
|
else:
|
|
raise NotImplementedError(f'Operator {node.ops}')
|
|
|
|
# Assume the type doesn't change when descending into a binary operator
|
|
# e.g. you can do `"hello" * 3` with the code below (yet)
|
|
|
|
return BinaryOp(
|
|
exp_type,
|
|
operator,
|
|
self.visit_Module_FunctionDef_expr(module, function, our_locals, exp_type, node.left),
|
|
self.visit_Module_FunctionDef_expr(module, function, our_locals, exp_type, node.comparators[0]),
|
|
)
|
|
|
|
if isinstance(node, ast.Call):
|
|
return self.visit_Module_FunctionDef_Call(module, function, our_locals, exp_type, node)
|
|
|
|
if isinstance(node, ast.Constant):
|
|
return self.visit_Module_FunctionDef_Constant(
|
|
module, function, exp_type, node,
|
|
)
|
|
|
|
if isinstance(node, ast.Attribute):
|
|
return self.visit_Module_FunctionDef_Attribute(
|
|
module, function, our_locals, exp_type, node,
|
|
)
|
|
|
|
if isinstance(node, ast.Subscript):
|
|
return self.visit_Module_FunctionDef_Subscript(
|
|
module, function, our_locals, exp_type, node,
|
|
)
|
|
|
|
if isinstance(node, ast.Name):
|
|
if not isinstance(node.ctx, ast.Load):
|
|
_raise_static_error(node, 'Must be load context')
|
|
|
|
if node.id not in our_locals:
|
|
_raise_static_error(node, 'Undefined variable')
|
|
|
|
act_type = our_locals[node.id]
|
|
if exp_type != act_type:
|
|
_raise_static_error(node, f'Expected {codestyle.type_(exp_type)}, {node.id} is actually {codestyle.type_(act_type)}')
|
|
|
|
return VariableReference(act_type, node.id)
|
|
|
|
if isinstance(node, ast.Tuple):
|
|
if not isinstance(node.ctx, ast.Load):
|
|
_raise_static_error(node, 'Must be load context')
|
|
|
|
if not isinstance(exp_type, TypeTuple):
|
|
_raise_static_error(node, f'Expression is expecting a {codestyle.type_(exp_type)}, not a tuple')
|
|
|
|
if len(exp_type.members) != len(node.elts):
|
|
_raise_static_error(node, f'Expression is expecting a tuple of size {len(exp_type.members)}, but {len(node.elts)} are given')
|
|
|
|
tuple_constructor = TupleConstructor(exp_type)
|
|
|
|
func = module.functions[tuple_constructor.name]
|
|
|
|
result = FunctionCall(func)
|
|
result.arguments = [
|
|
self.visit_Module_FunctionDef_expr(module, function, our_locals, mem.type, arg_node)
|
|
for arg_node, mem in zip(node.elts, exp_type.members)
|
|
]
|
|
return result
|
|
|
|
raise NotImplementedError(f'{node} as expr in FunctionDef')
|
|
|
|
def visit_Module_FunctionDef_Call(self, module: Module, function: Function, our_locals: OurLocals, exp_type: TypeBase, node: ast.Call) -> Union[FunctionCall, UnaryOp]:
|
|
if node.keywords:
|
|
_raise_static_error(node, 'Keyword calling not supported') # Yet?
|
|
|
|
if not isinstance(node.func, ast.Name):
|
|
raise NotImplementedError(f'Calling methods that are not a name {node.func}')
|
|
if not isinstance(node.func.ctx, ast.Load):
|
|
_raise_static_error(node, 'Must be load context')
|
|
|
|
if node.func.id in module.structs:
|
|
struct = module.structs[node.func.id]
|
|
struct_constructor = StructConstructor(struct)
|
|
|
|
func = module.functions[struct_constructor.name]
|
|
elif node.func.id in WEBASSEMBLY_BUILDIN_FLOAT_OPS:
|
|
if not isinstance(exp_type, (TypeFloat32, TypeFloat64, )):
|
|
_raise_static_error(node, f'Cannot make {node.func.id} result in {codestyle.type_(exp_type)}')
|
|
|
|
if 1 != len(node.args):
|
|
_raise_static_error(node, f'Function {node.func.id} requires 1 arguments but {len(node.args)} are given')
|
|
|
|
return UnaryOp(
|
|
exp_type,
|
|
'sqrt',
|
|
self.visit_Module_FunctionDef_expr(module, function, our_locals, exp_type, node.args[0]),
|
|
)
|
|
elif node.func.id == 'len':
|
|
if not isinstance(exp_type, TypeInt32):
|
|
_raise_static_error(node, f'Cannot make {node.func.id} result in {exp_type}')
|
|
|
|
if 1 != len(node.args):
|
|
_raise_static_error(node, f'Function {node.func.id} requires 1 arguments but {len(node.args)} are given')
|
|
|
|
return UnaryOp(
|
|
exp_type,
|
|
'len',
|
|
self.visit_Module_FunctionDef_expr(module, function, our_locals, module.types['bytes'], node.args[0]),
|
|
)
|
|
else:
|
|
if node.func.id not in module.functions:
|
|
_raise_static_error(node, 'Call to undefined function')
|
|
|
|
func = module.functions[node.func.id]
|
|
|
|
if func.returns != exp_type:
|
|
_raise_static_error(node, f'Expected {codestyle.type_(exp_type)}, {func.name} actually returns {codestyle.type_(func.returns)}')
|
|
|
|
if len(func.posonlyargs) != len(node.args):
|
|
_raise_static_error(node, f'Function {node.func.id} requires {len(func.posonlyargs)} arguments but {len(node.args)} are given')
|
|
|
|
result = FunctionCall(func)
|
|
result.arguments.extend(
|
|
self.visit_Module_FunctionDef_expr(module, function, our_locals, arg_type, arg_expr)
|
|
for arg_expr, (_, arg_type) in zip(node.args, func.posonlyargs)
|
|
)
|
|
return result
|
|
|
|
def visit_Module_FunctionDef_Attribute(self, module: Module, function: Function, our_locals: OurLocals, exp_type: TypeBase, node: ast.Attribute) -> Expression:
|
|
del module
|
|
del function
|
|
|
|
if not isinstance(node.value, ast.Name):
|
|
_raise_static_error(node, 'Must reference a name')
|
|
|
|
if not isinstance(node.ctx, ast.Load):
|
|
_raise_static_error(node, 'Must be load context')
|
|
|
|
if not node.value.id in our_locals:
|
|
_raise_static_error(node, f'Undefined variable {node.value.id}')
|
|
|
|
node_typ = our_locals[node.value.id]
|
|
if not isinstance(node_typ, TypeStruct):
|
|
_raise_static_error(node, f'Cannot take attribute of non-struct {node.value.id}')
|
|
|
|
member = node_typ.get_member(node.attr)
|
|
if member is None:
|
|
_raise_static_error(node, f'{node_typ.name} has no attribute {node.attr}')
|
|
|
|
if exp_type != member.type:
|
|
_raise_static_error(node, f'Expected {codestyle.type_(exp_type)}, {node.value.id}.{member.name} is actually {codestyle.type_(member.type)}')
|
|
|
|
return AccessStructMember(
|
|
VariableReference(node_typ, node.value.id),
|
|
member,
|
|
)
|
|
|
|
def visit_Module_FunctionDef_Subscript(self, module: Module, function: Function, our_locals: OurLocals, exp_type: TypeBase, node: ast.Subscript) -> Expression:
|
|
if not isinstance(node.value, ast.Name):
|
|
_raise_static_error(node, 'Must reference a name')
|
|
|
|
if not isinstance(node.slice, ast.Index):
|
|
_raise_static_error(node, 'Must subscript using an index')
|
|
|
|
if not isinstance(node.ctx, ast.Load):
|
|
_raise_static_error(node, 'Must be load context')
|
|
|
|
if not node.value.id in our_locals:
|
|
_raise_static_error(node, f'Undefined variable {node.value.id}')
|
|
|
|
node_typ = our_locals[node.value.id]
|
|
|
|
slice_expr = self.visit_Module_FunctionDef_expr(
|
|
module, function, our_locals, module.types['i32'], node.slice.value,
|
|
)
|
|
|
|
if isinstance(node_typ, TypeBytes):
|
|
return AccessBytesIndex(
|
|
module.types['u8'],
|
|
VariableReference(node_typ, node.value.id),
|
|
slice_expr,
|
|
)
|
|
|
|
if isinstance(node_typ, TypeTuple):
|
|
if not isinstance(slice_expr, ConstantInt32):
|
|
_raise_static_error(node, 'Must subscript using a constant index')
|
|
|
|
idx = slice_expr.value
|
|
|
|
if len(node_typ.members) <= idx:
|
|
_raise_static_error(node, f'Index {idx} out of bounds for tuple {node.value.id}')
|
|
|
|
member = node_typ.members[idx]
|
|
if exp_type != member.type:
|
|
_raise_static_error(node, f'Expected {codestyle.type_(exp_type)}, {node.value.id}[{idx}] is actually {codestyle.type_(member.type)}')
|
|
|
|
return AccessTupleMember(
|
|
VariableReference(node_typ, node.value.id),
|
|
member,
|
|
)
|
|
|
|
_raise_static_error(node, f'Cannot take index of {node_typ} {node.value.id}')
|
|
|
|
def visit_Module_FunctionDef_Constant(self, module: Module, function: Function, exp_type: TypeBase, node: ast.Constant) -> Expression:
|
|
del module
|
|
del function
|
|
|
|
_not_implemented(node.kind is None, 'Constant.kind')
|
|
|
|
if isinstance(exp_type, TypeUInt8):
|
|
if not isinstance(node.value, int):
|
|
_raise_static_error(node, 'Expected integer value')
|
|
|
|
if node.value < 0 or node.value > 255:
|
|
_raise_static_error(node, 'Integer value out of range')
|
|
|
|
return ConstantUInt8(exp_type, node.value)
|
|
|
|
if isinstance(exp_type, TypeUInt32):
|
|
if not isinstance(node.value, int):
|
|
_raise_static_error(node, 'Expected integer value')
|
|
|
|
if node.value < 0 or node.value > 4294967295:
|
|
_raise_static_error(node, 'Integer value out of range')
|
|
|
|
return ConstantUInt32(exp_type, node.value)
|
|
|
|
if isinstance(exp_type, TypeUInt64):
|
|
if not isinstance(node.value, int):
|
|
_raise_static_error(node, 'Expected integer value')
|
|
|
|
if node.value < 0 or node.value > 18446744073709551615:
|
|
_raise_static_error(node, 'Integer value out of range')
|
|
|
|
return ConstantUInt64(exp_type, node.value)
|
|
|
|
if isinstance(exp_type, TypeInt32):
|
|
if not isinstance(node.value, int):
|
|
_raise_static_error(node, 'Expected integer value')
|
|
|
|
if node.value < -2147483648 or node.value > 2147483647:
|
|
_raise_static_error(node, 'Integer value out of range')
|
|
|
|
return ConstantInt32(exp_type, node.value)
|
|
|
|
if isinstance(exp_type, TypeInt64):
|
|
if not isinstance(node.value, int):
|
|
_raise_static_error(node, 'Expected integer value')
|
|
|
|
if node.value < -9223372036854775808 or node.value > 9223372036854775807:
|
|
_raise_static_error(node, 'Integer value out of range')
|
|
|
|
return ConstantInt64(exp_type, node.value)
|
|
|
|
if isinstance(exp_type, TypeFloat32):
|
|
if not isinstance(node.value, (float, int, )):
|
|
_raise_static_error(node, 'Expected float value')
|
|
|
|
# FIXME: Range check
|
|
|
|
return ConstantFloat32(exp_type, node.value)
|
|
|
|
if isinstance(exp_type, TypeFloat64):
|
|
if not isinstance(node.value, (float, int, )):
|
|
_raise_static_error(node, 'Expected float value')
|
|
|
|
# FIXME: Range check
|
|
|
|
return ConstantFloat64(exp_type, node.value)
|
|
|
|
raise NotImplementedError(f'{node} as const for type {exp_type}')
|
|
|
|
def visit_type(self, module: Module, node: ast.expr) -> TypeBase:
|
|
if isinstance(node, ast.Constant):
|
|
if node.value is None:
|
|
return module.types['None']
|
|
|
|
_raise_static_error(node, f'Unrecognized type {node.value}')
|
|
|
|
if isinstance(node, ast.Name):
|
|
if not isinstance(node.ctx, ast.Load):
|
|
_raise_static_error(node, 'Must be load context')
|
|
|
|
if node.id in module.types:
|
|
return module.types[node.id]
|
|
|
|
if node.id in module.structs:
|
|
return module.structs[node.id]
|
|
|
|
_raise_static_error(node, f'Unrecognized type {node.id}')
|
|
|
|
if isinstance(node, ast.Tuple):
|
|
if not isinstance(node.ctx, ast.Load):
|
|
_raise_static_error(node, 'Must be load context')
|
|
|
|
result = TypeTuple()
|
|
|
|
offset = 0
|
|
|
|
for idx, elt in enumerate(node.elts):
|
|
member = TypeTupleMember(idx, self.visit_type(module, elt), offset)
|
|
|
|
result.members.append(member)
|
|
offset += member.type.alloc_size()
|
|
|
|
key = result.render_internal_name()
|
|
|
|
if key not in module.types:
|
|
module.types[key] = result
|
|
constructor = TupleConstructor(result)
|
|
module.functions[constructor.name] = constructor
|
|
|
|
return module.types[key]
|
|
|
|
raise NotImplementedError(f'{node} as type')
|
|
|
|
def _not_implemented(check: Any, msg: str) -> None:
|
|
if not check:
|
|
raise NotImplementedError(msg)
|
|
|
|
def _raise_static_error(node: Union[ast.mod, ast.stmt, ast.expr], msg: str) -> NoReturn:
|
|
raise StaticError(
|
|
f'Static error on line {node.lineno}: {msg}'
|
|
)
|