wyil.transform.NameResolution Maven / Gradle / Ivy
// Copyright 2011 The Whiley Project Developers
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
package wyil.transform;
import wycc.lang.Syntactic;
import wycc.util.AbstractCompilationUnit.Identifier;
import wycc.util.AbstractCompilationUnit.Name;
import wycc.util.AbstractCompilationUnit.Ref;
import wycc.util.AbstractHeap;
import wyc.util.ErrorMessages;
import wyil.lang.WyilFile;
import wyil.lang.WyilFile.*;
import static wyil.lang.WyilFile.*;
import java.io.IOException;
import java.lang.reflect.Array;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import wycc.util.ArrayUtils;
import wyil.util.AbstractConsumer;
/**
*
* Responsible for resolving a name which occurs at some position in a
* compilation unit. This takes into account the context and, if necessary, will
* traverse import statements to resolve the query. For example, consider a
* compilation unit entitled "file":
*
*
*
* import std::ascii::*
*
* function f(T1 x, T2 y) -> (int r):
* return g(x,y)
*
*
*
* Here the name "g
" is not fully qualified. Depending on which
* file the matching declaration of g
occurs will depend on what
* its fully qualified name is. For example, if g
is declared in
* the current compilation unit then it's fully quaified name would be
* test.g
. However, it could well be declared in a compilation unit
* matching the import whiley.lang.*
.
*
*
*
* An important challenge faced is the linking of external declarations. In
* particular, such external declarations must be brought into the current
* WyilFile. Such external declarations may themselves require additional
* external declarations be imported and we must transitively import them all.
*
*
* @author David J. Pearce
*
*/
public class NameResolution {
private final WyilFile target;
/**
* The resolver identifies unresolved names and produces patches based on them.
*/
private final Resolver resolver;
private final SymbolTable symbolTable;
/**
* If enabled, import all referenced symbols in full (i.e. rather than stubs).
*/
private boolean linking = false;
private boolean status = true;
public NameResolution(List dependencies, WyilFile target, boolean link) throws IOException {
this.target = target;
this.symbolTable = new SymbolTable(target,dependencies);
this.resolver = new Resolver();
this.linking = link;
}
/**
* Apply this name resolver to a given WyilFile.
*
* @param wf
*/
public boolean apply() {
// FIXME: need to make this incremental
checkImports(target);
// Create initial set of patches.
List patches = resolver.apply(target);
// Keep iterating until all patches are resolved
while (patches.size() > 0) {
// Create importer
Importer importer = new Importer(target, !linking);
// Now continue importing until patches all resolved.
for (int i = 0; i != patches.size(); ++i) {
// Import and link the given patch
patches.get(i).apply(importer);
}
// Switch over to the next set of patches
patches = importer.getPatches();
}
// Consolidate any imported declarations as externals.
symbolTable.consolidate();
//
return status;
}
/**
* Sanity check that import statements make sense. Specifically. that: (1) the
* module being imported from exists; (2) any names being imported exist.
*
* @param target
*/
private void checkImports(WyilFile target) {
for(Decl.Unit unit : target.getModule().getUnits()) {
for(Decl d : unit.getDeclarations()) {
if(d instanceof Decl.Import) {
// Found one to check!
Decl.Import imp = (Decl.Import) d;
Tuple path = imp.getPath();
// Sanity check module exists
Name name = new Name(path.toArray(Identifier.class));
// Sanity check imported module
if(!name.toString().contains("*") && !symbolTable.contains(name)) {
// Cannot identify name
syntaxError(path.get(path.size()-1), RESOLUTION_ERROR);
} else if(imp.hasFrom() || imp.hasWith()) {
Tuple names = imp.getNames();
for (int i = 0; i != names.size(); ++i) {
Identifier id = names.get(i);
if (!id.get().equals("*") && !symbolTable.contains(new QualifiedName(name, id))) {
// Sanity check imported names (if applicable)
syntaxError(id, RESOLUTION_ERROR);
}
}
}
}
}
}
}
/**
* Responsible for identifying unresolved names which remain to be resolved to
* their corresponding declarations. This is achieved by traversing from a given
* declaration and identifying all static variables and callables which are
* referred to. In each case, a "patch" is created which identifies a location
* within the module which needs to be resolved. Whilst in some cases we could
* resolve immediately, for external symbols we cannot. Therefore, patches can
* be thought of as "lazy" resolution which works for both local and non-local
* names.
*
* @author David J. Pearce
*
*/
private class Resolver extends AbstractConsumer> {
/**
* The list of patches being constructed
*/
private ArrayList patches = new ArrayList<>();
/**
* Cache of previously resolved names.
*/
private HashMap cache = new HashMap<>();
/**
* Used to indicate when resolving something which is exposed (and, hence,
* relevant members should themselves be exposed).
*/
private boolean isVisible = false;
public List apply(WyilFile module) {
super.visitModule(module, null);
return patches;
}
@Override
public void visitUnit(Decl.Unit unit, List unused) {
// Reset cache as this is necessary when moving between compilation units!
cache.clear();
// Create an initially empty list of import statements.
super.visitUnit(unit, new ArrayList<>());
}
@Override
public void visitExternalUnit(Decl.Unit unit, List unused) {
// NOTE: we override this to prevent unnecessarily traversing units
}
@Override
public void visitType(Decl.Type decl, List unused) {
isVisible = isPublic(decl);
super.visitType(decl, unused);
isVisible = false;
}
@Override
public void visitProperty(Decl.Property decl, List unused) {
isVisible = isPublic(decl);
super.visitProperty(decl, unused);
isVisible = false;
}
@Override
public void visitFunction(Decl.Function decl, List unused) {
isVisible = isPublic(decl);
visitVariables(decl.getParameters(), unused);
visitVariables(decl.getReturns(), unused);
visitExpressions(decl.getRequires(), unused);
visitExpressions(decl.getEnsures(), unused);
isVisible = false;
visitStatement(decl.getBody(), unused);
}
@Override
public void visitMethod(Decl.Method decl, List unused) {
isVisible = isPublic(decl);
visitVariables(decl.getParameters(), unused);
visitVariables(decl.getReturns(), unused);
visitExpressions(decl.getRequires(), unused);
visitExpressions(decl.getEnsures(), unused);
isVisible = false;
visitStatement(decl.getBody(), unused);
}
@Override
public void visitImport(Decl.Import decl, List imports) {
// Reset cache as an import statement can have non-trivial effects. Ideally, it
// would be nice to incrementally update the cache.
cache.clear();
//
super.visitImport(decl, imports);
// Add this import statements to list of visible imports
imports.add(decl);
}
@Override
public void visitLambdaAccess(Expr.LambdaAccess expr, List imports) {
super.visitLambdaAccess(expr, imports);
// Resolve to qualified name
QualifiedName name = resolveAs(expr.getLink(), imports);
// Sanity check result
if(name != null) {
// Create patch
patches.add(new Patch(isVisible, name, null, expr));
}
}
@Override
public void visitStaticVariableAccess(Expr.StaticVariableAccess expr, List imports) {
super.visitStaticVariableAccess(expr, imports);
// Resolve to qualified name
QualifiedName name = resolveAs(expr.getLink(), imports);
// Sanity check result
if(name != null) {
// Create patch
patches.add(new Patch(isVisible, name, null, expr));
}
}
@Override
public void visitInvoke(Expr.Invoke expr, List imports) {
super.visitInvoke(expr, imports);
// Resolve to qualified name
QualifiedName name = resolveAs(expr.getLink(), imports);
// Sanity check result
if(name != null) {
// Create patch
patches.add(new Patch(isVisible, name, null, expr));
}
}
@Override
public void visitTypeNominal(Type.Nominal type, List imports) {
super.visitTypeNominal(type, imports);
// Resolve to qualified name
QualifiedName name = resolveAs(type.getLink(), imports);
// Sanity check result
if (name != null) {
// Create patch
patches.add(new Patch(isVisible, name, null, type));
}
}
/**
* Resolve a given name in a given compilation Unit to all corresponding
* (callable) declarations. If the name is already fully qualified then this
* amounts to checking that the name exists and finding its declaration(s);
* otherwise, we have to process the list of important statements for this
* compilation unit in an effort to qualify the name.
*
* @param name
* The name to be resolved
* @param enclosing
* The enclosing declaration in which this name is contained.
* @return
*/
private QualifiedName resolveAs(Decl.Link> link, List imports) {
QualifiedName r;
Name name = link.getName();
QualifiedName qualified = cache.get(name);
if(qualified != null) {
// Have previously resolved this particular name in this particular context.
return qualified;
} else {
// Resolve unqualified name to qualified name
switch (name.size()) {
case 1:
qualified = unqualifiedResolveAs(name.get(0), imports);
break;
case 2:
qualified = partialResolveAs(name.get(0), name.get(1), imports);
break;
default:
// NOTE: don't put these into cache as not necessary!
return new QualifiedName(name.getPath(), name.getLast());
}
// Cache resolution for performance
cache.put(name, qualified);
// Done
return qualified;
}
}
/**
* Resolve a name which is completely unqualified (e.g. to_string
).
* That is, it's just an identifier. This could be a name in the current unit,
* or an explicitly imported name/
*
* @param name
* @param imports
* @return
*/
private QualifiedName unqualifiedResolveAs(Identifier name, List imports) {
// Attempt to local resolve
Decl.Unit unit = name.getAncestor(Decl.Unit.class);
QualifiedName localName = new QualifiedName(unit.getName(), name);
if (symbolTable.contains(localName)) {
// Yes, matching local name
return localName;
} else {
// No, attempt to non-local resolve
for (int i = imports.size() - 1; i >= 0; --i) {
Decl.Import imp = imports.get(i);
if (imp.hasFrom() || imp.hasWith()) {
Tuple names = imp.getNames();
for (int j = 0; j != names.size(); ++j) {
Identifier with = names.get(j);
if (with.get().equals("*") || name.equals(with)) {
return new QualifiedName(imp.getPath(), name);
}
}
}
}
// No dice.
syntaxError(name, RESOLUTION_ERROR);
return null;
}
}
/**
* Resolve a name which is partially qualified (e.g.
* ascii::to_string
). This consists of an unqualified unit and a
* name.
*
* @param unit
* @param name
* @param kind
* @param imports
* @return
*/
private QualifiedName partialResolveAs(Identifier unit, Identifier name, List imports) {
Decl.Unit enclosing = name.getAncestor(Decl.Unit.class);
if (unit.equals(enclosing.getName().getLast())) {
// A local lookup on the enclosing compilation unit.
return unqualifiedResolveAs(name, imports);
} else {
for (int i = imports.size() - 1; i >= 0; --i) {
Decl.Import imp = imports.get(i);
Tuple path = imp.getPath();
Identifier last = path.get(path.size() - 1);
//
if (!imp.hasFrom() && last.equals(unit)) {
// Resolving partially qualified names requires no "from".
QualifiedName qualified = new QualifiedName(path, name);
if (symbolTable.contains(qualified)) {
return qualified;
}
}
}
// No dice.
syntaxError(name, RESOLUTION_ERROR);
return null;
}
}
}
/**
* Records information about a name which needs to be "patched" with its
* corresponding declaration in a given expression.
*
* @author David J. Pearce
*
*/
public class Patch {
public final boolean isPublic;
public final QualifiedName name;
public final Type type;
private final Syntactic.Item target;
private Patch parent;
public Patch(boolean isPublic, QualifiedName name, Type type, Syntactic.Item target) {
if(name == null || target == null) {
throw new IllegalArgumentException("name cannot be null");
}
this.isPublic = isPublic;
this.name = name;
this.type = type;
this.target = target;
}
/**
* Set the parent of this patch.
*
* @param parent
*/
public void setParent(Patch parent) {
this.parent = parent;
}
/**
* Get qualified name being resolved at the top-level.
*
* @return
*/
public QualifiedName getRootName() {
if(parent == null) {
return name;
} else {
return parent.getRootName();
}
}
/**
* Import and link a given patch. The import process is only necessary for
* external symbols. For these, their declarations must be imported lazily as
* stubs from the relevant declaration.
*
* @param importer
* --- The importer to use for importing.
*/
public void apply(Importer importer) {
// Import
imPort(importer);
// And, link.
link();
}
private void imPort(Importer importer) {
// Import external declarations as necessary
if (!symbolTable.isAvailable(name)) {
// FIXME: want to optimise so don't bring in the whole type unless we are doing
// link-time analysis or generating a single binary.
ArrayList imported = new ArrayList<>();
for (Decl.Named d : symbolTable.getRegisteredDeclarations(name)) {
// Sanity check import
imported.add((Decl.Named) importer.allocate(d, this));
}
symbolTable.addAvailable(name, imported);
} else {
for (Decl.Named d : symbolTable.getRegisteredDeclarations(name)) {
// Sanity check local declarations
if(isPublic && !isPublic(d)) {
syntaxError(target,EXPOSING_HIDDEN_DECLARATION);
}
}
}
}
/**
* Connect the given syntactic item with its target declaration (which may have
* just been imported).
*/
private void link() {
// Apply patch to target expression
switch (target.getOpcode()) {
case EXPR_staticvariable: {
Expr.StaticVariableAccess e = (Expr.StaticVariableAccess) target;
Decl.StaticVariable d = select(Decl.StaticVariable.class);
if(d != null) {
e.getLink().resolve(d);
}
break;
}
case EXPR_invoke: {
Expr.Invoke e = (Expr.Invoke) target;
Decl.Link link = e.getLink();
if(type != null) {
// do nothing?
Decl.Callable resolved = select(type, Decl.Callable.class);
if (resolved != null) {
e.getLink().resolve(resolved);
}
} else {
Decl.Callable[] resolved = selectAll(Decl.Callable.class);
if (resolved != null) {
e.getLink().resolve(resolved);
}
}
break;
}
case EXPR_lambdaaccess: {
Expr.LambdaAccess e = (Expr.LambdaAccess) target;
Decl.Callable[] resolved = selectAll(Decl.Callable.class);
if(resolved != null) {
e.getLink().resolve(filterParameters(e.getParameterTypes().size(), resolved));
}
break;
}
default:
case TYPE_nominal: {
Type.Nominal e = (Type.Nominal) target;
Decl.Type d = select(Decl.Type.class);
if(d != null && e.getParameters().size() != d.getTemplate().size()) {
if(e.getParameters().size() > d.getTemplate().size()) {
syntaxError(e.getLink().getName(), TOOMANY_TEMPLATE_PARAMETERS);
} else {
syntaxError(e.getLink().getName(), MISSING_TEMPLATE_PARAMETERS);
}
} else if(d != null){
e.getLink().resolve(d);
} else {
// NOTE: if we get here, then we have a public member referring to a hidden
// member. For now, this is prohibited further upstream.
}
break;
}
}
}
/**
* Resolve a name which is fully qualified (e.g.
* std::ascii::to_string
) to first matching declaration. This
* consists of a qualified unit and a name.
*
* @param name
* Fully qualified name
* @param kind
* Declaration kind we are resolving.
* @return
*/
private T select(Class kind) {
List declarations = symbolTable.getAvailableDeclarations(name);
Identifier id = name.getName();
for (int i = 0; i != declarations.size(); ++i) {
Decl.Named d = declarations.get(i);
//
if (kind.isInstance(d)) {
// Found direct instance
return (T) d;
}
}
if(parent == null) {
syntaxError(name.getName(), RESOLUTION_ERROR);
} else {
syntaxError(getRootName().getName(), INTERNAL_RESOLUTION_ERROR, name.getUnit(), name.getName());
}
return null;
}
/**
* Resolve a name which is fully qualified (e.g.
* std::array::equals
) and includes a distinguishing type signature.
*
* @param name
* Fully qualified name
* @param kind
* Declaration kind we are resolving.
* @return
*/
private T select(Type type, Class kind) {
List declarations = symbolTable.getAvailableDeclarations(name);
Identifier id = name.getName();
for (int i = 0; i != declarations.size(); ++i) {
Decl.Named d = declarations.get(i);
//
if (kind.isInstance(d) && d.getType().equals(type)) {
// Found direct instance
return (T) d;
}
}
if(parent == null) {
syntaxError(name.getName(), RESOLUTION_ERROR);
} else {
syntaxError(getRootName().getName(), INTERNAL_RESOLUTION_ERROR, name.getUnit(), name.getName());
}
return null;
}
/**
* Resolve a name which is fully qualified (e.g.
* std::ascii::to_string
) to all matching declarations. This
* consists of a qualified unit and a name.
*
* @param name
* Fully qualified name
* @param kind
* Declaration kind we are resolving.
* @return
*/
private T[] selectAll(Class kind) {
List declarations = symbolTable.getAvailableDeclarations(name);
Identifier id = name.getName();
// Determine how many matches
int count = 0;
for (int i = 0; i != declarations.size(); ++i) {
Decl.Named d = declarations.get(i);
if (kind.isInstance(d)) {
count++;
}
}
// Create the array
@SuppressWarnings("unchecked")
T[] matches = (T[]) Array.newInstance(kind, count);
// Populate the array
for (int i = 0, j = 0; i != declarations.size(); ++i) {
Decl.Named d = declarations.get(i);
if (kind.isInstance(d)) {
matches[j++] = (T) d;
}
}
// Check for resolution error
if (matches.length == 0) {
if(parent == null) {
syntaxError(name.getName(), RESOLUTION_ERROR);
} else {
syntaxError(getRootName().getName(), INTERNAL_RESOLUTION_ERROR, name.getUnit(), name.getName());
}
return null;
} else {
return matches;
}
}
/**
* Filter the given callable declarations based on their parameter count.
*
* @param parameters
* @param resolved
* @return
*/
private Decl.Callable[] filterParameters(int parameters, Decl.Callable[] resolved) {
// Remove any with incorrect number of parameters
for (int i = 0; i != resolved.length; ++i) {
Decl.Callable c = resolved[i];
if (parameters > 0 && c.getParameters().size() != parameters) {
resolved[i] = null;
}
}
return ArrayUtils.removeAll(resolved, null);
}
@Override
public String toString() {
return "<" + name + "," + target + ">";
}
}
private static Ref REF_UNKNOWN_DECL = new Ref(new Decl.Unknown());
/**
* Responsible for importing an item from one syntactic heap into another. This
* relies on all items externally referenced by the imported item having been
* already resolved.
*
* @author David J. Pearce
*
*/
private class Importer extends AbstractHeap.Allocator {
/**
* Signals whether or not to only import stubs.
*/
private final boolean stubsOnly;
/**
* List of patches generated during imports by this importer.
*/
private final ArrayList patches = new ArrayList<>();
private final Syntactic.Item REF_UNKNOWN;
public Importer(AbstractHeap heap, boolean stubsOnly) {
super(heap);
this.stubsOnly = stubsOnly;
this.REF_UNKNOWN = super.allocate(REF_UNKNOWN_DECL);
}
/**
* Import a given item as a result of a given patch.
*
* @param item
* @param parent
* @return
*/
public Syntactic.Item allocate(Syntactic.Item item, Patch parent) {
int index = patches.size();
// Allocate the item
Syntactic.Item r = allocate(item);
// Set parent for patches
for(int i=index;i ref = (Ref>) item;
Syntactic.Item referent = ref.get();
if (referent.getHeap() != heap && referent instanceof Decl.Named) {
// This is a deference to a named declaration in a different module. This will
// need to be patched.
return REF_UNKNOWN;
} else {
return super.allocate(item);
}
case DECL_function:
case DECL_method: {
if (stubsOnly) {
// drop the body from the function, making it a stub
return allocateStub((Decl.FunctionOrMethod) item);
} else {
return super.allocate(item);
}
}
// The linkable items require patching
case EXPR_staticvariable:
case EXPR_invoke:
case EXPR_lambdaaccess:
case TYPE_nominal: {
Linkable linkable = (Linkable) item;
Decl.Link extends Decl.Named> link = linkable.getLink();
item = super.allocate(item);
// Register patch
patches.add(new Patch(false, link.getTarget().getQualifiedName(), link.getTarget().getType(), item));
// Done
return item;
}
case DECL_property:
// NOTE: properties are included here because we always import the full "body".
default:
return super.allocate(item);
}
//
}
public List getPatches() {
return patches;
}
/**
* Allocate a stub into the underlying heap.
*
* @param fm
* @return
*/
private Syntactic.Item allocateStub(Decl.FunctionOrMethod fm) {
Syntactic.Item item = map.get(fm);
if (item != null) {
// item is already allocated as a stub, therefore must return that.
return item;
}
Tuple template = (Tuple) super.allocate(fm.getTemplate());
Tuple params = (Tuple) super.allocate(fm.getParameters());
Tuple returns = (Tuple) super.allocate(fm.getReturns());
Tuple requires = (Tuple) super.allocate(fm.getRequires());
Tuple ensures = (Tuple) super.allocate(fm.getEnsures());
if (fm instanceof Decl.Function) {
// Create function stub
Decl.Function f = (Decl.Function) fm;
item = new Decl.Function(f.getModifiers(), f.getName(), template, params, returns,
requires, ensures, new Stmt.Block());
} else {
// Create method stub
Decl.Method m = (Decl.Method) fm;
item = new Decl.Method(m.getModifiers(), m.getName(), template, params,
returns, requires, ensures, new Stmt.Block());
}
// Allocate new item using underlying allocator. This will recursively allocate
// child nodes.
item = super.allocate(item);
// Store item in map to ensure we don't allocate it more than once.
map.put(fm, item);
// Done
return item;
}
}
/**
* Check whether named declaration is public or not.
*
* @param decl
* @return
*/
private boolean isPublic(Decl.Named decl) {
return decl.getModifiers().match(Modifier.Public.class) != null;
}
private void syntaxError(Syntactic.Item e, int code, Syntactic.Item... context) {
status = false;
ErrorMessages.syntaxError(e, code, context);
}
}