Skip to main content

TypePal Configuration

rascal-0.40.16
typepal-0.14.8

Synopsis

Configuration options for TypePal

Description

TypePal provides configuration options for

  • Name Resolution & Overloading: configures how names are resolved and which overloading is allowed.
  • Operations on Types: configures how operations like subtype and least-upper-bound (lub) are defined.
  • Retrieval of Types: configures how named and structured types are handled.
  • Extension Points: configures operations before an after solving.
  • Miscellaneous: utility functions that can be configured.

Here is an overview:

TypePalConfig.png

isAcceptableSimple

/* Configuration field */ Accept (TModel tm, loc def, Use use) isAcceptableSimple

Here

  • tm is a given TModel
  • def is a proposed definition
  • use is the use (characterized by the Use data type that contains, name, occurrence, scope and identifier roles of the use for which the definition is proposed.

isAcceptableSimple accepts or rejects a proposed definition for the use of a simple name in a particular role. The returned Accept data type is defined as:

data Accept 
= acceptBinding()
| ignoreContinue()
| ignoreSkipPath()
;

The default isAcceptableSimple returns acceptBinding()`.

Typical concerns addressed by isAcceptableSimple are:

  • enforce definition before use;
  • check access rights, e.g. visibility.

By comparing the offset of the source locations of definition, respectively, the use, we enforce definition before use:

Accept myIsAcceptableSimple(TModel tm, loc def, Use use)
= use.occ.offset > def.offset ? acceptBinding() : ignoreContinue();

isAcceptableQualified

/* Configuration field */ Accept (TModel tm, loc def, Use use) isAcceptableQualified

Here

  • tm is a given TModel
  • def is a proposed definition
  • use is the use for which the definition is proposed.

isAcceptableQualified accepts or rejects a proposed definition for the use of a qualified name in a particular role.

isAcceptablePath

/* Configuration field */ 
Accept (TModel tm, loc defScope, loc def, Use use, PathRole pathRole) isAcceptablePath

Here

  • tm is a given TModel;
  • defScope is the scope in which the proposed definition occurs;
  • def is a proposed definition;
  • use is the use for which the definition is proposed;
  • pathRole is the role of the semantic path.

isAcceptablePath accepts or rejects a proposed access path between use and definition.

To illustrate this, assume a language with modules and imports. A module may contain variable definitions but these are not visible from outside the module. This can be enforced as follows:

Accept myIsAcceptablePath(TModel tm, loc def, Use use, PathRole pathRole) {
return variableId() in use.idRoles ? ignoreContinue() : acceptBinding();
}

mayOverload

/* Configuration field */ bool (set[loc] defs, map[loc, Define] defines) mayOverload 

mayOverload determines whether a set of definitions (defs) are allowed to be overloaded, given their definitions (defines).

For example, Featherweight Java the only allowed overloading is between class names and constructor names.

bool fwjMayOverload (set[loc] defs, map[loc, Define] defines) {
roles = {defines[def].idRole | def <- defs};
return roles ### {classId(), constructorId()};
}
  • ❶ First collect all the roles in which the overloaded names have been defined.
  • ❷ Only allow the combination of class name and constructor name.

Operations on Types

Various operations on types can be configured by way of user-defined functions.

isSubType

/* Configuration field */ bool (AType l, AType r) isSubType 

Function that checks whether l is a subtype of r.

getLub

/* Configuration field */ AType (AType l, AType r) getLub

Function that computes the least upperbound of two types and l and r.

getMinAType

/* Configuration field */ AType() getMinAType 

Function that returns the smallest type of the type lattice.

getMaxAType

/* Configuration field */ AType() getMaxAType

Function that returns the largest type of the type lattice.

instantiateTypeParameters

/* Configuration field */ AType (Tree current, AType def, AType ins, AType act, Solver s) instantiateTypeParameters

The function instantiateTypeParameters defines instantiation of language-specific type parameters, where:

  • current is a source code fragment for which type act has already been determined, but any language-specific type parameters in act may still need to be instantiated.
  • def is the parameterized type of act.
  • ins is an instantiated version of the type of act (i.e., with bound type parameters).
  • act is the actual type found for current that needs to be instantiated.

instantiateTypeParameters will match def with ins and the resulting bindings will be used to instantiate act. The instantiated version of act is returned.

In the StructParameters demo parameterized structs (records) are defined. The formal type of such a struct is structDef(str name, list[str] formals), i.e., a struct has a name and a list of named formal type parameters. The actual type of a struct is structType(str name, list[AType] actuals), i.e., a struct name followed by actual types for the parameters.

The definition of instantiateTypeParameters for this example is as follows:

AType structParametersInstantiateTypeParameters(Tree current, structDef(str name1, list[str] formals), structType(str name2, list[AType] actuals), AType t, Solver s){
if(size(formals) != size(actuals)) throw checkFailed([]);
bindings = (formals[i] : actuals [i] | int i <- index(formals));

return visit(t) { case typeFormal(str x) => bindings[x] };
}

default AType structParametersInstantiateTypeParameters(Tree current, AType def, AType ins, AType act, Solver s) = act;

getTypeNamesAndRole

/* Configuration field */  tuple[list[str] typeNames, set[IdRole] idRoles] (AType atype) getTypeNamesAndRole

This function determines whether a given atype is a named type or not. This is needed for the customization of indirect type computations such as useViaType and getTypeInType. When atype is a named type getTypeNamesAndRole returns:

  • A list of names that may be associated with it. In most languages this will contain just a single element, the name of the type. In more sophisticated cases the list may contain a list of named types to be considered.
  • A list of roles in which the type name can be bound.

Here are the definitions for the Struct demo:

tuple[list[str] typeNames, set[IdRole] idRoles] structGetTypeNamesAndRole(structType(str name)){
return <[name], {structId()}>;
}

default tuple[list[str] typeNames, set[IdRole] idRoles] structGetTypeNamesAndRole(AType t){
return <[], {}>;
}
  • ❶ A structType(name) has a name that is bound in the role structId(). Return the name and role.
  • ❷ Any other type is unnamed; return an empty list of type names.

Another example is the Rascal type checker, where we need to model the case that all abstract data types are a subtype of Tree. In that case getTypeNamesAndRole will return <["A", "Tree"], roles> for an abstract data type A. The net effect is that when the search for a name in A fails, the search is continued in Tree.

getTypeInTypeFromDefine

/* Configuration field */  AType (Define containerDef, str selectorName, set[IdRole] idRolesSel, Solver s) getTypeInTypeFromDefine

In some extreme cases (think Rascal) the type of a field selection cannot be determined by considering all the fields defined in a container type and as a last resort one needs to fall back to information that has been associated with the original definition of the container type. getTypeInTypeFromDefine is called as a last resort from getTypeInType.

In the Rascal type checker common keyword parameters of data declarations are handled using getTypeInTypeFromDefine.

getTypeInNamelessType

/* Configuration field */ AType(AType containerType, Tree selector, loc scope, Solver s) getTypeInNamelessType

getTypeInNamelessType describes field selection on built-types that have not been explicitly declared with a name. A case in point is a length field on a built-in string type.

In the StaticFields demo)) this is done as follows:

AType staticFieldsGetTypeInNamelessType(AType containerType, Tree selector, loc scope, Solver s){
if(containerType ### strType() && "<selector>" ### "length") return intType();
s.report(error(selector, "Undefined field %q on %t", selector, containerType));
}

preSolver

/* Configuration field */ TModel(map[str,Tree] namedTrees, TModel tm) preSolver

A function preSolver that can enrich or transform the TModel before the Solver is applied to it.

postSolver

/* Configuration field */ void (map[str,Tree] namedTrees, Solver s) postSolver

A function postSolver that can enrich or transform the TModel after constraint solving is complete.

normalizeName

/* Configuration field */  str(str) normalizeName  

A function normalizeName to define language-specific escape rules for names. By default, all backslashes are removed from names.

createLogicalLoc

/* Configuration field */ loc (Define def, str modelName, PathConfig pcfg) createLogicalLoc

Internally, TypePal operates on physical source locations, e.g., source locations that point to a specific location in a source file. This all works fine, until modules and separate compilation come into play. Case in point is a function f declared in file A that is used in file B. When file A is edited, the source location of f will most likely change, while in most cases the definition of f has not been changed. Any information based on f's original source location will be broken. To solve this problem logical source locations are introduced that abstract away from the precise source locations and are thus better.

Given a Define def, the function createLogicalLoc returns the logical version of def.defined if desired, otherwise it returns def.defined unmodified. This function gives complete freedom for which IdRoles logical locations will be generated and in what way they are encoded. In the case of a simple functional language called fun, the function f will, for instance, be encoded as |fun+function:///A/f|. In the case of Rascal where extensive function overloading is present, we generate |rascal+function:///A/f$abcde|, where $abcde is a prefix of the md5hash of the complete function declaration of f.

danger

When a function is overloaded, measures have to be taken to create unique logical locations for each overloaded variant of that function. A naive solution is to use the line number of the declaration. A more solid solution is to use a hash of the actual contents of the function to distingush each function variant.