Eli   Documents

General Information

 o Eli: Translator Construction Made Easy
 o Global Index
 o Frequently Asked Questions
 o Typical Eli Usage Errors

Tutorials

 o Quick Reference Card
 o Guide For new Eli Users
 o Release Notes of Eli
 o Tutorial on Name Analysis
 o Tutorial on Scope Graphs
 o Tutorial on Type Analysis
 o Typical Eli Usage Errors

Reference Manuals

 o User Interface
 o Eli products and parameters
 o LIDO Reference Manual
 o Typical Eli Usage Errors

Libraries

 o Eli library routines
 o Specification Module Library

Translation Tasks

 o Lexical analysis specification
 o Syntactic Analysis Manual
 o Computation in Trees

Tools

 o LIGA Control Language
 o Debugging Information for LIDO
 o Graphical ORder TOol

 o FunnelWeb User's Manual

 o Pattern-based Text Generator
 o Property Definition Language
 o Operator Identification Language
 o Tree Grammar Specification Language
 o Command Line Processing
 o COLA Options Reference Manual

 o Generating Unparsing Code

 o Monitoring a Processor's Execution

Administration

 o System Administration Guide

Mail Home

Tutorial for Name Analysis Using ScopeGraphs

Previous Chapter Next Chapter Table of Contents


Selecting Acceptable Bindings

Consider the file `clash.nl':

clash.nl[123]==

import fsm.*;
import random.*;
{ int count = 0;
  state = 0;
  while (state != 7) {
    if (ran() < 0.25) advance(1);
    else if (ran() < 0.8) advance(2);
    else advance(3);
    count = count + 1;
  }
}

package fsm;
int state;
void advance(int input) {
  if (input == 1) state = state - 1;
  else if (input == 2) state = state + 1;
}

package random;
int state = 100001;
float ran() {
  state = state * 125;
  state = state - (state / 2796203) * 2796203;
  return state / 2796203.0;
}
This macro is attached to a non-product file.

Two entities named state are available in the context of the program body, imported on demand from separate packages. Because state is ambiguous, no unique binding for either of the two applied occurrences in lines 4 and 5 can be found. See Import on demand, Exercise 3.c, for a discussion of this problem.

File `clash.nl' illustrates a weakness of the package facility. The variable random.state must retain its value from one call of ran to another, but there is no need for the name to be visible outside of random. As language designers, we could address this weakness by adding rules to NameLan allowing the programmer to specify that the binding for random.state could not be identified by an applied occurrence of state in the context of lines 4 and 5 of `clash.nl'.

This situation typifies an aspect of the name analysis process that we call acceptability: random.state is an available binding for the applied occurrence in line 4 of `clash.nl' according to the general scope rules of NameLan, but the additional rules prevent the entity bound by it from being acceptable in that context.

There are two common kinds of additional rules that prevent an available binding from being acceptable in a particular context: access rules and position rules. Access rules define some directive that a user can attach to a declaration to specify the contexts in which the binding it creates is acceptable. Position rules involve the relative locations of the defining and applied occurrences in the source text. We will cover each in the remainder of this chapter.

Access rules

As language designers, we can solve the problem of `clash.nl' by introducing a private directive into NameLan. Any variable, method, or class can be marked as being private to the containing package:

Phrase structure[124]==

Declaration: 'private' VarDecl.
Declaration: 'private' MethodDecl.
Declaration: 'private' ClassDecl.
This macro is defined in definitions 2, 25, 30, 32, 41, 50, 55,
   67, 76, 97, 118, and 124.
This macro is invoked in definition 3.

The following access rules describe the effect of a private directive:

  • Let binding `(i,k)' be associated with a defining occurrence of `i' located in package `P' and marked private. An applied occurrence of `i' that is not located in package `P' cannot identify `(i,k)'.

  • Let binding `(i,k)' be associated with a defining occurrence of `i' located in class `A' within package `P' and marked private. Let `C' be a subclass of `A'. An applied occurrence of `i' within the body of `C' cannot identify `(i,k)' if `C' or any class `B' that is a superclass of `C' and a subclass of `A' is not located in package `P'.
Here's how a private directive is used to avoid the ambiguity that occurred in `clash.nl':

private.nl[125]==

