blob: 56be8c7def2c7c1068d745fc677bf6d6da5719b2 [file] [log] [blame] [edit]
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# Copyright 2019, Data61, CSIRO (ABN 41 687 119 230)
#
# SPDX-License-Identifier: BSD-2-Clause
#
#
from __future__ import absolute_import, division, print_function, \
unicode_literals
from camkes.internal.seven import cmp, filter, map, zip
from camkes.ast import ASTObject, Instance, Reference, SimpleTraversalContext, Group, Instance
from .exception import ParseError
import collections, six
class ScopingContext(SimpleTraversalContext):
'''
A global context for managing recursive namespace scopes.
The primary purpose of this class is for reference resolution. References
may appear in the AST that consist of a symbol name. From their context and
the CAmkES grammar, the type of the entity to which they refer can be
inferred. References with attached type information are present in the
output of the stage 3 parser, a.k.a. the lifted AST. The stage 4 parser
uses one of these contexts to replace all such references by resolving them
to the entities to which they refer.
Usage of this class can be determined by looking at the stage 4 parser, but
essentially, once you have constructed one of these, call `open` every time
you enter a nested scope and `close` every time you leave one. Within a
scope, `register` can be used to save an AST object by name. `lookup` is
used to search for an entity by name, optionally limiting to type, in the
immediate and all containing scopes. The first (deepest) matching entity or
entities are returned. When you close a scope, all registered entities at
that level are discarded.
'''
def __init__(self):
self.scopes = []
def open(self):
self.scopes.append(collections.defaultdict(dict))
def close(self):
assert len(self.scopes) > 0
self.scopes.pop()
def register(self, obj):
assert isinstance(obj, ASTObject)
assert not isinstance(obj, Reference)
assert len(self.scopes) > 0
if not hasattr(obj, 'name') or obj.name is None:
return
scope_type = type(obj)
if scope_type in [Group, Instance]:
scope_type = "group/component"
else:
scope_type = type(obj).__name__
duplicate = self.scopes[-1][obj.name].get(scope_type)
if duplicate is not None:
raise ParseError('duplicate definition of %s \'%s\'; previous '
'definition was at %s:%s' % (scope_type, obj.name,
duplicate.filename or '<unnamed>', duplicate.lineno),
obj.location)
self.scopes[-1][obj.name][scope_type] = obj
def lookup(self, ref, type=None):
'''
Look up a symbol in the current context.
The caller is expected to provide a list of stems of a qualified
reference as `ref`. That is, to look up the qualified reference
'foo.bar.baz', the caller would provide `ref` as ['foo', 'bar', 'baz'].
The values returned can be filtered by type using the optional `type`
argument.
'''
assert isinstance(ref, list) and len(ref) > 0 and \
all([isinstance(x, six.string_types) for x in ref])
# Peel off the root stem.
head, tail = ref[0], ref[1:]
# Look backwards through the scopes to ensure we yield inner results
# before outer results.
for scope in reversed(self.scopes):
for candidate in scope[head].values():
if len(tail) == 0:
# Bottomed out resolution of the full reference.
if type is None or isinstance(candidate, type):
yield candidate
else:
# Construct a scope for the entity we've found and
# continue the search, descending into it.
inner = within(candidate)
for c in inner.lookup(tail, type):
if type is None or isinstance(c, type):
yield c
def __enter__(self):
self.open()
def __exit__(self, *_):
self.close()
class ForwardScopingContext(ScopingContext):
'''
A scoping context that supports the resolution of forward references.
Note that this class needs support from the associated traversal action to
function correctly; namely, the traversal action needs to track the last
AST object it has seen at any point. It is also likely that any caller will
want to immediately register all top-level AST objects on creation of this
context. See the usage in the stage 4 parser for more details.
'''
def __call__(self, action):
'''
Register all children of the last thing our traversal action saw. This
is designed to be used in a `with` block. The purpose of registering
all children is so they can be found in the penultimate scope when
looked up from the ultimate one.
'''
assert hasattr(action, 'last_seen')
assert isinstance(action.last_seen, ASTObject)
self.open()
[self.register(x) for x in action.last_seen.children
if x is not None and not isinstance(x, Reference)]
return self
def __enter__(self):
# Suppress `__enter__` in our parent which would open another scope.
pass
def within(item):
'''
Construct a scope for resolution within an AST object.
'''
assert isinstance(item, ASTObject)
# Create a new scope.
scope = ScopingContext()
scope.open()
if isinstance(item, Instance):
# HACK: We want references inside an instance to actually resolve to
# the children of the *type*, not the instance.
item = item.type
# Register all children of this object.
for f in [x for x in item.child_fields if hasattr(item, x) and
getattr(item, x) is not None]:
member = getattr(item, f)
if isinstance(member, list):
[scope.register(x) for x in member]
else:
scope.register(member)
return scope