Commit dd8237fe authored by Chris Jewell's avatar Chris Jewell
Browse files

Extensive review of the semantic analysis steps to try to include type...

Extensive review of the semantic analysis steps to try to include type checking.  Semantic analysis now has its own folder with gemlang, scopes are now encapsulated within a SymbolTable object, TreeWalker updated to climb up a type hierarchy where AST Nodes inherit from parent nodes.  This allows us to match generic types of node.
parent 9b2ad613
......@@ -66,6 +66,9 @@ class ASTNode:
def __str__(self):
return self.__class__.__name__
def __repr__(self):
return self.__class__.__name__
class NullNode(ASTNode):
"""Represents NULL node. This can be used to effectively remove subtrees from the AST.
......@@ -77,9 +80,12 @@ class NullNode(ASTNode):
super().__init__(*args, **kwargs)
self.value = value
def __str__(self):
def __repr__(self):
return f"NullNode(<{self.value}>)"
def __str__(self):
return str(self.value)
class GEMProgram(ASTNode):
"""Represents a GEM program
......@@ -121,9 +127,12 @@ class FArg(ASTNode):
super().__init__(**kwargs)
self.value = name
def __str__(self):
def __repr__(self):
return f"{self.__class__.__name__} {self.value}"
def __str__(self):
return str(self.value)
class FArgList(ASTNode):
"""Represents a list of ArgDefs
......@@ -146,6 +155,9 @@ class KwRef(ASTNode):
super().__init__(**kwargs)
self.value = name
def __str__(self):
def __repr__(self):
return "{cls} {name}".format(cls=self.__class__.__name__,
name=self.value)
def __str__(self):
return str(self.value)
......@@ -24,10 +24,10 @@ import operator
import numpy as np
from gem.gemlang.ast.ast_base import Statement, ASTNode, KwRef
from gem.gemlang.symbol import Scope
from gem.gemlang.semantics.scope import Scope
__all__ = ['Expr', 'UnaryExpr', 'BinaryExpr', 'Call', 'KwArg',
'IdRef', 'Number', 'String', 'TransitionDef', 'ArgList']
'IdRef', 'Number', 'Integer', 'Float', 'String', 'TransitionDef', 'ArgList']
_BINARY_OPS = ['truediv', 'mul', 'matmul', 'add', 'sub', 'pow', 'lt', 'gt',
'le', 'ge', 'eq', 'ne']
......@@ -158,14 +158,16 @@ class IdRef(Expr):
:param name: the name of the symbol.
"""
def __init__(self, name, *args, **kwargs):
super().__init__(*args, **kwargs)
self.value = str(name) # :ivar the symbol name (str)
def __str__(self):
def __repr__(self):
return "IdRef {}".format(self.value)
def __str__(self):
return str(self.value)
## Data classes
class Number(Expr):
......@@ -183,10 +185,28 @@ class Number(Expr):
self.ndim = data.ndim
def __str__(self):
return "Number {}".format(self.value)
return str(self.value)
def __repr__(self):
return self.value
return f"{self.__class__.__name__} {self.value}"
class Integer(Number):
"""Represents an integer
:param data: an integer
"""
def __init__(self, data, *args, **kwargs):
data = int(data)
super().__init__(data, *args, **kwargs)
class Float(Number):
"""Represents a float
:param data: a floating point number
"""
def __init__(self, data, *args, **kwargs):
data = float(data)
super().__init__(data, *args, **kwargs)
class String(Expr):
......@@ -199,10 +219,10 @@ class String(Expr):
super().__init__(**kwargs)
self.value = data #: value: the value of the string.
def __str__(self):
def __repr__(self):
return "String \"{}\"".format(self.value)
def __repr__(self):
def __str__(self):
return self.value
......
......@@ -54,9 +54,11 @@ class EpiDecl(Decl):
self.add_child(param)
self.add_child(block)
def __str__(self):
def __repr__(self):
return f"EpiDecl {self.name}"
__str__ = __repr__
class AssignExpr(Statement):
"""Assignment of lhs = rhs
......
......@@ -31,7 +31,7 @@ def serialize(node):
:param node: an AST node.
"""
assert isinstance(node, ASTNode)
string = "{node}".format(node=str(node))
string = "{node}".format(node=repr(node))
for child in node.children:
string += " " + serialize(child)
return "({})".format(string)
......
......@@ -44,9 +44,12 @@ class ASTWalker:
self.__in_place = in_place
def __call(self, when, ast_node):
attr = "{}_{}".format(when, ast_node.__class__.__name__)
action = getattr(self, attr, self.__default__)
return action(ast_node)
for cls in ast_node.__class__.mro():
attr = "{}_{}".format(when, cls.__name__)
action = getattr(self, attr, None)
if action is not None:
return action(ast_node)
return self.__default__(ast_node)
def __callOnEntry(self, ast_node):
new_node = self.__call('onEnter', ast_node)
......
......@@ -27,7 +27,7 @@ if have_graphviz:
from gem.gemlang.ast import ASTNode
from gem.gemlang.ast.ast_statement import Statement
from gem.gemlang.ast.ast_expression import IdRef
from gem.gemlang.symbol import RandomVariableSymbol, VariableSymbol, STMSymbol
from gem.gemlang.semantics.symbol import RandomVariableSymbol, VariableSymbol, STMSymbol
class DAGNode(ASTNode):
......
......@@ -60,7 +60,7 @@ _atom: "(" expr ")"
| string
// | indexedvar
call: _atom "(" arglist ")"
call: idref "(" arglist ")"
//indexedvar: _atom "[" _subscriptlist "]"
_subscriptlist: expr ("," expr)* [","]
......
......@@ -2,9 +2,9 @@ from gem.gemlang import ASTWalker
from gem.gemlang.ast.ast_base import NullNode
from gem.gemlang.ast.ast_expression import IdRef
from gem.gemlang.ast.ast_statement import AssignExpr
from gem.gemlang.symbol import PlaceholderSymbol
from gem.gemlang.tf_output import convert_to_maths_layer
from gem.gemlang.semantics.symbol import *
class DataAttacher(ASTWalker):
"""InjectData does three things:
......@@ -38,11 +38,11 @@ class DataAttacher(ASTWalker):
lhs = assign.children[0]
rhs = assign.children[1]
rhs_symb = rhs.children[0].symbol if rhs.has_children() else None
if isinstance(rhs_symb, PlaceholderSymbol):
if isinstance(rhs_symb, ExternalDataSymbol):
# ensure lhs is represented in the data
if not lhs.value in self.__data.keys():
raise KeyError(
f"Data for Placeholder '{lhs.value}' not supplied at line {lhs.meta.line} column {lhs.meta.column}")
f"Data for external data reference '{lhs.value}' not supplied at line {lhs.meta.line} column {lhs.meta.column}")
# TODO ensure types match
return NullNode("Placeholder removed")
......
......@@ -19,12 +19,13 @@
#
import gem.gemlang.tf_output as out
from gem.gemlang.ast.ast_base import NullNode
from gem.gemlang.ast.ast_expression import Call
from gem.gemlang.ast_walker import ASTWalker
from gem.gemlang.symbol import *
from gem.gemlang.symbol_resolve import get_random_vars
from gem.gemlang.semantics.symbol_table import SymbolTable
from gem.gemlang.semantics.symbol import *
from gem.gemlang.semantics.symbol_resolve import get_random_vars
def error_pos(msg, meta):
......@@ -44,6 +45,7 @@ class CodeGenerator(ASTWalker):
self.__workspace_stack = []
self.vars = []
self.random_vars = []
self.external_data = {}
@property
def current_workspace(self):
......@@ -56,24 +58,34 @@ class CodeGenerator(ASTWalker):
def pop_ws(self):
return self.__workspace_stack.pop()
def generate(self, ast, symbol_table):
self.vars = [symb.name for symb in symbol_table.get_by_type(VariableSymbol)]
def generate(self, ast, symbol_table, external_data={}):
self.vars = [symb.name for symb in symbol_table.globals.get_by_type(VariableSymbol)]
self.random_vars = get_random_vars(symbol_table)
self.external_data = external_data
return self._walk(ast)
def onEnter_GEMProgram(self, output_obj):
self.push_ws()
def onExit_GEMProgram(self, output_obj):
stmts = [stmt for stmt in output_obj.children if
not isinstance(stmt, NullNode)]
stmts = [out.AssignExternal(k) for k in self.external_data.keys()]
stmts.extend([stmt for stmt in output_obj.children if
not isinstance(stmt, NullNode)])
self.pop_ws()
return str(out.GEMProgram(self.vars, stmts))
def onEnter_Block(self, block):
pass
def onExit_Block(self, block):
pass
def onEnter_EpiDecl(self, epidecl):
self.push_ws()
self.current_workspace['states'] = {}
self.current_workspace['states'] = []
self.current_workspace['initial'] = []
self.current_workspace['transitions'] = []
return epidecl
def onExit_EpiDecl(self, epidecl):
name = epidecl.name
......@@ -89,8 +101,6 @@ class CodeGenerator(ASTWalker):
fsymb = call.children[0].symbol
fname = self._walk(call.children[0])
args = self._walk(call.children[1])
if isinstance(fsymb, BuiltInFunctionSymbol):
return out.builtin_function[str(fname)](args)
return out.Function(str(fname), args)
def onExit_ArgList(self, arglist):
......@@ -98,7 +108,7 @@ class CodeGenerator(ASTWalker):
return args
def onExit_KwArg(self, kwarg):
return out.KwArg(kwarg.children[0], kwarg.children[1])
return str(kwarg.children[1])
def onExit_TruedivExpr(self, op):
return out.Truediv(op.children[0], op.children[1])
......@@ -142,13 +152,21 @@ class CodeGenerator(ASTWalker):
def onExit_NotExpr(self, op):
return out.Not(op.children[0])
def onEnter_AssignExpr(self, ast_node):
lhs = ast_node.children[0]
rhs = ast_node.children[1]
if lhs.symbol.attrib & SymbolTable.ATTRIB.EXTERN:
return NullNode('external reference removed')
if lhs.type == SymbolTable._state:
initial = self._walk(rhs.children[1])
self.current_workspace['states'].append(lhs.value)
self.current_workspace['initial'].append(initial[0])
return NullNode('state reference removed')
def onExit_AssignExpr(self, ast_node):
lhs = ast_node.children[0]
rhs = ast_node.children[1]
if isinstance(rhs, out.BuiltinState):
self.current_workspace['states'][str(lhs)] = rhs
else:
return out.Assign(lhs, rhs)
return out.Assign(lhs, rhs)
def onEnter_StochasticAssignExpr(self, ast_node):
lhs = ast_node.children[0]
......@@ -158,12 +176,12 @@ class CodeGenerator(ASTWalker):
args = self._walk(rhs.children[1])
observed = getattr(rhs, 'observed', None)
if str(
fname) in out.builtin_function.keys(): # TODO find a better solution to translating GEM distros to TF
rv_obj = out.builtin_function[str(fname)](args, value=observed,
name=varname)
fname) in out.builtins_.keys(): # TODO find a better solution to translating GEM distros to TF
rv_obj = out.builtins_[str(fname)](args, value=observed,
name=varname)
else:
rv_obj = out.STMInstance(fname, args, value=observed, name=varname)
return out.Assign(out.Ref(varname), rv_obj)
return out.Assign(varname, rv_obj)
def onExit_AssignTransition(self, ast_node):
transition = ast_node.children[0]
......@@ -182,7 +200,7 @@ class CodeGenerator(ASTWalker):
return out.Number(n.value)
def onExit_IdRef(self, ref):
return out.Ref(ref.value)
return str(ref.value)
def onExit_String(self, str):
return out.String(str.value)
def onExit_String(self, string):
return str(string.value)
......@@ -140,7 +140,11 @@ class GEMParser(Transformer):
return IdRef(str(args[0]), meta=meta)
def number(self, args, meta):
return Number(float(args[0]), meta=meta)
if args[0].type == 'INT':
return Integer(args[0], meta=meta)
if args[0].type == 'FLOAT':
return Float(args[0], meta=meta)
raise TypeError(f"Unrecognised Numeric type at line {meta.line} column {meta.column}.")
def string(self, args, meta):
return String(str(args[0][1:-1]), meta=meta)
......
# Copyright 2019 Chris Jewell <c.jewell@lancaster.ac.uk>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
# IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
# Copyright 2019 Chris Jewell <c.jewell@lancaster.ac.uk>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
# IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
from abc import ABC, abstractmethod
from collections import OrderedDict
from gem.gemlang.ast import ASTNode
class ScopeBase(ABC):
"""Base class interface for a scope
"""
@property
@abstractmethod
def name(self):
pass
@property
@abstractmethod
def parent_scope(self):
"""Where to look next for symbols.
:returns: an instance of a Scope
"""
return self.enclosing_scope
@property
@abstractmethod
def enclosing_scope(self):
"""Returns the Scope in which this Scope is defined.
:returns: an instance of Scope, or None if global scope.
"""
return self.__enclosing_scope
@property
@abstractmethod
def level(self):
"""Returns the scope level relative to a root scope.
:returns: an integer giving the scope level (0-based)
"""
@abstractmethod
def declare(self):
pass
@abstractmethod
def resolve(self):
pass
class Scope(ScopeBase):
"""Base class for a symbol scope
:param enclosing_scope: an enclosing scope
"""
def __init__(self, enclosing_scope=None):
self.__enclosing_scope = enclosing_scope
self.__symbols = OrderedDict()
self.__level = enclosing_scope.level + 1 if enclosing_scope is not None else 0
self.__ast_def = None
@property
def level(self):
return self.__level
@property
def enclosing_scope(self):
return self.__enclosing_scope
@property
def parent_scope(self):
return self.enclosing_scope
@property
def ast_def(self):
return self.__ast_def
@ast_def.setter
def ast_def(self, ast_def):
assert isinstance(ast_def, ASTNode)
self.__ast_def = ast_def
def declare(self, symbol):
"""Declare a symbol.
:param symbol: a symbol
"""
if symbol.name in self.__symbols:
raise NameError("Duplicate declaration '{}'".format(symbol.name))
self.__symbols[symbol.name] = symbol
symbol.scope = self
def resolve(self, symbol_name):
"""Resolve a symbol
:param symbol_name: the name of a symbol
:type symbol_name: str
:return reference to a Symbol object
:raise NameError if the symbol_name doesn't exist.
"""
symbol = self.__symbols.get(symbol_name)
if symbol is not None:
return symbol
elif self.parent_scope is not None:
return self.parent_scope.resolve(symbol_name)
else:
raise NameError("Undefined symbol '{}'".format(symbol_name))
def get_by_type(self, symbol_type):
"""Returns all symbols of a given type
:param symbol_type: a symbol class name
:return a list of symbols of type symbol_type
"""
return [symb for symb in self.__symbols.values() if
isinstance(symb, symbol_type)]
def __str__(self):
symtab_header = "{} scope symbol table".format(self.__class__)
lines = [symtab_header]
lines.extend(
('%s%s: %r' % (" " * self.__level, key, value))
for key, value in self.__symbols.items()
)
s = '\n'.join(lines)
return s
class GlobalScope(Scope):
"""A global scope.
"""
def __init__(self):
super().__init__(enclosing_scope=None)
@property
def name(self):
return 'global'
class LocalScope(Scope):
"""A local scope
:param enclosing_scope: the parent scope
"""
def __init__(self, enclosing_scope=None):
assert enclosing_scope is not None
super().__init__(enclosing_scope)
@property
def name(self):
return 'local'
# Copyright 2019 Chris Jewell <c.jewell@lancaster.ac.uk>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.
#
# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
# FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS
# OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
# WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR
# IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
#
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
#
#
from gem.gemlang import ASTWalker
from gem.gemlang.semantics.scope import ScopeBase
class ScopedWalker(ASTWalker):
def __init__(self):
super().__init__(in_place=True)
self._scope_stack = []
@property
def current_scope(self):
return self._scope_stack[-1]
def _push_scope(self, scope):
assert isinstance(scope, ScopeBase)
self._scope_stack.append(scope)
return scope
def _pop_scope(self):
return self._scope_stack.pop()
\ No newline at end of file
# Copyright 2019 Chris Jewell <c.jewell@lancaster.ac.uk>
#
# Permission is hereby granted, free of charge, to any person obtaining a copy of this software
# and associated documentation files (the "Software"), to deal in the Software without restriction,
# including without limitation the rights to use, copy, modify, merge, publish, distribute,
# sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is
# furnished to do so, subject to the following conditions:
#
# The above copyright notice and this permission notice shall be included in all copies or
# substantial portions of the Software.