import fsm.*;
import random.*;
{ int count = 0;
  state = 0;
  while (state != 7) {
    if (ran() < 0.25) advance(1);
    else if (ran() < 0.8) advance(2);
    else advance(3);
    count = count + 1;
  }
}

package fsm;
int state;
void advance(int input) {
  if (input == 1) state = state - 1;
  else if (input == 2) state = state + 1;
}

package random;
private int state = 100001;
float ran() {
  state = state * 125;
  state = state - (state / 2796203) * 2796203;
  return state / 2796203.0;
}
This macro is attached to a non-product file.

The access rules describing the private directive have no effect on the structure of the generic lookup algorithm; there are still two available bindings in `private.nl' for the applied occurrence of state on line 4. In order to handle access rules, the generic lookup algorithm invokes one of three functions whenever it finds a binding at a scope graph node n (see Is the binding acceptable? of Name Analysis Reference Manual). Which of the three is invoked depends on the context in which the search was undertaken and the state of the search:

isAcceptableSimple(DefTableKey def, DefTableKey app)
is invoked in a search for a simple identifier when n is the initial node or a node reached by following a parent edge.

isAcceptableQualified(DefTableKey def, DefTableKey app)
is invoked in a search for a qualified identifier when n is the initial node.

isAcceptablePath(DefTableKey def, DefTableKey app, int kind)
is invoked in a search for a simple identifier or a qualified identifier when n is the tip of a path edge with label kind.
The def argument is the available binding, and app is the UseKey of the applied occurrence (see Applied Occurrences of Name Analysis Reference Manual).

A default version of each of these functions, which finds any binding acceptable, is available in the library; the appropriate default version will be used unless the developer overrides it by specifying a C-coded function. As developers, we implement the access rules describing the effect of a private directive by providing appropriate implementations for these functions.

Recall that the first two arguments to these functions are definition table keys. Those keys must have properties that will allow the functions to make the appropriate decision (see The Definition Table Module of Property Definition Language).

Properties and property computations[126]==

InPackage: DefTableKey; /* Enclosing package */
IsPrivate: int;         /* 1 if the available binding is private */
This macro is defined in definitions 79, 94, 126, 132, 143, and 148.
This macro is invoked in definition 80.

If a binding is found for a simple name in an initial node or a node reached by following a parent edge, then the applied occurrence lies in the range of the defining occurrence. The range of a defining occurrence does not cross a package boundary, so the binding will always be acceptable. We can therefore use the default isAcceptableSimple.

If the initial node is a qualifier, a binding found for the qualified identifier will not be acceptable if the applied occurrence is not in the same package as the defining occurrence. This condition is checked by the following computation, which is common to both isAcceptableQualified and isAcceptablePath:

Common private access check[127]==

if (!GetIsPrivate(def, 0)) return AcceptBinding;
defpkg = GetInPackage(def, NoKey);
apppkg = GetInPackage(app, NoKey);
if (defpkg != apppkg) return IgnoreSkipPath;
This macro is invoked in definitions 128 and 130.

If the available binding isn't private, then it is acceptable. If the available binding and the applied occurrence are not in the same package, then the binding is not acceptable. The function returns IgnoreSkipPath in that case because, although the binding is not acceptable, it hides any other bindings for the same identifier along the current search path (see Is the binding acceptable? of Name Analysis Reference Manual).

At the end of this computation, we know that the entity is private, and that its defining occurrence is in the same package as the applied occurrence. That information is sufficient to accept a binding for a qualified identifier at the initial node:

Check acceptability of a qualified identifier[128]==

int
isAcceptableQualified (DefTableKey def, DefTableKey  app)
{ DefTableKey defpkg, apppkg;
  Common private access check[127]
  return AcceptBinding;
}
This macro is invoked in definition 138.

Let's see how this works on a contrived example:

accessPack.nl[129]==

{ }

package P;
class A { private int x, y; } /* package access */
class B {
  class C extends A {
    void f() { x = y; } /* legal */
  }
  class D extends Q.F {
    void g() { x = A.y; } /* illegal x */
  }
}

package Q;
class F extends P.A {
  void h() { x = P.A.y; } /* illegal x, y */
}
This macro is attached to a non-product file.

First consider the qualified name A.y in method g. The generic lookup finds a binding for the qualified identifier y in the node for the members of class A, which is the initial node of the search. It therefore invokes isAcceptableQualified, and the arguments would have the following properties:

IsPrivate attribute of def = 1
InPackage attribute of def = P
InPackage attribute of app = P
You should verify that the result would be AcceptBinding.

Now consider the qualified name P.A.y in method h. The generic lookup finds the same binding for the qualified identifier y, but this time the InPackage property of the app argument is Q. You should verify that isAcceptableQualified would return IgnoreSkipPath in this case.

All of the applied occurrences of x in `accessPack.nl' must have their available bindings checked by isAcceptablePath because each of those bindings is found at the tip of an inheritance edge. The binding for the applied occurrence of x in method h can be rejected by the common private access check, but that check will neither accept nor reject the bindings for the applied occurrences in method f or method g.

