package rebelsky.compiler.misc;

import java.util.Hashtable;
import java.util.Vector;
import rebelsky.compiler.lexer.Identifier;
import rebelsky.compiler.types.Type;

/**
 * A simple implementation of symbol tables for a Pascal or other
 * compiler.  In essence, a symbol table is a mechanism for 
 * associated attributes with symbols that permits nested scopes
 * (so that you can forget associations en masse).
 *
 * @author Samuel A. Rebelsky
 * @version 1.1.1 of April 2004
 */
public class SymbolTable
{
  // +----------------------+----------------------------------------------
  // | Implementation Notes |
  // +----------------------+

/*
  The symbol table is stored as a stack of hash tables.  The
  current scope is at the top of the stack, and the enclosing
  stacks are below that scope.

  An outermost scope is stored on the stack for safety (and
  so that you can use the symbol table without worrying about
  scope if you don't want to do so).

  Each symbol/attribute pair is turned into a key by building
  a pair.  This implementation may not be the most efficient,
  but it's fairly general (and probably more efficient and 
  correct than alternatives like string concatenation).
 */


  // +--------+------------------------------------------------------------
  // | Fields |
  // +--------+
  /**
   * The collection of hash tables used to implement this symbol table.
   */
  Vector contents;

  /**
   * The index into the vector that gives the current scope.
   */
  int scope;


  // +--------------+------------------------------------------------------
  // | Constructors |
  // +--------------+

  /**
   * Build a new symbol table.
   */
  public SymbolTable() {
    // Set up the collection of hash tables.
    contents = new Vector();
    // Add the first hash table to that collection
    contents.add(0, new Hashtable());
    // Note that we're at the outermost scope.
    scope = 0;
  } // SymbolTable()


  // +------------------------+--------------------------------------------
  // | Private Helper Methods |
  // +------------------------+

  /**
   * Turn an identifier and an attribute into a key.  Included as
   * a separate method so that it's easy to change the kinds of keys.
   */
  private static Object makeKey(Object sym, Object att)
  {
    return new Pair(sym,att);
  } // makeKey(String,String)

  
  // +---------------------------+-----------------------------------------
  // | General Attribute Methods |
  // +---------------------------+

  /**
   * Get a specified attribute of a symbol.  Returns null if the
   * attribute is not currently set.
   */
  public Object get(Object symbol, Object attribute)
  {
    // Build the key to use.
    Object key = makeKey(symbol,attribute);
    // Look through each scope
    for (int curScope = scope; curScope >= 0; curScope--) {
      Hashtable currentScope = ((Hashtable) contents.get(curScope));
      Object result = currentScope.get(key);
      if (result != null) return result;
    } // for(curScope)
    // If we've gotten this far, the symbol is not in any scope,
    // so give up.
    return null;
  } // get(Object,Object)
 
  /**
   * Check if a specified attribute of a symbol is set in the
   * current scope.
   */
  public boolean isSet(Object symbol, Object attribute)
  {
    // Build the key to use.
    Object key = makeKey(symbol,attribute);
    // Look up the current hash table.
    Hashtable currentScope = ((Hashtable) contents.get(scope));
    // If the key is in the hash table, the attribute is set.
    return currentScope.containsKey(key);
  } // isSet()

  /**
   * Set a specified attribute of a symbol in the current scope.  
   * Can only be used to set a new attribute.  If the attribute 
   * is already set in the current scope, throws an exception.  
   * Use update to change.
   *
   * @exception Exception
   *   If the attribute is already set.
   */
  public void set(Object symbol, Object attribute, Object value)
    throws Exception
  {
    // Build the key to use.
    Object key = makeKey(symbol,attribute);
    // Look up the current hash table.
    Hashtable currentScope = ((Hashtable) contents.get(scope));
    // See if the symbol/attribute pair is already in the table
    if (currentScope.containsKey(key)) {
      throw new Exception("symbol '" + symbol.toString() + "'"
                          + " already has the "
                          + "'" + attribute + "' attribute.");
    } // if the symbol/attribute pair is already in the table
    // Finally, set it.
    currentScope.put(key, value);
  } // set(Object,Object)