Consider the applied occurrence of x in method g of class D. The generic search will find the available binding in class A of package P, and that binding is marked private. D is a subclass of F, and F is a subclass of A. Class F does not lie in package P. According to the scope rules of NameLan, this means that the applied occurrence of x in method g cannot identify the binding in class A.

An inheritance path consists of a sequence of scope graph nodes owned by classes. The scope graphs module provides a function, CheckPathsNsp, that visits every node in the path defined by a starting node, an ending node, and a kind of path edge (see Useful graph operations of Name Analysis Reference Manual). The fourth argument of CheckPathsNsp is a user-defined function that CheckPathsNsp invokes at each node visit, passing the definition table key of the owner of the node being visited.

In order to verify accessibility of a binding that is neither accepted nor rejected by the common private access check, isAcceptablePath must verify that all nodes of the inheritance path are owned by classes within the package containing its declaration:

Check acceptability at the tip of a path edge[130]==

int
isAcceptablePath (DefTableKey def, DefTableKey  app, int lab)
{ DefTableKey defpkg, apppkg;
  Common private access check[127]
  pkg = defpkg; /* defpkg is set by the Common private access check */
  if (!CheckPathsNsp(
        GetInClassNode(app, NoNodeTuple),
        GetInClassNode(def, NoNodeTuple),
        lab,
        ChkClass))
    return IgnoreSkipPath;
  return AcceptBinding;
}
This macro is invoked in definition 138.

ChkClass is the function that CheckPathsNsp uses to verify that a class on the inheritance path is in the package containing the declaration. It requires a global variable pkg, which must be set before the path is scanned:

Call-back function to verify inheritance paths[131]==

int
ChkClass(DefTableKey cls)
{ if (GetInPackage(cls, NoKey) == pkg) return 1;
  return 0;
}
This macro is invoked in definition 138.

In order to specify the beginning of the inheritance path for a particular binding, we need to know the scope graph node owned by the class containing the applied occurrence of the identifier being sought; the end of that inheritance path is the scope graph node containing the binding. The necessary information can be provided by an InClassNode property for each identifier occurrence:

Properties and property computations[132]==

InClassNode: NodeTuplePtr;
This macro is defined in definitions 79, 94, 126, 132, 143, and 148.
This macro is invoked in definition 80.

Let us now consider how the properties that we have defined are set. We will use the class symbol DeclContext to abstract the two contexts, Declaration and DeclStmt, in which entities are declared. A computation can then reach up to the enclosing DeclContext for an attribute, private, indicating whether there was a private directive. In most cases there will be no such directive, so we provide a default computation for DeclContext.private:

Abstract syntax tree[133]==

SYMBOL DeclContext: private: int;
SYMBOL DeclContext COMPUTE SYNT.private = 0; END;

SYMBOL Declaration INHERITS DeclContext END;
SYMBOL DeclStmt    INHERITS DeclContext END;

RULE: Declaration ::= 'private' VarDecl COMPUTE
  Declaration.private = 1;
END;

RULE: Declaration ::= 'private' MethodDecl COMPUTE
  Declaration.private = 1;
END;

RULE: Declaration ::= 'private' ClassDecl COMPUTE
  Declaration.private = 1;
END;
This macro is defined in definitions 10, 18, 22, 38, 47, 52, 53,
   65, 69, 71, 88, 89, 92, 93, 95, 96, 101, 105, 108, 110, 111,
   112, 113, 122, 133, 134, 135, 136, 137, 144, and 153.
This macro is invoked in definition 11.

For every defining occurrence that might be modified by a private directive, we need to set the IsPrivate property of the Key. To avoid duplication, we will define the necessary computation in a class symbol DeclaredName and use LIDO inheritance:

Abstract syntax tree[134]==

SYMBOL VarDefName    INHERITS DeclaredName END;
SYMBOL MethodDefName INHERITS DeclaredName END;
SYMBOL ClassDefName  INHERITS DeclaredName END;

SYMBOL DeclaredName COMPUTE
  SYNT.GotDefKeyProp +=
    ResetIsPrivate(THIS.Key, INCLUDING DeclContext.private);
END;
This macro is defined in definitions 10, 18, 22, 38, 47, 52, 53,
   65, 69, 71, 88, 89, 92, 93, 95, 96, 101, 105, 108, 110, 111,
   112, 113, 122, 133, 134, 135, 136, 137, 144, and 153.
This macro is invoked in definition 11.

Note the use of an accumulating computation for the void attribute GotDefKeyProp (see Accumulating Computations of LIDO -- Reference Manual). This guarantees that the IsPrivate property is set before it can be used (see Information access of Name Analysis Reference Manual).

A computation at each declaration sets the InPackage property of the entity being declared to the key representing the package containing that declaration (see Libraries).

Abstract syntax tree[135]==

SYMBOL DeclaredName COMPUTE
  SYNT.GotDefKeyProp +=
    ResetInPackage(
      THIS.Key,
      INCLUDING (PackageBody.ScopeKey, Collection.ScopeKey));
END;
This macro is defined in definitions 10, 18, 22, 38, 47, 52, 53,
   65, 69, 71, 88, 89, 92, 93, 95, 96, 101, 105, 108, 110, 111,
   112, 113, 122, 133, 134, 135, 136, 137, 144, and 153.
This macro is invoked in definition 11.

Four symbols in our abstract syntax represent applied occurrences. In every one of these four contexts we need to set the InPackage property of the UseKey to the key representing the package containing the applied occurrence. To avoid duplication, we define the necessary computation in a class symbol AppliedName and use LIDO inheritance:

Abstract syntax tree[136]==

SYMBOL AppliedName COMPUTE
  SYNT.GotUseKeyProp +=
    ResetInPackage(
      THIS.UseKey,
      INCLUDING (PackageBody.ScopeKey, Collection.ScopeKey));
END;

SYMBOL SimpleName       INHERITS AppliedName END;
SYMBOL QualifiedId      INHERITS AppliedName END;
SYMBOL SimpleWLName     INHERITS AppliedName END;
SYMBOL QualifiedWLId    INHERITS AppliedName END;
This macro is defined in definitions 10, 18, 22, 38, 47, 52, 53,
   65, 69, 71, 88, 89, 92, 93, 95, 96, 101, 105, 108, 110, 111,
   112, 113, 122, 133, 134, 135, 136, 137, 144, and 153.
This macro is invoked in definition 11.

The computations to set the InClassNode property of the keys for the bindings and applied occurrences are similar to the computations setting the InPackage property. In the case of the InClassNode property, however, both defining and applied occurrences can lie outside of class definitions.

Abstract syntax tree[137]==

SYMBOL DeclaredName COMPUTE
  SYNT.GotDefKeyProp +=
    ResetInClassNode(
      THIS.Key,
      INCLUDING (ClassBody.Env, Collection.Env));
END;

SYMBOL AppliedName COMPUTE
  SYNT.GotUseKeyProp +=
    ResetInClassNode(
      THIS.UseKey,
      INCLUDING (ClassBody.Env, Collection.Env));
END;
This macro is defined in definitions 10, 18, 22, 38, 47, 52, 53,
   65, 69, 71, 88, 89, 92, 93, 95, 96, 101, 105, 108, 110, 111,
   112, 113, 122, 133, 134, 135, 136, 137, 144, and 153.
This macro is invoked in definition 11.

The C functions must be combined and the global variable pkg provided:

AccCtl.c[138]==

#include "LangSpecFct.h"
#include "err.h"

DefTableKey pkg;

Check acceptability of a qualified identifier[128]
Call-back function to verify inheritance paths[131]
Check acceptability at the tip of a path edge[130]