  /**
   * Update a specified attribute of a symbol in the current scope.  
   * Can only be used to set an existing attribute.  If the attribute 
   * is not yet set in the current scope, throws an exception.  
   * Use set to set the intial value.
   *
   * @exception Exception
   *   If the attribute is not yet set.
   */
  public void update(Object symbol, Object attribute, Object value)
    throws Exception
  {
    // Build the key to use.
    Object key = makeKey(symbol,attribute);
    // Look up the current hash table.
    Hashtable currentScope = ((Hashtable) contents.get(scope));
    // See if the symbol/attribute pair is already in the table
    if (!currentScope.containsKey(key)) {
      throw new Exception("symbol '" + symbol.toString() + "'"
                          + " lacks the "
                          + "'" + attribute + "' attribute.");
    }
    // Finally, set it.
    currentScope.put(key, value);
  } // update(Object,Object)


  // +---------------+-----------------------------------------------------
  // | Scope Methods |
  // +---------------+

  /**
   * Start a new scope.
   */
  public void beginScope()
  {
    // Note that we've started a new scope and add a new hashtable.
    contents.add(++scope, new Hashtable());
  } // beginScope()
  
  /**
   * End a scope.
   *
   * @exception Exception
   *   If there's no corresponding call to beginScope.
   */
  public void endScope()
    throws Exception
  {
    // Sanity check: Are we at the topmost scope?
    if (scope == 0) 
      throw new Exception("Attempt to end outermost scope.");
    // Remove the hash table and decrement our count of scopes.
    contents.remove(scope--);
  } // endScope()

  // +--------------+------------------------------------------------------
  // | Type Methods |
  // +--------------+

  /**
   * Associate a type with an identifier.
   *
   * @exception Exception
   *   If a type is already associated with the given identifier.
   */
  public void setType(Identifier id, Type type)
    throws Exception
  {
    this.set(id, "type", type);
  } // setType(Identifier, Type)

  /**
   * Determine the type associated with an identifier.
   * 
   * @exception Exception
   *   If the identifier is untyped.
   */
  public Type getType(Identifier id)
    throws Exception
  {
    Type type = (Type) this.get(id, "type");
    if (type == null)
      throw new Exception(id + " has no type");
    return type;
  } // getType(Identifier)

  // +---------------+-----------------------------------------------------
  // | Misc. Methods |
  // +---------------+

} // class SymbolTable

/**
 * A pair of objects.  Useful for many things (e.g., as part of a
 * list), but primarily used as a way of making compound keys.
 */
class Pair
{
  // +--------+------------------------------------------------------------
  // | Fields |
  // +--------+

  /** The first part of the pair. */
  Object car;

  /** The second part of the pair. */
  Object cdr;


  // +--------------+------------------------------------------------------
  // | Constructors |
  // +--------------+

  public Pair(Object car, Object cdr) {
    this.car = car;
    this.cdr = cdr;
  } // Pair(Object, Object)


  // +---------+-----------------------------------------------------------
  // | Methods |
  // +---------+

  /** Determine if this pair equals another object. */
  public boolean equals(Object other)
  {
    try { return equals((Pair) other); }
    catch (ClassCastException cce) { return false; }
  } // equals(Object)

  /** Determine if this pair equals another pair. */
  public boolean equals(Pair other)
  {
    return this.car.equals(other.car) && this.cdr.equals(other.cdr);
  } // equals(Pair)

  /** Compute the hash code of this combined object. */
  public int hashCode() 
  {
    // By averaging the hash codes, we're able to ensure that
    // the result is in the right range.  We divide before
    // adding to avoid overflow.
    return car.hashCode()/2 + cdr.hashCode()/2;
  } // hashCode()

  /** 
   * Convert to a string for printing or whatever. 
   * Currently uses the Scheme pair notation.  Might eventually be
   * updated to use the standard Scheme "print a list" notation.
   */
  public String toString() 
  {
    return "(" + car.toString() + " . " + cdr.toString() + ")";
  } // toString()
} // class Pair