This macro is attached to a product file.

`AccCtl.c' is a specification file:

Specification files[139]==

AccCtl.c
This macro is defined in definitions 12, 16, 23, 27, 74, 78, 81,
   91, 117, 121, 139, 142, 146, and 152.
This macro is invoked in definition 13.

Exercises

These exercises are based on files defined in the Tutorial. To obtain copies of those files in your current directory, enter Eli and give the following command:

-> $elipkg/Name/LearnSG%AccCtl > .

None of these files will have write permission in your current directory. You will need to add write permission in order to do the exercises.

  1. Consider the role of the DeclStmt context in the NameLan language design.

    1. Do you think that it would be reasonable to use a private directive in this context? Explain briefly.

    2. If a private directive would never be used in the DeclStmt context, why do we bother to have DeclStmt inherit DeclContext? (Hint: try deleting that inheritance and building a processor.)

  2. If a qualifier is the initial node, why is a binding found for the qualified identifier acceptable if the applied occurrence is in the same package as the defining occurrence?

  3. Draw the scope graph for `accessPack.nl', and highlight the inheritance path for the applied occurrence of x in method g.

  4. Use `Bindings.specs' to generate a processor named acc that will show bindings for applied occurrences.

    1. Apply acc to `private.nl' and verify the bindings.

    2. Apply acc to `accessPack.nl' and verify the bindings.

  5. Add the following class to package P of `accessPack.nl':

    class E {
      void i() { Q.F.x = A.y; }
    }
    

    1. List the available bindings for the applied occurrence x in method i. Can this applied occurrence identify any of these bindings? Explain your answer using the scope rules of NameLan.

    2. Which of the isAcceptable functions will be called by the generic lookup for each of the bindings in (a)? Explain briefly.

    3. Briefly explain how the isAcceptable function reaches its decision in each case.

    4. Apply acc to the modified file and verify the predicted behavior.

Position control

A method body or block consists of a sequence of statements and variable declarations. The rules of NameLan only allow a reference to a variable declared in such a sequence to take the form of a simple name; it cannot be inherited or used in a qualified name. This means that there is no mechanism by which the variable can be accessed outside of the method body or block in which it was declared. We therefore call these variables local variables.

Local variable declarations may appear at arbitrary locations in a method body or block. How should we interpret their meaning in relation to each other and to the statements in that method body or block? The programmer might well consider the variable name to be unknown before its declaration. As language designers, we could formalize that intuition:

  • The scope of a defining occurrence VarDefName in a DeclStmt phrase begins at the VarDefName and ends at the end of the smallest enclosing Block.

  • No two defining occurrences VarDefName of the same identifier may have scopes that end at the same point.
Note that the first rule applies only to the special case of a local variable. All other VarDefName occurrences still obey the scope rules stated in earlier chapters. The second rule applies to all VarDefName occurrences. It prevents multiple variable declarations in a range (see Error reporting).

If a scope ends at the end of a Block phrase, the smallest abstract syntax subtree encompassing that scope is the corresponding Block subtree. That subtree can be chosen as the range of the scope, just as it would be the range of a VarDefName scope defined by the rules of the kernel language (see Basic Scope Rules of Name analysis according to scope rules). This means that the attribute computation seeking an available binding is used for all applied occurrences in the Block subtree. However, if the available binding is for a local variable, then it may not be acceptable due to the position of the applied occurrence. Here's a trivial example to show how the developer can use position control to implement our new scope rule:

cscope.nl[140]==

int i;
{ i = -42;
  float i;
  i = 42;
}
This macro is attached to a non-product file.

There are two variables named i in `cscope.nl'. The scope of the floating-point variable declared on the third line begins at the end of that line, and ends at the closing brace on the fifth line. Thus the applied occurrence on the second line does not have the same meaning as the applied occurrence on the fourth line.

The search for a binding for the applied occurrence on the second line begins in the scope graph node for the block. That scope graph node contains a defining occurrence on the third line, which must be checked for acceptability. This binding is not acceptable because the applied occurrence does not follow the defining occurrence in the text, and is therefore not in the scope of that defining occurrence. The search must continue in the scope node for the complete text, where an acceptable binding is available.

The search for a binding for the applied occurrence on the fourth line also begins in the scope graph node for the program block. In this case the binding is acceptable, because the applied occurrence follows the defining occurrence in the text and is therefore in the scope of that defining occurrence.

In order to compare positions of defining and applied occurrences, we use values of type CoordPtr (see Source Text Coordinates and Error Reporting of The Eli Library). By default, the CoordPtr value specifies the text line number and column index at which the phrase begins.

Module computations set the Coord property of every IdDefScope.Key attribute to the CoordPtr value for the phrase rooted in the IdDefScope node (see Defining Occurrences of Name Analysis Reference Manual). Similar module computations set the Coord property of every UseKey to the CoordPtr value for its phrase (see Applied Occurrences of Name Analysis Reference Manual). We can override the default version of isAcceptableSimple with a version that applies the function earlier to the Coord properties of the keys for the defining and applied occurrences of a local variable to check whether the binding found by the generic lookup is acceptable:

PosCtl.c[141]==

#include "LangSpecFct.h"

int
isAcceptableSimple (DefTableKey def, DefTableKey app)
{ if (GetIsLocal(def, 0)) {
    CoordPtr defcoord = GetCoord(def, NoPosition);
    CoordPtr appcoord = GetCoord(app, NoPosition);
    if (!earlier(defcoord, appcoord)) return IgnoreContinue;
  }
  return AcceptBinding;
}
This macro is attached to a product file.

See Source Text Coordinates and Error Reporting of The Eli Library, for the details of the earlier function.

`PosCtl.c' is an additional specification:

Specification files[142]==

PosCtl.c
This macro is defined in definitions 12, 16, 23, 27, 74, 78, 81,
   91, 117, 121, 139, 142, 146, and 152.
This macro is invoked in definition 13.

IsLocal is a property indicating that the entity is a local variable (see The Definition Table Module of Property definition language).

Properties and property computations[143]==

IsLocal: int;
This macro is defined in definitions 79, 94, 126, 132, 143, and 148.
This macro is invoked in definition 80.

Here are computations that will establish the value of the IsLocal property of the key of a local variable:

Abstract syntax tree[144]==

SYMBOL DeclContext: IsLocal: int;

SYMBOL Declaration COMPUTE SYNT.IsLocal=0; END;
SYMBOL DeclStmt    COMPUTE SYNT.IsLocal=1; END;

SYMBOL VarDefName COMPUTE
  SYNT.GotDefKeyProp +=
    ResetIsLocal(THIS.Key, INCLUDING DeclContext.IsLocal);
END;
This macro is defined in definitions 10, 18, 22, 38, 47, 52, 53,
   65, 69, 71, 88, 89, 92, 93, 95, 96, 101, 105, 108, 110, 111,
   112, 113, 122, 133, 134, 135, 136, 137, 144, and 153.
This macro is invoked in definition 11.

Exercises

These exercises are based on files defined in the Tutorial. To obtain copies of those files in your current directory, enter Eli and give the following command:

-> $elipkg/Name/LearnSG%PosCtl > .

None of these files will have write permission in your current directory.

  1. Use file `Bindings.specs' to generate a processor named `loc' that will show bindings of applied occurrences, and apply it to `cscope.nl', verifying that the bindings for the applied occurrences are correct in both cases.

  2. Program `cscope.nl' shows that, if a language allows the scope of a local variable to be a part of a block, it is possible for two applied occurrences of a particular variable identifier to have different meanings within that block. Many language designers consider this to be bad programming style. Suppose that you decide to make processor loc issue a message about this example of bad programming style (see Error Reporting of Frame Library Reference Manual).

    1. What severity would you use for that message? Explain briefly.

    2. Alter the specifications given in this section to issue such a message. Generate a new processor, rpt, from your modified specification. Test rpt on `cscope.nl'.

    3. Delete the first line of `cscope.nl' and apply rpt to the resulting program. Is the result what you expected? Do you think that the result is reasonable? Explain briefly.

  3. If you were not satisfied with rpt's error reporting, carefully consider the placement of the message call. Try to improve the result by saving some information in isAcceptableSimple and using it as a condition for invoking message in LookupComplete (see Initialization and finalization of Name Analysis Reference Manual).

    Test your improvements on both versions of `cscope.nl'.


Previous Chapter Next Chapter Table of Contents