/*
 * @(#)BaseParser.java  2.5.0  18 May 2007
 *
 * Copyright 2007
 * College of Computer and Information Science
 * Northeastern University
 * Boston, MA  02115
 *
 * The Java Power Tools software may be used for educational
 * purposes as long as this copyright notice is retained intact
 * at the top of all source files.
 *
 * To discuss possible commercial use of this software, 
 * contact Richard Rasala at Northeastern University, 
 * College of Computer and Information Science,
 * 617-373-2462 or rasala@ccs.neu.edu.
 *
 * The Java Power Tools software has been designed and built
 * in collaboration with Viera Proulx and Jeff Raab.
 *
 * Should this software be modified, the words "Modified from 
 * Original" must be included as a comment below this notice.
 *
 * All publication rights are retained.  This software or its 
 * documentation may not be published in any media either
 * in whole or in part without explicit permission.
 *
 * This software was created with support from Northeastern 
 * University and from NSF grant DUE-9950829.
 */

package edu.neu.ccs.parser;

import edu.neu.ccs.*;
import edu.neu.ccs.util.*;
import java.text.ParseException;
import java.util.*;
import java.awt.geom.Point2D;
import java.math.*;

//debug
// import edu.neu.ccs.console.*;

/**
 * <p>The class <code>BaseParser</code> is the
 * base class for classes of objects that provide
 * functionality for evaluating strings into
 * primitive types and objects using a language
 * with a simple syntactic structure.</p>
 *
 * <p>Revisions in 2.5.0:</p>
 * 
 * <ul>
 *   <li>The name of this class has been changed from
 *       <code>AbstractParser</code> to
 *       <code>BaseParser</code> since the class is
 *       no longer an <code>abstract</code> class.</li>
 *   <li>We have refactored the <code>parse</code> method
 *       and other related algorithmic methods from the
 *       derived class <code>JPTParser</code> into this
 *       class. This is why this class is no longer an
 *       <code>abstract</code> class. Nevertheless this
 *       class has limited functionality since no constants,
 *       functions, or operations have been predefined. We
 *       have introduced various special forms: set, let, if,
 *       eval, and random.  We treat random as a special form
 *       in order to allow 0, 1, or 2 arguments.
 *   <li>We have added a method <code>parseWithArgumentList</code>
 *       to enable the parser to set local variables for use
 *       during the parse operation.</li>
 *   <li>The parse methods may be called recursively.  If
 *       an exception is thrown, the error message now
 *       accumulates context information.</li>
 *   <li>The parser now permits replacement of functions
 *       and operations by new definitions.</li>
 *   <li>We have refactored all static inner classes into
 *       separate class files.</li>
 *   <li>We have constrained constant and variable id's and
 *       function names to follow the rules for identifiers
 *       in Java and also to be distinct.</li>
 *   <li>We have constrained operation symbols to be created
 *       from a specific list of characters.</li>
 *   <li>We have changed critical member data from protected
 *       to private.</li>
 *   <li>We have added several public and protected methods
 *       and renamed a few methods to increase clarity.</li>
 *   <li>We have, in particular, added methods to call
 *       mathematical functions installed in the parser
 *       and to create tables of values for such functions.</li>
 *   <li>We have made many utility methods final but have
 *       kept algorithmic methods as protected and non-final
 *       so that they may be overridden in a derived class.</li>
 *   <li>We have improved code comments and error messages.</li>
 *   <li>We have fixed small bugs.</li>
 * </ul>
 * 
 * <p>Further comments:</p>
 * 
 * <p><code>BaseParser</code> contains all fundamental parsing algorithms
 * but has no installed abstract functions, operations, or constants.</p>
 * 
 * <p><code>BaseParser</code> has the ability to define <i>special forms</i>
 * and five such forms are introduced:
 * <code>set</code>,
 * <code>let</code>,
 * <code>if</code>,
 * <code>eval</code>,
 * and <code>random</code>.
 * Here is a brief synopsis of these special forms.</p>
 * 
 * <table border="1" cellpadding="5">
 *   <tr>
 *     <th class="centered">Special Form</th>
 *     <th class="centered">Typical Usage</th>
 *   </tr>
 *   
 *   <tr>
 *     <td><code>set</code></td>
 *     <td><code>set(identifier,expression)</code></td>
 *   </tr>
 *   
 *   <tr>
 *     <td><code>let</code></td>
 *     <td><code>let(identifier,expression)</code></td>
 *   </tr>
 *   
 *   <tr>
 *     <td><code>if</code></td>
 *     <td><code>if(test,expression-1,expression-2)</code></td>
 *   </tr>
 *   
 *   <tr>
 *     <td><code>eval</code></td>
 *     <td><code>eval(expression-1,...,expression-n)</code></td>
 *   </tr>
 *   
 *   <tr>
 *     <td><code>random</code></td>
 *     <td><code>random()</code> or <code>random(x)</code>
 *         or <code>random(x,y)</code></td>
 *   </tr>
 *   
 * </table>
 * 
 * <p>The special forms are interpreted as follows.</p>
 * 
 * <ul>
 *   <li><code>set</code> evaluates the expression and assigns it to
 *       the identifier in a persistant fashion that will be available
 *       across invocations of the parser.
 *       <br /><code>set</code> returns the
 *       evaluated expression as its value.</li>
 *   <li><code>let</code> evaluates the expression and assigns it to
 *       the identifier in a temporary fashion that will be available
 *       only in this invocation of the parser.
 *     <br /><code>let</code>
 *       returns the evaluated expression as its value.</li>
 *   <li><code>if</code> evaluates the test expression as a boolean.
 *       If the result is true,
 *       expression-1 is evaluated and returned
 *       and expression-2 is skipped.
 *       If the result is false,
 *       expression-2 is evaluated and returned
 *       and expression-1 is skipped.</li>
 *   <li><code>eval</code> evaluates all expressions from left to right
 *       and returns the value of the last expression.</li>
 *   <li><code>random()</code> returns a random double between 0 and 1.</li>
 * <li><code>random(x)</code> returns a random double between 0 and x. </li>
 *   <li><code>random(x,y)</code> returns a random double between x and y.</li>
 * </ul>
 * 
 * <p><code>random</code> is introduced as a <i>special form</i> since in
 * our simple parsing scheme ordinary functions cannot be overloaded to
 * take different numbers of arguments.</p>
 * 
 * @author  Richard Rasala
 * @author  Jeff Raab
 * @version 2.5
 */
public class BaseParser
    implements Parser
//  debug
//  ,ConsoleAware
{
    
    /** 
     * <p>The IDENTITY operation, equivalent to the function 
     * <I>f</I>(<I>x</I>,<I>y</I>)=<I>y</I>.</p>
     * 
     * <p>This operation is inserted with the lowest level of
     * precedence and is used as the base for evaluation of
     * an expression.</p>
     * 
     * <p>This object is identical to Operation.IDENTITY.</p>
     */
    public static final Operation IDENTITY =
        Operation.IDENTITY;
    
    /** 
     * <p>The singleton operation object which designates that
     * a symbol is a prefix for a known operation.</p>
     * 
     * <p>This operation operates as a return flag and may not
     * be inserted into the precedence structure of this class.</p>
     * 
     * <p>This object is identical to Operation.OPERATION_PREFIX.</p>
     */
    public static final Operation OPERATION_PREFIX =
        Operation.OPERATION_PREFIX;
    
    
    /** Value designating string data that has form of an integer number. */
    protected static final int INTEGRAL = 100;
    
    /** Value designating string data that has form of a floating number. */
    protected static final int FLOATING = 101;
    
    
    /** String token representing the start of a nested expression. */
    protected String NESTED_EXPRESSION_START = "(";
    
    /** String token representing the end of a nested expression. */
    protected String NESTED_EXPRESSION_END = ")";
    
    /** String token representing the start of an argument list. */
    protected String ARGUMENT_LIST_START = "(";
    
    /** String token representing the end of an argument list. */
    protected String ARGUMENT_LIST_END = ")";
    
    /** String token representing the radix point. */
    protected String RADIX_POINT = ".";
    
    /** String token representing the argument separator. */
    protected String ARGUMENT_SEPARATOR = ",";
    
    /** Char with UNDERSCORE character for identifiers. */
    protected char UNDERSCORE = '_';
    
    
    /**
     * The reserved keyword "set" to indicate variable assignment
     * that is persistent across invocations of the
     * <code>parse</code> method.
     */
    protected final String ASSIGNMENT_BY_SET = "set";
    
    /**
     * The reserved keyword "let" to indicate variable assignment
     * that is temporary, that is, one that affects only the
     * current invocation of the <code>parse</code> method.
     */
    protected final String ASSIGNMENT_BY_LET = "let";
    
    /** The reserved keyword "if" to indicate if-then-else. */
    protected final String IF_THEN_ELSE = "if";
    
    /**
     * The reserved keyword "eval" to indicate the special
     * function that evaluates its arguments from left to
     * right and then returns the value of the rightmost
     * argument.
     */
    protected final String EVAL = "eval";
    
    
    /**
     * <p>The reserved keyword "random" to indicate the special
     * function that takes 0, 1, or 2 numeric arguments and
     * has the following interpretation.</p>
     * 
     * <table border="1" cellpadding="5">
     *   <tr>
     *     <td style="text-align:center"><b>Function Name</b></td>
     *     <td style="text-align:center"><b>Typical Usage</b></td>
     *     <td style="text-align:center"><b>Interpretation</b></td>
     *   </tr>
     *   <tr>
     *     <td style="text-align:center;font-family:monospace">random</td>
     *     <td style="text-align:center;font-family:monospace">random()</td>
     *     <td>Random number between 0 and 1</td>
     *   </tr>
     *   <tr>
     *     <td style="text-align:center;font-family:monospace">random</td>
     *     <td style="text-align:center;font-family:monospace">random(x)</td>
     *     <td>Random number between 0 and x</td>
     *   </tr>
     *   <tr>
     *     <td style="text-align:center;font-family:monospace">random</td>
     *     <td style="text-align:center;font-family:monospace">random(x,y)</td>
     *     <td>Random number between x and y</td>
     *   </tr>
     * </table>
     */
    protected final String RANDOM = "random";
    
    
    /**
     * Table of reserved identifiers
     * for special functions or operations.
     */
    private Hashtable reserved = new Hashtable();
    
    /**
     * Table of constant identifiers
     * and their corresponding values. 
     */
    private Hashtable constants = new Hashtable();
    
    /**
     * Table of variable identifiers defined by "set"
     * and their corresponding values. 
     */
    private Hashtable set_variables = new Hashtable();
    
    /**
     * Table of variable identifiers defined by "let"
     * in the current invocation of
     * the <code>parse</code> method
     * and their corresponding values. 
     */
    private Hashtable let_variables = new Hashtable();
    
    /** 
     * Table of function names 
     * and their corresponding functions. 
     */
    private Hashtable functions = new Hashtable();

    /** 
     * Table of operation symbols 
     * and their corresponding operations. 
     */    
    private Hashtable operations = new Hashtable();
    
    /**
     * Table of prefixes of operation symbols.
     */
    private Hashtable prefixes = new Hashtable();
    
    /** 
     * List of hashtables storing the precedence relationship 
     * between operations.  Operations with the same precedence
     * are placed in the same hashtable.  The list is ordered
     * from lowest precedence to highest.
     */
    private Vector precedence = new Vector();
    
    /**
     * The vector that operates as a stack to save and restore
     * the parser context in each call of <code>parse</code>.
     */
    private Vector parserContextStack = new Vector();
    
    /**
     * The current String to be evaluated
     * by the current call of <code>parse</code>.
     */
    protected String data = "";
    
    /** 
     * The index of the next character to examine
     * in the current String to be evaluated
     * by the current call of <code>parse</code>.
     */
    protected int next = 0;
    
    /**
     * <p>The member data to control whether the parser should be
     * evaluating operations, functions, and special functions
     * or should rather be parsing formally in order to extract
     * the next expression that is to be parsed as a string.</p>
     * 
     * <p>The default value is <code>0</code>.</p>
     * 
     * <p>A value of <code>0</code> or negative signals that
     * the parser should proceed to evaluate.</p>
     * 
     * <p>A positive value signals that
     * the parser should suspend evaluation and parse formally.</p>
     * 
     * <p>Normally, methods that wish to suspend evaluation should
     * increment suspend on entry and decrement suspend prior to
     * exit. This protocol will make it easy for such methods to
     * be recursive and to interact well with similar methods.</p>
     */
    protected int suspend = 0;
    
    
    /////////////////
    // Constructor //
    /////////////////
    
    /**
     * <p>Constructs a new parser by initializing structures
     * and by adding the standard operations, functions,
     * and constants available for this parser.</p>
     * 
     * <p>Calls the following 5 methods in turn:</p>
     * 
     * <ul>
     *  <li><code>initializeStructures()</code></li>
     *  <li><code>addReserved()</code></li>
     *  <li><code>addConstants()</code></li>
     *  <li><code>addFunctions()</code></li>
     *  <li><code>addOperations()</code></li>
     * </ul>
     * 
     * <p>Normally the last 3 methods should be overridden
     * in a derived class.</p>
     */
    public BaseParser() {
        initializeStructures();
        addReserved();
        addConstants();
        addFunctions();
        addOperations();
    }
    
    
    ////////////
    // Parser //
    ////////////
    
    /**
     * <p>Parses the given string d and 
     * returns the <code>Object</code> it represents,
     * given this parsing scheme.</p>
     * 
     * <p>This implementation proceeds as follows.</p>
     * 
     * <p>Checks that the data is not <code>null</code>.</p>
     * 
     * <p>To enable recursive calls of this method, pushes
     * the current parsing context on a stack.</P>
     * 
     * <p>Calls the recursive method
     * <code>parseExpression</code> together with a default
     * <code>ObjectOperationPair</code> to initiate the
     * parsing operation on this data.</p>
     * 
     * <p>Checks for errors:</p>
     * 
     * <p>Requires the value returned by the call to
     * <code>parseExpression</code> to be
     * non-<code>null</code>.</p>
     * 
     * <p>Requires that the parse operation consumed
     * all of the characters in the data.</p>
     * 
     * <p>Restores the original parsing context.</p>
     * 
     * <p>Returns the value returned by the call to
     * <code>parseExpression</code>.</p>
     *
     * <p>Implementation note: This method simply
     * calls <code>parseWithArgumentList</code>
     * with the last 2 parameters <code>null</code>.</p>
     * 
     * @param d the <CODE>String</CODE> to be parsed
     * @throws ParseException if the data is malformed
     */
    public final Object parse(String d) 
        throws ParseException 
    {
        return parseWithArgumentList(d, null, null);
    }

    
    /**
     * <p>Parses the given string d in the context of
     * a symbolic argument list and associated values
     * and returns the <code>Object</code> represented
     * by the string d given this parsing scheme.</p>
     * 
     * <p>This implementation proceeds as follows.</p>
     * 
     * <p>To enable recursive calls of this method, pushes
     * the current parsing context on a stack.</P>
     * 
     * <p>Checks that the data is not <code>null</code>.</p>
     * 
     * <p>Assigns the values to corresponding ids.  Will
     * ignore the lists ids and values if both lists are
     * <code>null</code>.</p>
     * 
     * <p>Calls the recursive method
     * <code>parseExpression</code> together with a default
     * <code>ObjectOperationPair</code> to initiate the
     * parsing operation on this data.</p>
     * 
     * <p>Checks for errors.</p>
     * 
     * <p>Requires the value returned by the call to
     * <code>parseExpression</code> to be
     * non-<code>null</code>.</p>
     * 
     * <p>Requires that the parse operation consumed
     * all of the characters in the data.</p>
     * 
     * <p>Restores the original parsing context.</p>
     * 
     * <p>Returns the value returned by the call to
     * <code>parseExpression</code>.</p>
     *
     * @param d the string data to be parsed
     * @param ids the argument list ids
     * @param values the argument list values
     * @throws ParseException if the data is malformed
     */
    public final Object parseWithArgumentList
        (String d, String[] ids, Object[] values) 
        throws ParseException 
    {
        // error message if exception is trapped
        try {
            // push the current context and
            // initialize a new context
            // to parse the string d
            pushContext(d);
            
            // sanity check for null data
            if (d == null)
                throw new ParseException("Data to parse was null", -1);
            
            // assign arguments
            assignArgumentList(ids, values);
            
             // evaluate the expression
            Object value = parseExpression();
            
            // if value is null report error
            if (value == null)
                throw new ParseException("Expected expression", next);
            
            // if there is more data to parse report error
            if (next < data.length())
                throw new ParseException("Expected end of expression", next);
            
            // restore the previous context
            popContext();
            
            return value;
        }
        catch (Throwable ex) {
            throwAgainAndPop(ex.getMessage(), d);
            
            return null; // never reached
        }
    }
    
    
    ////////////////
    // Public API //
    ////////////////
    
    /**
     * <p>Call the function with the given name
     * that is installed in this parser
     * using the given double data as arguments
     * and return a double value.<p>
     * 
     * <p>The function should in effect
     * expect doubles as arguments
     * and should return a number.</p>
     * 
     * <p>The length of the arguments array must
     * equal the number of arguments expected by
     * the function.</p>
     * 
     * <p>Throws <code>ParseException</code> if
     * an error occurs.</p>
     * 
     * @param name the name of the function
     *        installed in this parser
     * @param arguments the arguments given
     *        as an array of double
     * @throws ParseException
     */
    public final double call(String name, double[] arguments)
        throws ParseException
    {
        String message = null;
        
        if (name == null) {
            message = "Null name passed to call";
            throw new ParseException(message, 0);
        }
        
        AbstractFunction f = getFunction(name);
        
        if (f == null) {
            message = "Function " + name
                + " is not installed in this parser";
            
            throw new ParseException(message, 0);
        }
        
        return call(f, arguments);
    }
    
    
    /**
     * <p>Call the function with the given name
     * that is installed in this parser
     * using the given double x as the argument
     * and return a double value.<p>
     * 
     * <p>The function should in effect
     * expect 1 double as an argument
     * and should return a number.</p>
     * 
     * <p>Throws <code>ParseException</code> if
     * an error occurs.</p>
     * 
     * @param name the name of the function
     *        of 1 argument
     *        installed in this parser
     * @param x argument 1
     * @throws ParseException
     */
    public final double call(String name, double x)
        throws ParseException
    {
        return call(name, new double[] {x} );
    }
    
    
    /**
     * <p>Call the function with the given name
     * that is installed in this parser
     * using the given doubles x,y as arguments
     * and return a double value.<p>
     * 
     * <p>The function should in effect
     * expect 2 doubles as an arguments
     * and should return a number.</p>
     * 
     * <p>Throws <code>ParseException</code> if
     * an error occurs.</p>
     * 
     * @param name the name of the function
     *        of 2 arguments
     *        installed in this parser
     * @param x argument 1
     * @param y argument 2
     * @throws ParseException
     */
    public final double call(String name, double x, double y)
        throws ParseException
    {
        return call(name, new double[] {x, y} );
    }


    /**
     * <p>Call the function with the given name
     * that is installed in this parser
     * using the given doubles x,y,z as arguments
     * and return a double value.<p>
     * 
     * <p>The function should in effect
     * expect 3 doubles as an arguments
     * and should return a number.</p>
     * 
     * <p>Throws <code>ParseException</code> if
     * an error occurs.</p>
     * 
     * @param name the name of the function
     *        of 3 arguments
     *        installed in this parser
     * @param x argument 1
     * @param y argument 2
     * @param z argument 3
     * @throws ParseException
     */
    public final double call(String name, double x, double y, double z)
        throws ParseException
    {
        return call(name, new double[] {x, y, z} );
    }


    /**
     * <p>Call the function with the given name
     * that is installed in this parser
     * using the given doubles x,y,z,w as arguments
     * and return a double value.<p>
     * 
     * <p>The function should in effect
     * expect 4 doubles as an arguments
     * and should return a number.</p>
     * 
     * <p>Throws <code>ParseException</code> if
     * an error occurs.</p>
     * 
     * @param name the name of the function
     *        of 4 arguments
     *        installed in this parser
     * @param x argument 1
     * @param y argument 2
     * @param z argument 3
     * @param w argument 4
     * @throws ParseException
     */
    public final double call
        (String name, double x, double y, double z, double w)
        throws ParseException
    {
        return call(name, new double[] {x, y, z, w} );
    }
    
    
    /**
     * <p>Views the given String as a comma separated
     * list of names of <code>AbstractFunction</code>
     * objects installed in this parser;
     * for each such function object f,
     * makes an XPoint2D array of pairs (x,f(x))
     * where x is sampled on the given interval
     * divided into the given number of divisions;
     * all such arrays are collected and returned in
     * a 2-dimensional array.</p>
     *
     * <p>If names is <code>null</code>,
     * returns <code>null</code>.</p>
     * 
     * <p>Otherwise, assume that the string names
     * contains N function names for some N&gt;=0.</p>
     * 
     * <p>If divisions is less than 1, it will be set
     * to 1.</p>
     * 
     * <p>The 2 dimensions of the output array
     * will be N by (divisions+1).</p>
     *
     * <p>Throws <code>ParseException</code> if
     * an error occurs.</p>
     * 
     * <p>See also the <code>makeTable</code> methods in
     * <code>DataTables2D</code>.</p>
     * 
     * @param names     a comma separated list of names
     *                  of functions installed in this parser
     * @param endpointA an endpoint of the interval
     * @param endpointB an endpoint of the interval
     * @param divisions the number of subdivisions of the interval
     * @return the 2-dimensional array of (x,f(x)) pairs
     * @throws ParseException
     */
    public final XPoint2D[][] makeTable
        (String names,
         double endpointA,
         double endpointB,
         int divisions)
        throws ParseException
    {
        if (names == null)
            return null;
        
        String message = null;
        
        String[] list = Strings.splitCommaList(names);
        
        int N = list.length;
        
        XPoint2D[][] result = new XPoint2D[N][];
        
        if (divisions < 1)
            divisions = 1;
        
        for (int i = 0; i < N; i++) {
            AbstractFunction f = getFunction(list[i]);
            
            if (f == null) {
                message = "Function " + list[i]
                    + " is not installed in this parser";
                
                throw new ParseException(message, 0);
            }
            
            result[i] =
                makeTable(f, endpointA, endpointB, divisions);
        }
        
        return result;
    }
    
    
    /**
     * <p>Views the given String as a comma separated
     * list of names of <code>AbstractFunction</code>
     * objects installed in this parser;
     * for each such function object f,
     * makes an XPoint2D array of pairs (x,f(x))
     * where x is sampled on the given interval
     * divided into the given number of divisions;
     * all such arrays are collected and returned in
     * a 2-dimensional array.</p>
     *
     * <p>If names is <code>null</code>
     * or limits is <code>null</code>,
     * returns <code>null</code>.</p>
     * 
     * <p>Otherwise, assume that the string names
     * contains N function names for some N&gt;=0.</p>
     * 
     * <p>If divisions is less than 1, it will be set
     * to 1.</p>
     * 
     * <p>The 2 dimensions of the output array
     * will be N by (divisions+1).</p>
     *
     * <p>Throws <code>ParseException</code> if
     * an error occurs.</p>
     * 
     * <p>See also the <code>makeTable</code> methods in
     * <code>DataTables2D</code>.</p>
     * 
     * @param names     a comma separated list of names
     *                  of functions installed in this parser
     * @param limits    the interval
     * @param divisions the number of subdivisions of the interval
     * @return the 2-dimensional array of (x,f(x)) pairs
     * @throws ParseException
     */
    public final XPoint2D[][] makeTable
        (String names,
         XInterval limits,
         int divisions)
        throws ParseException
    {
        if (limits == null)
            return null;
        
        double endpointA = limits.getMinimum();
        double endpointB = limits.getMaximum();
        
        return makeTable(names, endpointA, endpointB, divisions);
    }
    
    
    /**
     * <p>Call the given abstract function
     * using the given double data as arguments
     * and return a double value.<p>
     * 
     * <p>The function should in effect
     * expect doubles as arguments
     * and should return a number.</p>
     * 
     * <p>The length of the arguments array must
     * equal the number of arguments expected by
     * the function.</p>
     * 
     * <p>Throws <code>ParseException</code> if
     * an error occurs.</p>
     * 
     * @param f the abstract function
     * @param arguments the arguments given
     *        as an array of double
     * @throws ParseException
     */
    public static double call
        (AbstractFunction f, double[] arguments)
        throws ParseException
    {
        String message = null;
        
        if (f == null) {
            message = "Null AbstractFunction passed to call";
            
            throw new ParseException(message, 0);
        }
        
        if (arguments == null) {
            message = "Null arguments passed to call";
            throw new ParseException(message, 0);
        }
        
        String name = f.name();
        int args = f.arguments();
        int size = arguments.length;
        
        if (args != size) {
            message = "Function " + name
                + " requires "     + args + " arguments"
                + " but provided " + size + " arguments";
            
            throw new ParseException(message, 0);
        }
        
        XDouble[] data = new XDouble[size];
        
        for (int i = 0; i < size; i++)
            data[i] = new XDouble(arguments[i]);
        
        Object result = f.functionCall(data);
        
        if (! (result instanceof XNumber)) {
            message = "Function " + name
                + " failed to return a numeric value";
            
            throw new ParseException(message, 0);
        }
        
        XNumber v = (XNumber) result;
        
        return v.doubleValue();
    }
    
    
    /**
     * <p>Call the given abstract function
     * using the given double x as the argument
     * and return a double value.<p>
     * 
     * <p>The function should in effect
     * expect 1 double as an argument
     * and should return a number.</p>
     * 
     * <p>Throws <code>ParseException</code> if
     * an error occurs.</p>
     * 
     * @param f the abstract function
     *        of 1 argument
     * @param x argument 1
     * @throws ParseException
     */
    public static double call
        (AbstractFunction f, double x)
        throws ParseException
    {
        return call(f, new double[] {x} );
    }
    
    
    /**
     * <p>Call the given abstract function
     * using the given doubles x,y as arguments
     * and return a double value.<p>
     * 
     * <p>The function should in effect
     * expect 2 doubles as an arguments
     * and should return a number.</p>
     * 
     * <p>Throws <code>ParseException</code> if
     * an error occurs.</p>
     * 
     * @param f the abstract function
     *        of 2 arguments
     * @param x argument 1
     * @param y argument 2
     * @throws ParseException
     */
    public static double call
        (AbstractFunction f, double x, double y)
        throws ParseException
    {
        return call(f, new double[] {x, y} );
    }


    /**
     * <p>Call the given abstract function
     * using the given doubles x,y,z as arguments
     * and return a double value.<p>
     * 
     * <p>The function should in effect
     * expect 3 doubles as an arguments
     * and should return a number.</p>
     * 
     * <p>Throws <code>ParseException</code> if
     * an error occurs.</p>
     * 
     * @param f the abstract function
     *        of 3 arguments
     * @param x argument 1
     * @param y argument 2
     * @param z argument 3
     * @throws ParseException
     */
    public static double call
        (AbstractFunction f, double x, double y, double z)
        throws ParseException
    {
        return call(f, new double[] {x, y, z} );
    }


    /**
     * <p>Call the given abstract function
     * using the given doubles x,y,z,w as arguments
     * and return a double value.<p>
     * 
     * <p>The function should in effect
     * expect 4 doubles as an arguments
     * and should return a number.</p>
     * 
     * <p>Throws <code>ParseException</code> if
     * an error occurs.</p>
     * 
     * @param f the abstract function
     *        of 4 arguments
     * @param x argument 1
     * @param y argument 2
     * @param z argument 3
     * @param w argument 4
     * @throws ParseException
     */
    public static double call
        (AbstractFunction f, double x, double y, double z, double w)
        throws ParseException
    {
        return call(f, new double[] {x, y, z, w} );
    }
    
    
    /**
     * <p>Make an XPoint2D array of pairs (x,f(x))
     * where x is sampled on the given interval
     * divided into the given number of divisions.</p>
     *
     * <p>If the given function is <code>null</code>,
     * returns <code>null</code>.</p>
     *
     * <p>If divisions is less than 1, it will be set to 1.  The size
     * of the output array will be (divisions+1).</p>
     *
     * <p>See also the <code>makeTable</code> methods in
     * <code>DataTables2D</code>.</p>
     * 
     * @param f         the function of one argument to evaluate
     * @param endpointA an endpoint of the interval
     * @param endpointB an endpoint of the interval
     * @param divisions the number of subdivisions of the interval
     * @return the array of (x,f(x)) pairs
     * @throws ParseException
     */
    public static XPoint2D[] makeTable
        (AbstractFunction f,
         double endpointA,
         double endpointB,
         int divisions)
        throws ParseException
    {
        String message;
        
        if (f == null) {
            return null;
        }
        
        String name = f.name();
        int args = f.arguments();
        
        if (args != 1) {
            message = "Method makeTable requires "
                + "a function of one argument but was passed "
                + name + " which needs " + args + " arguments";
            
            throw new ParseException(message, 0);
        }
        
        if (divisions < 1)
            divisions = 1;
        
        int d = divisions;
        
        XPoint2D[] result = new XPoint2D[d + 1];
        
        result[0] =
            new XPoint2D(endpointA, call(f, endpointA));
        
        result[d] =
            new XPoint2D(endpointB, call(f, endpointB));
        
        if (d == 1)
            return result;
        
        double delta = (endpointB - endpointA) / d;
        
        for (int i = 1; i < d; i++) {
            double x = endpointA + i * delta;
            result[i] = new XPoint2D(x, call(f, x));
        }
        
        return result;
    }


    
    /**
     * <p>Make an XPoint2D array of pairs (x,f(x))
     * where x is sampled on the given interval
     * divided into the given number of divisions.</p>
     *
     * <p>If the given function is <code>null</code>
     * or limits is <code>null</code>,
     * returns <code>null</code>.</p>
     *
     * <p>If divisions is less than 1, it will be set to 1.  The size
     * of the output array will be (divisions+1).</p>
     *
     * <p>See also the <code>makeTable</code> methods in
     * <code>DataTables2D</code>.</p>
     * 
     * @param f         the function of one argument to evaluate
     * @param limits    the interval
     * @param divisions the number of subdivisions of the interval
     * @return the array of (x,f(x)) pairs
     * @throws ParseException
     */
    public static XPoint2D[] makeTable
        (AbstractFunction f,
         XInterval limits,
         int divisions)
        throws ParseException
    {
        if (limits == null)
            return null;
        
        double endpointA = limits.getMinimum();
        double endpointB = limits.getMaximum();
        
        return makeTable(f, endpointA, endpointB, divisions);
    }
    
    
    /**
     * <p>Add this identifier to the table of
     * reserved identifiers so it may not be
     * used for a constant or variable id or
     * a function name.</p>
     * 
     * <p>A reserved identifier is intended to
     * be the identifier for a special function
     * that may have an indefinite number of
     * arguments and may not fully evaluate the
     * arguments.</p>
     * 
     * <p>Reserving an identifier does not define
     * the special function or implement the test
     * that makes that behavior accessible to the
     * parser.  For examples of how to do this,
     * see the source for this class and examine
     * how the special functions set, let, if,
     * eval, and random have been defined.</p>
     * 
     * <p>Throws <code>IllegalArgumentException</code>
     * in the following cases:</p>
     * 
     * <ul>
     *   <li>The id is not valid when tested by
     *       <code>isPossibleIdentfier</code></li>
     *   <li>The id is already installed in the
     *       parser as a constant id
     *       or as a function name.</li>
     * </ul>
     * 
     * <p>If a reserved id is introduced after the
     * definition of the id as a variable id, then
     * the variable will become inaccessible.</p>
     * 
     * <p>It is recommended that reserved id's be
     * set before any other id's are introduced.</p>
     * 
     * @param id the id to set as reserved
     */
    public final void reserveID(String id) {
        String message = null;
        
        if (! isPossibleIdentifier(id)) {
            message = id + " is not a valid identifier"
                + " for defining a reserved ID";
        }
        
        else if (isConstantID(id)) {
            message = id + " may not define a reserved ID"
            + " since it already defines a constant ID";
        }
        
        else if (isFunctionName(id)) {
            message = id + " may not define a reserved ID"
            + " since it already defines a function name";
        }
        
        if (message != null)
            throw new IllegalArgumentException(message);
        
        if (!reserved.containsKey(id))
            reserved.put(id,id);
    }
    
    
    /**
     * <p>Adds the given constant to the table
     * of constants using the given identifier
     * and value.</p>
     *
     * <p>Does nothing if either argument is
     * <code>null</code>.</p>
     * 
     * <p>A constant identifier once installed
     * may not be removed or redefined.</p>
     * 
     * <p>Throws <code>IllegalArgumentException</code>
     * in the following cases:</p>
     * 
     * <ul>
     *   <li>The id is not valid when tested by
     *       <code>isPossibleIdentfier</code></li>
     *   <li>The id is already installed in the
     *       parser as a reserved id,
     *       as a constant id,
     *       or as a function name.</li>
     * </ul>
     * 
     * <p>If a constant id is introduced after the
     * definition of the id as a variable id, then
     * the variable will become inaccessible.</p>
     * 
     * <p>It is recommended that constant id's be
     * set before variable id's are introduced.</p>
     * 
     * @param id the identifier for the constant
     * @param value the value for the constant
     * @throws IllegalArgumentException
     */
    public final void addConstant(String id, Object value) {
        if ((id == null) || (value == null))
            return;
        
        String message = null;
        
        if (! isPossibleIdentifier(id)) {
            message = id + " is not a valid identifier"
                + " for defining a constant";
        }
        
        else if (isReservedID(id)) {
            message = id + " may not define a constant ID"
            + " since it already defines a reserved ID";
        }
        
        else if (isConstantID(id)) {
            message = id + " may not define a constant ID"
            + " since it already defines a constant ID";
        }
        
        else if (isFunctionName(id)) {
            message = id + " may not define a constant ID"
            + " since it already defines a function name";
        }
        
        if (message != null)
            throw new IllegalArgumentException(message);
        
        constants.put(id, value);
    }
    
    
    /**
     * <p>Returns true if the given string is a possible
     * variable id in the current parser context.</p>
     * 
     * <p>Specifically, returns true if:</p>
     * 
     * <ul>
     *   <li><code>isPossibleIdentifier(id)</code>
     *       is true</li>
     *   <li>the string is not installed as a reserved
     *       or constant id or as a function name in
     *       the current parser context</li>
     * </ul>
     * 
     * @param id the string to test as a variable id
     */
    public final boolean isPossibleVariableID(String id) {
        if (id == null)
            return false;
        
        if (! isPossibleIdentifier(id))
            return false;
        
        if (isReservedID(id))
            return false;
        
        if (isConstantID(id))
            return false;
         
        if (isFunctionName(id))
            return false;
        
        return true;
    }
    
    
    /**
     * <p>Assigns the given value 
     * to the given identifier
     * as a persistant "set" variable for this parser, 
     * replacing any previous "set" variable
     * associated with the same identifier.</p>
     *
     * <p>Does nothing if id is <code>null</code>.</p>
     * 
     * <p>Assigns the variable if value is
     * non-<code>null</code>
     * and removes the variable otherwise.</p>
     * 
     * <p>Returns value.</p>
     * 
     * <p>Throws <code>IllegalArgumentException</code> if
     * <code>isPossibleVariableID(id)</code> is false.</p>
     * 
     * @param id the identifier for the variable
     * @param value the value of the variable
     * @throws IllegalArgumentException
     */
    public final Object assignSetVariable
        (String id, Object value)
    {
        return assignVariable(id, value, set_variables);
    }
    
    
    /**
     * <p>Assigns the given value 
     * to the given identifier
     * as a temporary "let" variable
     * in the context of the current call
     * to the <code>parse</code> method of this parser, 
     * replacing any previous "let" variable
     * associated with the same identifier in this same
     * context.</p>
     *
     * <p>If there is no invocation of a parse method
     * that is pending, this method call will be useless.</p>
     * 
     * <p>Does nothing if id is <code>null</code>.</p>
     * 
     * <p>Assigns the variable if value is
     * non-<code>null</code>
     * and removes the variable otherwise.</p>
     * 
     * <p>Returns value.</p>
     * 
     * <p>Throws <code>IllegalArgumentException</code> if
     * <code>isPossibleVariableID(id)</code> is false.</p>
     * 
     * @param id the identifier for the variable
     * @param value the value of the variable
     * @throws IllegalArgumentException
     */
    public final Object assignLetVariable
        (String id, Object value)
    {
        return assignVariable(id, value, let_variables);
    }
    
    
    /**
     * <p>This method takes id-value pairs from corresponding
     * positions in the lists ids and values and performs the
     * assignment using <code>assignLetVariable</code>.</p>
     *
     * <p>If there is no invocation of a parse method
     * that is pending, this method call will be useless.</p>
     * 
     * <p>Does nothing if both lists are <code>null</code>.</p>
     * 
     * <p>Otherwise throws <code>IllegalArgumentException</code>
     * in any of the following circumstances.</p>
     * 
     * <ul>
     *   <li>One list is <code>null</code> and the other is not.</li>
     *   <li>The two lists do not have the same length.</li>
     *   <li>Some id in the list ids is <code>null</code> or
     *   fails to satisfy <code>isPossibleVariableID</code>.</li>
     *   <li>Some value in the list values is <code>null</code>.</li>
     * </ul>
     * 
     * @param ids the list of identifiers to assign
     * @param values the list of values to assign to the identifiers
     * @throws IllegalArgumentException
     */
    public void assignArgumentList
        (String[] ids, Object[] values)
    {
        if ((ids == null) && (values == null))
            return;
        
        String message = null;
        
        if (ids == null) {
            message = "Null id list passed to "
                + "assignArgumentList";
        }
        
        else if (values == null) {
            message = "Null value list passed to "
                + "assignArgumentList";
        }
        
        else if (ids.length != values.length) {
            message = "In assignArgumentList "
                + "the id list and the value list "
                + "do not have the same length";
        }
        
        else {
            int length = ids.length;
            
            for (int i = 0; i < length; i++) {
                if (ids[i] == null) {
                     message = "In assignArgumentList "
                        + "the id in position "
                        + i
                        + " is null";
                    
                    break;
                }
                
                if (values[i] == null) {
                    message = "In assignArgumentList "
                        + "the value in position "
                        + i
                        + " is null";
                    
                    break;
                }
                    
                if (! isPossibleVariableID(ids[i])) {
                    message = "In assignArgumentList "
                        + "the id in position "
                        + i
                        + ", namely, "
                        + ids[i]
                        + ", is not a possible variable id";
                    
                    break;
                }
            }
        }
        
        if (message != null)
            throw new IllegalArgumentException(message);

        int length = ids.length;
        
        for (int i = 0; i < length; i++)
            assignLetVariable(ids[i], values[i]);
    }
    
    
    /**
     * <p>Adds the given function 
     * to the table of recognized functions.</p>
     * 
     * <p>Does nothing if the function is
     * <code>null</code>.</p>
     * 
     * <p>As of 2.5.0, permits the replacement of
     * an installed function by adding a new
     * function whose name is the same as the
     * older installed function.  This allows
     * experimentation with implementation.</p>
     *
     * <p>Throws <code>IllegalArgumentException</code>
     * in the following case:</p>
     * 
     * <ul>
     *   <li>The function name is already installed
     *       as a reserved or constant id</li>
     * </ul>
     * 
     * <p>If the function name is the same as the
     * id of a previously defined variable, then
     * the variable will become inaccessible.</p>
     * 
     * <p>A function may be removed based on its name
     * using <code>removeFunction</code>.</p>
     * 
     * @param  function the function to be added
     * @throws IllegalArgumentException
     */
    public final void addFunction(AbstractFunction function) {
        if (function == null)
            return;
        
        String name = function.name();
        
        String message = null;
        
        if (isReservedID(name)) {
            message = name + " may not define a function name"
            + " since it already defines a reserved ID";
        }
        
        else if (isConstantID(name)) {
            message = name + " may not define a function name"
            + " since it already defines a constant ID";
        }
        
        if (message != null)
            throw new IllegalArgumentException(message);
        
        functions.put(name, function);
    }
    
    
    /**
     * <p>Removes the function with the given name
     * from this parser.</p>
     * 
     * <p>Returns the removed <code>AbstractFunction</code>
     * or <code>null</code> if no such function was
     * found.</p>
     * 
     * @param name the function name
     */
    public final AbstractFunction removeFunction(String name) {
        if (name == null)
            return null;
        
        return (AbstractFunction) functions.remove(name);
    }
    
    
    /**
     * <p>Adds the given operation <code>op</code>
     * to the table of recognized operations,
     * at the same level of precedence as the
     * existing operation <code>compare</code>
     * installed in this parser.<p>
     *
     * <p>The operation <code>IDENTITY</code> is
     * initially installed in the parser at
     * precedence level 0 and may be used as the
     * base for adding additional operators.</p>
     * 
     * <p>Throws <code>IllegalArgumentException</code>
     * if the operation <code>compare</code> is not
     * an existing operation for this parser.</p>
     * 
     * <p>As of 2.5.0, permits the replacement of
     * an installed operation by an operation
     * with the same symbol.</p>
     *
     * <p>This method will work correctly if the
     * <code>compare</code> operation has the same
     * symbol as the operation <code>op</code>.
     * The precedence of the <code>compare</code>
     * operation will be determined and then the
     * <code>compare</code> operation will be
     * replaced by the operation <code>op</code>.</p>
     * 
     * @param compare an existing operation 
     *      at the desired level of precedence
     * @param op the operation to be added
     * @return the operation object <code>op</code>
     *      that was added to the operation table
     * @throws <code>IllegalArgumentException</code>
     */
    public final Operation addOperationAtPrecedenceOf(
        Operation compare, 
        Operation op) 
    {
        // find the precedence of comparable operation
        int pos = precedenceOf(compare);
        
        if (pos == -1) {
            String message = "addOperation... error: "
                + "compare operator not installed";
            
            throw new IllegalArgumentException(message);
        }
        
        // add the operation at the proper precedence
        addOperation(op, pos);
        
        // return the added operation
        return op;
    }
    
    
    /**
     * <p>Adds the given operation <code>op</code>
     * to the table of recognized operations,
     * at a level of precedence
     * immediately before the precendence of the
     * existing operation <code>compare</code>
     * installed in this parser.<p>
     * 
     * <p>A new precedence level is inserted
     * between that of the <code>compare</code>
     * operation and all lower precedence
     * operations.</p>
     * 
     * <p>The operation <code>IDENTITY</code> is
     * initially installed in the parser at
     * precedence level 0 and may be used as the
     * base for adding additional operators.
     * However, you may NOT install an operation
     * before the <code>IDENTITY</code>.</p>
     * 
     * <p>Throws <code>IllegalArgumentException</code>
     * if:</p>
     *
     * <ul>
     *   <li>the operation <code>compare</code> is not
     *       an existing operation for this parser</li>
     *   <li>the operation <code>compare</code> is at
     *       precedence 0 since it is illegal to
     *       insert an operation at precedence -1</li>
     * </ul>
     * 
     * <p>As of 2.5.0, permits the replacement of
     * an installed operation by an operation
     * with the same symbol.</p>
     *
     * <p>This method will "work" if the
     * <code>compare</code> operation has the same
     * symbol as the operation <code>op</code> but
     * it may not do what is desired.  The problem
     * is that it is likely that you want to keep
     * the current precedence level and not insert
     * a new level in this case.  Therefore, it is
     * recommended that you use the method
     * <code>addOperationAtPrecedenceOf</code> to
     * perform the replacement of an operator.</p>
     * 
     * @param compare an existing operation 
     *      at the desired level of precedence
     * @param op the operation to be added
     * @return the operation object <code>op</code>
     *      that was added to the operation table
     * @throws <code>IllegalArgumentException</code>
     */
    public final Operation addOperationBeforePrecedenceOf(
        Operation compare, 
        Operation op) 
    {
        // find precedence of comparable operation
        int pos = precedenceOf(compare);
        
        if (pos == -1) {
            String message = "addOperation... error: "
                + "compare operator not installed";
            
            throw new IllegalArgumentException(message);
        }
        
        if (pos == 0) {
            String message = "addOperation... error: "
                + "may not install before precedence 0";
            
            throw new IllegalArgumentException(message);
        }
        
        // add new precedence level before level pos
        precedence.insertElementAt(new Hashtable(), pos);
        
        // add the operation at level pos which will
        // be the level just constructed above
        addOperation(op, pos);
        
        // return the added operation
        return op;
    }
    
    
    /**
     * <p>Adds the given operation <code>op</code>
     * to the table of recognized operations,
     * at a level of precedence
     * immediately after the precendence of the
     * existing operation <code>compare</code>
     * installed in this parser.<p>
     * 
     * <p>A new precedence level is inserted
     * between that of the <code>compare</code>
     * operation and all higher precedence
     * operations.</p>
     * 
     * <p>The operation <code>IDENTITY</code> is
     * initially installed in the parser at
     * precedence level 0 and may be used as the
     * base for adding additional operators.</p>
     * 
     * <p>Throws <code>IllegalArgumentException</code>
     * if the operation <code>compare</code> is not
     * an existing operation for this parser.</p>
     * 
     * <p>As of 2.5.0, permits the replacement of
     * an installed operation by an operation
     * with the same symbol.</p>
     *
     * <p>This method will "work" if the
     * <code>compare</code> operation has the same
     * symbol as the operation <code>op</code> but
     * it may not do what is desired.  The problem
     * is that it is likely that you want to keep
     * the current precedence level and not insert
     * a new level in this case.  Therefore, it is
     * recommended that you use the method
     * <code>addOperationAtPrecedenceOf</code> to
     * perform the replacement of an operator.</p>
     * 
     * @param compare an existing operation 
     *      at the desired level of precedence
     * @param op the operation to be added
     * @return the operation object <code>op</code>
     *      that was added to the operation table
     * @throws <code>IllegalArgumentException</code>
     */
    public final Operation addOperationAfterPrecedenceOf(
        Operation compare, 
        Operation op) 
    {
        // find precedence of comparable operation
        int pos = precedenceOf(compare);
        
        if (pos == -1) {
            String message = "addOperation... error: "
                + "compare operator not installed";
            
            throw new IllegalArgumentException(message);
        }
        
        // add new precedence level after level pos
        precedence.insertElementAt(new Hashtable(), pos + 1);
        
        // add the operation at level (pos + 1) which
        // will be the level just constructed above
        addOperation(op, pos + 1);
        
        // return the added operation
        return op;
    }
    
    
    /**
     * <p>Returns whether or not the given id is
     * associated with a constant.</p>
     * 
     * @param id the constant id
     */
    public final boolean isReservedID(String id) {
        if (id == null)
            return false;
        
        return reserved.containsKey(id);
    }
    
    
    /**
     * <p>Returns whether or not the given id is
     * associated with a constant.</p>
     * 
     * @param id the constant id
     */
    public final boolean isConstantID(String id) {
        if (id == null)
            return false;
        
        return constants.containsKey(id);
    }
    
    
    /**
     * <p>Returns whether or not the given id is
     * associated with a "set" variable.</p> 
     * 
     * @param id the variable id
     */
    public final boolean isSetVariableID(String id) {
        if (id == null)
            return false;
        
        return set_variables.containsKey(id);
    }
    
    
    /**
     * <p>Returns whether or not the given id is
     * associated with a "let" variable
     * in the current invocation of the
     * <code>parse</code> method.</p> 
     * 
     * @param id the variable id
     */
    public final boolean isLetVariableID(String id) {
        if (id == null)
            return false;
        
        return let_variables.containsKey(id);
    }
    
    
    /**
     * <p>Returns whether or not the given id is
     * associated with a constant or variable.</p> 
     * 
     * @param id the constant or variable id
     */
    public final boolean isEnvironmentID(String id) {
        if (id == null)
            return false;
        
        return isConstantID(id)
            || isSetVariableID(id)
            || isLetVariableID(id);
    }
    
    
    /**
     * <p>Returns whether or not the given name is
     * associated with a function.</p>
     * 
     * @param name the function name
     */
    public final boolean isFunctionName(String name) {
        if (name == null)
            return false;
        
        return functions.containsKey(name);
    }
    
    
    /**
     * <p>Returns whether or not the given name is
     * associated with a function whose class
     * is or extends <code>SimpleFunction</code>.</p>
     * 
     * @param name the function name
     */
    public final boolean isSimpleFunctionName(String name) {
        AbstractFunction function = getFunction(name);
        
        if (function == null)
            return false;
        
        return (function instanceof SimpleFunction);
    }
    
    
    /**
     * <p>Returns whether or not the given name is
     * associated with a function whose class
     * does NOT extend <code>SimpleFunction</code>.</p>
     * 
     * @param name the function name
     */
    public final boolean isOrdinaryFunctionName(String name) {
        AbstractFunction function = getFunction(name);
        
        if (function == null)
            return false;
        
        return ! (function instanceof SimpleFunction);
    }
    
    
    /**
     * <p>Returns whether or not the given symbol is
     * associated with an operation.</p>
     * 
     * @param symbol the operation symbol
     */
    public final boolean isOperationSymbol(String symbol) {
        if (symbol == null)
            return false;
        
        return operations.containsKey(symbol);
    }
    
    
    /**
     * <p>Returns the value associated with a
     * constant or variable with the given id
     * or <code>null</code> if no such value
     * is defined.</p>
     * 
     * <p>Looks for values in the following
     * order:</p>
     * 
     * <ul>
     *   <li>constant</li>
     *   <li>variable defined by "let"</li>
     *   <li>variable defined by "set"</li>
     * </ul>
     * 
     * @param id the constant or variable id
     */
    public final Object getValue(String id) {
        if (id == null)
            return null;
        
        if (isConstantID(id))
            return constants.get(id);
        else if (isLetVariableID(id))
            return let_variables.get(id);
        else if (isSetVariableID(id))
            return set_variables.get(id);
        else
            return null;
    }
    
    
    /**
     * <p>Returns the function with the given name
     * or <code>null</code> if no such function is
     * defined.</p>
     * 
     * @param name the function name
     */
    public final AbstractFunction getFunction(String name) {
        if (isFunctionName(name))
            return (AbstractFunction) functions.get(name);
        else
            return null;
    }
    
    
    /**
     * <p>Returns the operation with the given symbol
     * or <code>null</code> if no such operation is
     * defined.</p>
     * 
     * @param symbol the operation symbol
     */
    public final Operation getOperation(String symbol) {
        if (isOperationSymbol(symbol))
            return (Operation) operations.get(symbol);
        else
            return null;
    }
    
    
    /**
     * <p>Returns an array of the reserved id's.</p>
     * 
     * <p>The id's are sorted alphabetically.</p>
     */
    public final String[] reserved() {
        Enumeration e = reserved.keys();
        
        Vector list = new Vector();
        
        while (e.hasMoreElements())
            list.add(e.nextElement());
        
        int size = list.size();
        
        String[] result = (String[]) list.toArray(new String[size]);
        
        Arrays.sort(result);
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of the constant id's.</p>
     * 
     * <p>The id's are sorted alphabetically.</p>
     */
    public final String[] constants() {
        Enumeration e = constants.keys();
        
        Vector list = new Vector();
        
        while (e.hasMoreElements())
            list.add(e.nextElement());
        
        int size = list.size();
        
        String[] result = (String[]) list.toArray(new String[size]);
        
        Arrays.sort(result);
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of the set variable id's.</p>
     * 
     * <p>The id's are sorted alphabetically.</p>
     */
    public final String[] set_variables() {
        Enumeration e = set_variables.keys();
        
        Vector list = new Vector();
        
        while (e.hasMoreElements())
            list.add(e.nextElement());
        
        int size = list.size();
        
        String[] result = (String[]) list.toArray(new String[size]);
        
        Arrays.sort(result);
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of all function names.</p>
     * 
     * <p>The names are sorted alphabetically.</p>
     */
    public final String[] functionNames() {
        Enumeration e = functions.keys();
        
        Vector list = new Vector();
        
        while (e.hasMoreElements())
            list.add(e.nextElement());
        
        int size = list.size();
        
        String[] result = (String[]) list.toArray(new String[size]);
        
        Arrays.sort(result);
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of the function names
     * that correspond to functions whose class
     * is or extends <code>SimpleFunction</code>.</p>
     * 
     * <p>The names are sorted alphabetically.</p>
     */
    public final String[] simpleFunctionNames() {
        Enumeration e = functions.keys();
        
        Vector list = new Vector();
        
        while (e.hasMoreElements()) {
            String name = (String)e.nextElement();
            
            if (isSimpleFunctionName(name))
                list.add(name);
        }
        
        int size = list.size();
        
        String[] result = (String[]) list.toArray(new String[size]);
        
        Arrays.sort(result);
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of the function names
     * that correspond to functions whose class
     * does NOT extend <code>SimpleFunction</code>.</p>
     * 
     * <p>The names are sorted alphabetically.</p>
     */
    public final String[] ordinaryFunctionNames() {
        Enumeration e = functions.keys();
        
        Vector list = new Vector();
        
        while (e.hasMoreElements()) {
            String name = (String)e.nextElement();
            
            if (isOrdinaryFunctionName(name))
                list.add(name);
        }
        
        int size = list.size();
        
        String[] result = (String[]) list.toArray(new String[size]);
        
        Arrays.sort(result);
        
        return result;
    }
    
    
    /**
     * <p>Returns an array of all functions.</p>
     * 
     * <p>The functions are sorted by name.</p>
     */
    public final AbstractFunction[] functions() {
        String[] names = functionNames();
        int length = names.length;
        
        AbstractFunction[] fcns =
            new AbstractFunction[length];
        
        for (int i = 0; i < length; i++) {
            fcns[i] = (AbstractFunction) functions.get(names[i]);
        }
         
        return fcns;
    }
    
    
    /**
     * <p>Returns an array of the simple functions,
     * that is, functions whose class is or extends
     * <code>SimpleFunction</code>.</p>
     * 
     * <p>The functions are sorted by name.</p>
     */
    public final SimpleFunction[] simpleFunctions() {
        String[] names = simpleFunctionNames();
        int length = names.length;
        
        SimpleFunction[] fcns =
            new SimpleFunction[length];
        
        for (int i = 0; i < length; i++) {
            fcns[i] = (SimpleFunction) functions.get(names[i]);
        }
         
        return fcns;
    }
    
    
    /**
     * <p>Returns an array of the ordinary functions,
     * that is, functions whose class does NOT
     * extend <code>SimpleFunction</code>.</p>
     * 
     * <p>The functions are sorted by name.</p>
     */
    public final AbstractFunction[] ordinaryFunctions() {
        String[] names = ordinaryFunctionNames();
        int length = names.length;
        
        AbstractFunction[] fcns =
            new AbstractFunction[length];
        
        for (int i = 0; i < length; i++) {
            fcns[i] = (AbstractFunction) functions.get(names[i]);
        }
         
        return fcns;
    }
    
    
    /**
     * <p>Returns an array of the operation symbols.</p>
     */
    public final String[] operationSymbols() {
        Enumeration e = operations.keys();
        
        Vector list = new Vector();
        
        while (e.hasMoreElements())
            list.add(e.nextElement());
        
        int size = list.size();
        
        return (String[]) list.toArray(new String[size]);
    }
    
    
    /**
     * <p>Returns an array of the operations.</p>
     */
    public final Operation[] operations() {
        String[] symbols = operationSymbols();
        int length = symbols.length;
        
        Operation[] ops =
            new Operation[length];
        
        for (int i = 0; i < length; i++) {
            ops[i] = (Operation) operations.get(symbols[i]);
        }
         
        return ops;
    }
    
    
    /**
     * <p>Returns a <code>String</code> representation
     * of all <code>Simple Function</code> objects
     * currently installed in this parser.</p>
     * 
     * <p>This representation is constructed as follows:</p>
     * 
     * <ul>
     *   <li>The method <code>simpleFunctions()</code>
     *       is called to obtain all simple functions.</li>
     *   <li>For each simple function, its
     *       <code>toString()</code> method is called.
     *       This string is appended to a temporary
     *       <code>StringBuffer</code> object.</li>
     *   <li>The final contents of the buffer is then
     *       returned as the value of this method.</li>
     * </ul>
     * 
     * <p>As a consequence, the string returned by this
     * method will contain 3*N lines where N is the
     * number of simple functions installed and the
     * 3 lines for each function will consist of its
     * function name, the comma separated list of its
     * function parameters, and its function body.</p>
     * 
     * <p>If a function has 0 parameters, the line for
     * its parameters will contain a blank to act as a
     * placeholder for parsing purposes.</p>
     */
    public final String simpleFunctionsToString() {
        StringBuffer buffer = new StringBuffer();
        
        SimpleFunction[] fcns = simpleFunctions();
        
        int length = fcns.length;
        
        for (int i = 0; i < length; i++)
            buffer.append(fcns[i].toString());
        
        return buffer.toString();
    }
    
    
    /**
     * <p>Treats the given <code>String</code>
     * as a representation for a list of
     * <code>SimpleFunction</code> objects,
     * builds those objects, and installs them
     * in this parser.  Existing simple functions
     * with the same name as one in the list will
     * be replaced.</p>
     * 
     * <p>Expectations:</p>
     * 
     * <ul>
     *   <li>The string has 3*N lines for some N.</li>
     *   <li>Each group of 3 lines should consist
     *       of a function name, a comma separated
     *       list of function parameters, and a
     *       function body.</li>
     *   <li>If a function has 0 parameters then
     *       the line corresponding to its
     *       parameters should contain one blank
     *       and therefore should NOT be empty.</li>
     * </ul>
     * 
     * <p>Throws <code>IllegalArgumentException</code>
     * if an error is detected.  Those definitions
     * that have been successful before the error was
     * detected will stand.</p>
     * 
     * @param string the string representation of a list
     *        of simple functions
     * @throws IllegalArgumentException
     */
    public void stringToSimpleFunctions(String string) {
        String message = null;
        String me = "stringToSimpleFunctions";
        
        if (string == null) {
            message = "Null string passed to " + me;
            
            throw new IllegalArgumentException(message);
        }
        
        String[] lines = Strings.tokenize(string, "\n", true);
        
        int length = lines.length;
        
        if ((length % 3) != 0) {
            message = "The string passed to " + me
                + "\nfails to consist of 3*N lines for some N"
                + "\nbut rather has " + length + " lines";
            
            throw new IllegalArgumentException(message);
        }
        
        int N = length / 3;
        int i = 0;
        int k = 0;
        
        try {
            while (i < N) {
                k = 3 * i;
                
                String name       = lines[k];
                String parameters = lines[k+1];
                String body       = lines[k+2];
                
                new SimpleFunction(this, name, parameters, body);
                
                i++;
            }
        } catch (IllegalArgumentException iae) {
            message = "In " + me 
                + "\nerror in function definition " + i
                + "\nat lines " + k + " to " + (k+2)
                + "\n" + iae.getMessage();
            
            throw new IllegalArgumentException(message);
        }
    }
    
    
    /**
     * <p>Returns true if the given id is a possible
     * identifier for a constant, variable, function
     * or reserved function.</p>
     * 
     * <p>More precisely:</p>
     * 
     * <ul>
     *   <li>the id must have length at least 1</li>
     *   <li>the first character must be
     *       a letter or underscore</li>
     *   <li>all subsequent characters must be
     *       letters, digits, or underscores</li>
     * </ul>
     * 
     * <p>This method is static and does not consider
     * the relation of this id to any identifiers
     * installed in any particular parser.</p>
     * 
     * @param id the id to test
     */
    public static final boolean isPossibleIdentifier(String id) {
        if ((id == null) || (id.length() == 0))
            return false;
        
        int length = id.length();
        
        char c;
        
        c = id.charAt(0);
        
        if (! (Character.isLetter(c) || (c == '_')))
            return false;
        
        for (int i = 1; i < length; i++) {
            c = id.charAt(i);
            
            if (! (Character.isLetter(c)
                    || Character.isDigit(c)
                    || (c == '_')))
                return false;
            
        }
        
        return true;
    }
    
    
    /**
     * <p>Returns true if the given string is a possible symbol for
     * an operation.</p>
     * 
     * <p>More precisely, the following punctuation characters will
     * be considered valid for an operation symbol:</p>
     * 
     * <pre>    + - * / % = < > ! ? : @ # $ ^ &amp; ' " ` ~ | \</pre>
     * 
     * <p>A string that consists of one or more of these characters
     * will be considered valid.</p>
     * 
     * <p>In addition, to support the built-in <code>Operation</code>
     * objects IDENTITY and OPERATION_PREFIX,
     * the strings "" and "\0" will be considered valid via an
     * ad hoc check.</p>
     * 
     * @param string the string to test
     */
    public static final boolean isPossibleOperation(String string) {
        if (string == null)
            return false;
        
        if (string.equals(""))
            return true;
        
        if (string.equals("\0"))
            return true;
        
        int length = string.length();
        
        char c;
        
        for (int i = 0; i < length; i++) {
            c = string.charAt(i);
            
            switch (c) {
                case '+':
                case '-':
                case '*':
                case '/':
                case '%':
                case '=':
                case '<':
                case '>':
                case '!':
                case '?':
                case ':':
                case '@':
                case '#':
                case '$':
                case '^':
                case '&':
                case '\'':
                case '"':
                case '`':
                case '~':
                case '|':
                case '\\':
                    break;
                    
                default:
                    return false;
            }
            
        }
        
        return true;
    }
    
    
    ///////////////////////
    // Protected methods //
    ///////////////////////
    
    /**
     * <p>Installs the reserved identifiers for
     * the special functions.</p>
     *
     * <p>This method may be overridden
     * by a derived class if the derived class
     * defines additional special functions.
     * In that case, this method should be
     * called first by the override method in
     * order that the reserved identifiers
     * defined by this parser are installed.</p>
     * 
     * <p>This method reserves:</p>
     * 
     * <ul>
     *   <li><code>ASSIGNMENT_BY_SET</code></li>
     *   <li><code>ASSIGNMENT_BY_LET</code></li>
     *   <li><code>IF_THEN_ELSE</code></li>
     *   <li><code>EVAL</code></li>
     *   <li><code>RANDOM</code></li>
     * </ul>
     */
    protected void addReserved() {
        reserveID(ASSIGNMENT_BY_SET);
        reserveID(ASSIGNMENT_BY_LET);
        reserveID(IF_THEN_ELSE);
        reserveID(EVAL);
        reserveID(RANDOM);
    }
    
    
    /**
     * <p>Adds the standard constants for this parser
     * to the environment.</p>
     *
     * <p>This method must be overridden
     * by a derived class if the derived class
     * has constants defined in its environment.</p>
     * 
     * <p>This method does nothing at present.</p>
     */
    protected void addConstants() {}
    
    
    /**
     * <p>Adds the standard functions for this parser
     * to the function table.</p>
     *
     * <p>This method must be overridden
     * by a derived class if the derived class
     * makes use of functions.</p>
     * 
     * <p>This method does nothing at present.</p>
     */
    protected void addFunctions() {}
    
    
    /**
     * <p>Adds the standard operations for this parser 
     * to the operation table.</p>
     *
     * <p>This method must be overridden
     * by a derived class if the derived class
     * makes use of operations.</p>
     * 
     * <p>This method does nothing at present.</p>
     */
    protected void addOperations() {}
    
    
    /**
     * <p>Returns the next term in the data string
     * with all leading unary operations already applied
     * or throws a <code>ParseException</code>
     * if no term is present.</p>
     * 
     * @throws ParseException
     */
    protected Object nextTerm()
        throws ParseException
    {
        Operation[] operations = nextUnaryOperations();
        int length = operations.length;
        
        Object term = nextSimpleTerm();
        
        // apply unary operations from right to left
        if (evaluate()) {
            for (int i = (length - 1); i >= 0; i--)
                term = operations[i].performOperation(null, term);
        }
        
        return term;
    }
    
    
    /**
     * <p>Returns the next simple term in the
     * data string
     * or throws a <code>ParseException</code>
     * if no term is present.</p>
     *
     * <p>A simple term is a term that is not preceded
     * by unary operations that will need to be applied
     * later. The work of collecting and applying unary
     * operations is done in the method
     * <code>nextTerm()</code>.</p>
     * 
     * <p>This method orchestrates the dispatch to the
     * methods that handle important special cases.</p>
     * 
     * <ul>
     *   <li>Nested expresssions</li>
     *   <li>Numbers</li>
     *   <li>Expressions introduced by an identifier</li>
     * </ul>
     * 
     * @throws ParseException
     */
    protected Object nextSimpleTerm()
        throws ParseException
    {
        skipWhitespace();
        
        Object term = null;

        if (nextTokenIs(NESTED_EXPRESSION_START))
            term = parseNestedExpression();
        else if (startsNumber())
            term = parseNumber();
        else if (startsIdentifier()) {
            term = parseIdentifierExpression();
        }
        else {
            throw new ParseException("Expected term", next);
        }
        
        skipWhitespace();
        
        return term;
    }
    
    
    /**
     * <p>Returns the array of unary operations that occur
     * after the current position in the data string and
     * throws a ParseException if an operation is
     * encountered that is binary but not unary.</p>
     * 
     * <p>The array returned may be empty but will not be
     * <code>null</code>.</p>
     * 
     * @throws ParseException
     */
    protected Operation[] nextUnaryOperations()
        throws ParseException
    {
        Vector operations = new Vector();
        
        while (true) {
            Operation operation = nextOperation();
            
            if (operation == null)
                break;
            
            if (operation.isUnary())
                operations.add(operation);
            else
                throw new ParseException
                    ("Expected a pending unary operation", next);
        }
        
        return (Operation[]) operations.toArray(new Operation[0]);
    }
    
    
    /**
     * <p>Parse an expression that is introduced by
     * an identifier.</p>
     * 
     * <p>This includes in the following order:</p>
     * 
     * <ul>
     *   <li>special function call</li>
     *   <li>function call</li>
     *   <li>constant value</li>
     *   <li>variable value</li>
     * </ul>
     * 
     * @throws ParseException
     */
    protected Object parseIdentifierExpression()
        throws ParseException
    {
        String identifier = nextIdentifier();
        
        Object term = null;
        
        if (isReservedID(identifier))
            term = parseSpecialFunction(identifier);
        else if (isFunctionName(identifier))
            term = parseFunctionCall(identifier);
        else if (isEnvironmentID(identifier))
            term = evaluateIdentifier(identifier);
        else {
            String message = "Unrecognized identifier: " + identifier;
            
            throw new ParseException(message, next);
        }
        
        return term;
    }
    
    
    /**
     * <p>Parse a special function whose parameter
     * list may not be completely evaluated.  The
     * id's of such special functions should be
     * installed as reserved id's.</p>
     * 
     * <p>Throws <code>ParseException</code> if:</p>
     * 
     * <ul>
     *   <li>The identifier is not a reserved id</li>
     *   <li>The parse/evaluation does not run successfully</li>
     * </ul>
     * 
     * <p>To add additional special functions this
     * method must be suitably overridden in a
     * derived class. To facilitate an override,
     * this method returns <code>null</code> if
     * the identifier is a reserved id but there
     * is no code in this method to handle that id.
     * A method that overrides this method should
     * first call this method and should intervene
     * only if this method returns <code>null</code>;
     * otherwise the override method should simply
     * return what this method has returned to it.</p>
     * </p>
     * 
     * <p>As a consequnce of this design, it is
     * entirely valid for this method to return
     * <code>null</code>.  This is what permits a
     * sequence of derived classes to add reserved
     * identifiers and the parsing code for them.
     * Of course, each derived class should follow
     * the same design and provide parsing code
     * for each new reserved word that it adds but
     * return <code>null</code> if it encounters a
     * reserved word that it does not know how to
     * parse.</p>
     * 
     * <p>Note that if you build a derived class
     * based on <code>JPTParser</code> then take
     * care not to introduce a conflict with the
     * function names and constant id's defined
     * in that class.</p>
     * 
     * @throws ParseException
     */
    protected Object parseSpecialFunction(String identifier)
        throws ParseException
    {
        // test if the identifier represents a special function
        if (! isReservedID(identifier))
            throw new ParseException
                ("Unrecognized special function " + identifier, next);
        
        if (identifier.equals(ASSIGNMENT_BY_SET)) {
            return parseAssignment(set_variables);
        }
        else if (identifier.equals(ASSIGNMENT_BY_LET)) {
            return parseAssignment(let_variables);
        }
        else if (identifier.equals(IF_THEN_ELSE)) {
            return parseIfThenElse();
        }
        else if (identifier.equals(EVAL)) {
            return parseEval();
        }
        else if (identifier.equals(RANDOM)) {
            return parseRandom();
        }
        
        return null;
    }
    
    
    /**
     * <p>Save the current parser context and
     * initialize a new context to parse
     * the string d.</p>
     * 
     * <p>This method sets the new string to
     * parse to d, sets the start position
     * to zero, and creates a new context
     * for "let" variables.</p>
     * 
     * <p>Use <code>popContext()</code> to
     * restore the previous context.</p>
     * 
     * @param d the string to parse
     */
    protected final void pushContext(String d) {
        parserContextStack.add
            (new ParserContext(data, next, let_variables));
        
        data = (d == null) ? "" : d;
        next = 0;
        let_variables = new Hashtable();
    }
    
    
    /**
     * <p>Save the current parser context and
     * prepare to parse the string d using
     * the current "let" variable context.</p>
     * 
     * <p>This method sets the new string to
     * parse to d and sets the start position
     * to zero but does not change the context
     * for the "let" variables.</p>
     * 
     * <p>This method permits the parsing of
     * d to be carried out in the context of
     * its caller rather than in a new "let"
     * variable context of its own.</p>
     * 
     * <p>This method is provided to support
     * unusual situations and should be used
     * with great care.</p>
     * 
     * <p>Use <code>popContext()</code> to
     * restore the previous context.</p>
     * 
     * @param d the string to parse
     */
    protected final void specialPush(String d) {
        parserContextStack.add
            (new ParserContext(data, next, let_variables));
        
        data = (d == null) ? "" : d;
        next = 0;
    }
    
    
    /** Pop and restore the previous parser context. */
    protected final void popContext() {
        int size = parserContextStack.size();
        
        ParserContext context =
            (ParserContext) parserContextStack.remove(size - 1);
        
        data = context.data();
        next = context.next();
        let_variables = context.let_variables();
    }
    
    
    /**
     * <p>Throws a <code>ParseException</code> with information
     * about the string d being parsed and the parser position
     * appended to the given exception message.</p>
     * 
     * <p>Nothing is appended if d is <code>null</code>.</p>
     * 
     * <p>Restores the previous parser context just before the
     * exception is thrown.  This will enable the parser
     * methods to recursively accumulate error information and
     * report all information in a manner similar to a stack
     * trace.</p>
     * 
     * @param message the current exception message
     * @param d the parsed string at this level of recursion
     */
    protected final void throwAgainAndPop(String message, String d)
        throws ParseException
    {
        if (message == null)
            message = "";
        
        if (d != null)
            message += "\nat position " + next
                + "\n" + d
                + "\n" + Strings.prefixRepeatChar("^", '.', next);
        
        ParseException pe = new ParseException(message, next);
        
        // restore the previous context
        popContext();
        
        throw pe;
    }
    
    
    /**
     * <p>Recursive method that parses the next expression 
     * in the data string.</p>
     *
     * <p>The given <code>ObjectOperationPair</code> object
     * should represent a binary operation together with
     * its left operand as the value component.</p>
     * 
     * <p>It is permitted for the left operand to be
     * <code>null</code> if it is unused by the operation.
     * This is in fact the case with the default pair whose
     * operation is <code>Operation.IDENTITY</code>.</p>
     *
     * <p>The given binary operation must NOT be
     * <code>null</code>.</p>
     * 
     * <p>In this implementation, the following additional
     * constraints are true.</p>
     * 
     * <p>As the parse proceeds, all valid unary operators
     * will be collected and applied to a subsequent term.</p>
     * 
     * <p>As the parse proceeds, all binary operations at
     * the same or higher precedence will be processed and
     * the results will be accumulated in the value slot of
     * the return pair.</p>
     * 
     * <p>The <code>ObjectOperationPair</code> <i>pair</i>
     * returned will have the following characteristics.</p>
     * 
     * <p>Normally, the value component of <i>pair</i> will
     * be non-<code>null</code> and will represent the final
     * result of applying the various binary operations,
     * functions, and special functions encountered
     * along the way.  However, if this method is called by
     * the method <code>extractExpression</code> then the
     * parser will suspend evaluation and so the return
     * value within <i>pair</i> may be <code>null</code>.</p>
     * 
     * <p>The operation component of <i>pair</i> will either
     * be <code>null</code> or be an operation of lower
     * precedence than the incoming operation.</p>
     * 
     * <p>In particular, if the input pair has the default
     * operation, <code>Operation.IDENTITY</code>, then the
     * operation in the return pair must be <code>null</code>.</p>
     * 
     * @param  pending the <code>ObjectOperationPair</code>
     *         object whose value is the left operand
     *         and whose operation is the operation
     *         immediately preceding the expression
     *         to be parsed
     * @return the <code>ObjectOperationPair</code> object
     *         with the value of the parsed expression and
     *         the lower precedence operation that
     *         immediately follows it or
     *         the <code>null</code> operation
     * @throws ParseException if the expression is malformed
     *         or if the incoming operation is <code>null</code>
     * @since 2.2
     */
    protected final ObjectOperationPair parseExpression
        (ObjectOperationPair pending) throws ParseException 
    {
        // debug
        // showState(pending);
        
        Object left = pending.value();
        Object term = null;
        
        Operation oldOp = pending.operation();
        Operation newOp = null;
        
        int oldPr = precedenceOf(oldOp);
        int newPr = -1;
        
        if (oldOp == null)
            throw new ParseException
                ("Expected a pending binary operation", next);
        
        // repeat until all consecutive binary operations
        // at the current level of precedence or higher
        // have been carried out
        while (true) {
            term  = nextTerm();
            newOp = nextOperation();
            newPr = precedenceOf(newOp);
            
            if ((newOp != null) && (! newOp.isBinary()))
                throw new ParseException
                    ("Expected a pending binary operation", next);
            
            // inner loop that uses recursion
            // to handle any higher levels of precedence
            while (newPr > oldPr) {
                ObjectOperationPair ooPair
                    = parseExpression(new ObjectOperationPair(term, newOp));
                
                term  = ooPair.value();
                newOp = ooPair.operation();
                newPr = precedenceOf(newOp);
            }
            
            // handle this level of precedence
            if (evaluate())
                term = oldOp.performOperation(left, term);
            
            if (newPr == oldPr) {
                // continue the outer loop
                left  = term;
                oldOp = newOp;
            }
            else {
                // exit when the next operation
                // has a lower level of precedence
                return new ObjectOperationPair(term, newOp);
            }
        }
    }
    
    
    /**
     * <p>Returns the result of parsing the next expression
     * in the data string.</p>
     * 
     * <p>Shorthand for:</p>
     * 
     * <pre>    parseExpression(new ObjectOperationPair()).value()</pre>
     * 
     * @throws parseException
     */
    protected final Object parseExpression()
        throws ParseException
    {
        return parseExpression(new ObjectOperationPair()).value();
    }
    
    
    /**
     * <p>Extracts the next pending expression as a string
     * without performing any operations or evaluations.
     * The string will be stripped of leading and trailing
     * white space.</p>
     * 
     * <p>The parser will be positioned at the end of the
     * expression parsed.</p>
     * 
     * <p>Throws <code>ParseException</code> if parsing
     * detects errors even when evaluation is turned off.
     * In other words, the expression must be valid in a
     * formal sense.</p>
     * 
     * <p>This method is supplied as a helper method for
     * the definition of special functions that may not
     * evaluate all of their arguments.</p>
     * 
     * <p>This method adheres to the protocol specified in
     * the comments on the <code>suspend</code> member data,
     * that is, it increments <code>suspend</code> on entry
     * and decrements <code>suspend</code> on exit.</p>
     * 
     * @throws ParseException
     */
    protected final String extractExpression()
        throws ParseException
    {
        suspend++;
        
        int start = next;
        
        parseExpression();
        
        int finish = next;
        
        suspend--;
        
        return data.substring(start,finish).trim();
    }
    
    
    /**
     * <p>Parse an expression in parentheses, that is, a nested
     * expression, and return its value.</p>
     * 
     * <p>Returns <code>null</code> if the next token in the data
     * is NOT the start of a nested expression.</p>
     * 
     * @throws ParseException
     */
    protected final Object parseNestedExpression()
        throws ParseException
    {
        Object term = null;
        
        if (nextTokenIs(NESTED_EXPRESSION_START)) {
            // eat left parenthesis
            next += NESTED_EXPRESSION_START.length();
            
            // parse contained expression
            term = parseExpression();
            
            skipWhitespace();
            
            if ((next == data.length())
                || (! nextTokenIs(NESTED_EXPRESSION_END))) 
                    throw new ParseException
                    ("Expected end of nested expression", next);

            // eat right parenthesis
            next += NESTED_EXPRESSION_END.length();
            
            skipWhitespace();
        }
        
        return term;
    }
    
    
    /**
     * <p>Evaluate a constant or variable identifier
     * and return its value.</p>
     * 
     * <p>If this method is called when the member method
     * <code>evaluate()</code> returns <code>false</code>
     * then this method will return <code>null</code>.</p>
     * 
     * @throws ParseException
     */
    protected final Object evaluateIdentifier(String identifier)
        throws ParseException
    {
        if (!evaluate())
            return null;
        
        // test that this variable is in the environment
        if (!isEnvironmentID(identifier)) {
            String message = "Unrecognized constant or variable "
                + identifier;
            
            throw new ParseException(message, next);
        }
        
        // obtain variable from environment
        return getValue(identifier);
    }
    
    
    /**
     * <p>Parse assignment, that is, "set" or "let".</p>
     * 
     * <p>The syntax for "set" is:</p>
     * 
     * <pre>    set(identifier,expression)</pre>
     * 
     * <p>The syntax for "let" is:</p>
     * 
     * <pre>    let(identifier,expression)</pre>
     * 
     * <p>A variable defined by "set" will be installed in this
     * parser in a persistant way so it may be used over the
     * course of many invocations of the <code>parse</code>
     * method.</p>
     * 
     * <p>A variable defined by "let" will be installed in this
     * parser for use in the current invocation of the
     * <code>parse</code> method but will be unavailable in any
     * other invocation including any recursive invocations.
     * Thus, a "let" variable is effectively a local variable.</p>
     * 
     * <p>The method <code>parseSpecialFunction</code> calls this
     * method and selects the appropriate hash table in which to
     * store the binding information.</p>
     * 
     * <p>The identifier must be a valid identifier and must not
     * overlap with a constant or reserved id or with a
     * function name.  In that case, the expression is
     * evaluated, assigned to the identifier in the given binding,
     * and also returned as the value of this method.</p>
     * 
     * <p>This method assumes that the "set" or "let keyword has
     * already been extracted and that it is the () expression
     * that must be parsed and evaluated.</p>
     * 
     * @param binding the binding used to store the id-value expression
     * @throws ParseException
     */
    protected final Object parseAssignment(Hashtable binding)
        throws ParseException
    {
        Object[] identifierExpression = parseIdentifierExpressionList();
        
        String identifier = (String) identifierExpression[0];
        Object expression = identifierExpression[1];
        
        try {
            if (evaluate())
                assignVariable(identifier, expression, binding);
        }
        catch (IllegalArgumentException ex) {
            throw new ParseException(ex.getMessage(), next);
        }
         
        return expression;
    }
    
    
    /**
     * <p>Assigns the given value 
     * to the given identifier
     * in the given binding environment, 
     * replacing any previous value in the environment
     * associated with the same identifier.</p>
     *
     * <p>Does nothing if id is <code>null</code>
     * or binding is <code>null</code>.</p>
     * 
     * <p>Assigns the variable if value is
     * non-<code>null</code>
     * and removes the variable otherwise.</p>
     * 
     * <p>Returns value.</p>
     * 
     * <p>Throws <code>IllegalArgumentException</code> if
     * <code>isPossibleVariableID(id)</code> is false.</p>
     * 
     * @param id the identifier for the variable
     * @param value the value of the variable
     * @param binding the hash table to store the id-value binding
     * @throws IllegalArgumentException
     */
    protected final Object assignVariable
        (String id, Object value, Hashtable binding)
    {
        if ((id == null) || (binding == null))
            return null;
        
        String message = null;
        
        if (! isPossibleIdentifier(id)) {
            message =  id + " is not a valid identifier"
            + " for defining a variable";;
        }
        
        if (isReservedID(id)) {
            message = id + " may not define a variable ID"
            + " since it already defines a reserved ID";
        }
        
        if (isConstantID(id)) {
            message = id + " may not define a variable ID"
            + " since it already defines a constant ID";
        }
        
        if (isFunctionName(id)) {
            message = id + " may not define a variable ID"
            + " since it already defines a function name";
        }
        
        if (message != null)
            throw new IllegalArgumentException(message);
        
        if (value == null)
            binding.remove(id);
        else
            binding.put(id, value);
        
        return value;
    }
    
    
    /**
     * <p>Parse a function with arguments and return its value.</p>
      * 
     * @throws ParseException
     */
    protected final Object parseFunctionCall(String identifier)
        throws ParseException
    {
        // test if the identifier represents a function
        if (! isFunctionName(identifier))
            throw new ParseException
                ("Unrecognized function " + identifier, next);
        
        // get the function
        AbstractFunction function = getFunction(identifier);
        
        Object term = null;
        
        // get the argument values and evaluate the function
        try {
            Object[] values = parseArgumentList();
            
            if (evaluate())
                term = function.functionCall(values);
        }
        catch (ParseException exception) {
            throw new ParseException(exception.getMessage(), next);
        }
        
        return term;
    }
    
    
    /**
     * <p>Parse if-then-else, that is, "if".</p>
     * 
     * <p>The syntax is:</p>
     * 
     * <pre>    if(boolean,expression1,expression2)</pre>
     * 
     * <p>If evaluation is on, this special function will first
     * evaluate the boolean expression to obtain a true-false
     * value.  Evaluates either expression1 or expression2 but
     * not both and returns the result.  Returns expression1 if
     * the boolean is true otherwise returns expression2.</p>
     * 
     * <p>If evaluation is off, then all three parts of this
     * form will be extracted but not evaluated.</p>
     * 
     * <p>This method assumes that the "if" keyword has already
     * been extracted and that it is the () expression that must
     * be parsed and evaluated.</p>
     * 
     * @throws ParseException
     */
    protected final Object parseIfThenElse()
        throws ParseException
    {
        Object result = null;
        
        // eat initial argument list start token
        if (!nextTokenIs(ARGUMENT_LIST_START))
        {
            String message = "Expected start of argument list";
            throw new ParseException(message, next);
        }
        
        next += ARGUMENT_LIST_START.length();
        
        skipWhitespace();
        
        if (evaluate()) {
            Object test = parseExpression();
            
            boolean first = true;
            
            if (test instanceof XBoolean) {
                XBoolean bool = (XBoolean) test;
                first = bool.getValue();
            }
            else {
                throw new ParseException
                    ("Expected boolean test in if", next);
            }
            
            // eat argument separator token
            if (!nextTokenIs(ARGUMENT_SEPARATOR))
            {
                String message = "Expected argument separator in list";
                throw new ParseException(message, next);
            }
            
            next += ARGUMENT_SEPARATOR.length();
            
            skipWhitespace();
            
            if (first) {
                result = parseExpression();
            }
            else {
                extractExpression();
            }
            
            // eat argument separator token
            if (!nextTokenIs(ARGUMENT_SEPARATOR))
            {
                String message = "Expected argument separator in list";
                throw new ParseException(message, next);
            }
            
            next += ARGUMENT_SEPARATOR.length();
            
            skipWhitespace();
            
            if (first) {
                extractExpression();
            }
            else {
                result = parseExpression();
            }
        }
        else {
            extractExpression();
            
            // eat argument separator token
            if (!nextTokenIs(ARGUMENT_SEPARATOR))
            {
                String message = "Expected argument separator in list";
                throw new ParseException(message, next);
            }
            
            next += ARGUMENT_SEPARATOR.length();
            
            skipWhitespace();
            
            extractExpression();
            
            // eat argument separator token
            if (!nextTokenIs(ARGUMENT_SEPARATOR))
            {
                String message = "Expected argument separator in list";
                throw new ParseException(message, next);
            }
            
            next += ARGUMENT_SEPARATOR.length();
            
            skipWhitespace();
            
            extractExpression();
        }
        
        // test for argument list end token
        if (!nextTokenIs(ARGUMENT_LIST_END))
        {
            String message = "Expected end of argument list";
            throw new ParseException(message, next);
        }

        // eat argument list end token
        next += ARGUMENT_LIST_END.length();
        
        skipWhitespace();
        
        return result;
    }
    
    
    /**
     * <p>Parse the "eval" operation.</p>
     * 
     * <p>The syntax is:</p>
     * 
     * <pre>    eval(expression-1,...,expression-n)</pre>
     * 
     * <p>If evaluation is on, this special function will
     * evaluate each expression from left to right and
     * then will return the value of the rightmost
     * expression, that is, expression-n.</p>
     * 
     * <p>If evaluation is off, then the expressions will
     * be extracted but not evaluated and the return
     * value will be <code>null</code>.</p>
     * 
     * <p>This method assumes that the "eval" keyword has already
     * been extracted and that it is the () expression that must
     * be parsed and evaluated.</p>
     * 
     * <p>An important application of "eval" is to permit
     * the definition of "let" variables which are then
     * used in the final expression.  A simple example of
     * this is as follows.</p>
     * 
     * <pre>    eval(let(a,3),let(b,5),let(c,2),b*b-4*a*c)</pre>
     * 
     * <p>In this example, a, b, c are temporary variables
     * used in the final expression <code>b*b-4*a*c</code>.</p>
     * 
     * @throws ParseException
     */
    protected final Object parseEval()
        throws ParseException
    {
        Object[] values = parseArgumentList();
        
        int length = values.length;
        
        if (length == 0)
            return null;
        else
            return values[length - 1];
    }
    
    
    /**
     * <p>The "random" function is a special function
     * that takes 0, 1, or 2 numeric arguments and
     * has the following interpretation.</p>
     * 
     * <table border="1" cellpadding="5">
     *   <tr>
     *     <td style="text-align:center"><b>Function Name</b></td>
     *     <td style="text-align:center"><b>Typical Usage</b></td>
     *     <td style="text-align:center"><b>Interpretation</b></td>
     *   </tr>
     *   <tr>
     *     <td style="text-align:center;font-family:monospace">random</td>
     *     <td style="text-align:center;font-family:monospace">random()</td>
     *     <td>Random number between 0 and 1</td>
     *   </tr>
     *   <tr>
     *     <td style="text-align:center;font-family:monospace">random</td>
     *     <td style="text-align:center;font-family:monospace">random(x)</td>
     *     <td>Random number between 0 and x</td>
     *   </tr>
     *   <tr>
     *     <td style="text-align:center;font-family:monospace">random</td>
     *     <td style="text-align:center;font-family:monospace">random(x,y)</td>
     *     <td>Random number between x and y</td>
     *   </tr>
     * </table>
     * 
     * @throws ParseException
     */
    protected final Object parseRandom()
        throws ParseException
    {
        String message = null;
        
        Object[] values = parseArgumentList();
        
        if (values == null) {
            message = "Null arguments to random";
            
            throw new ParseException(message, 0);
        }
        
        int length = values.length;
        
        if (length > 2) {
            message = "random requires 0, 1, or 2 arguments";
            
            throw new ParseException(message, 0);
        }
        
        for (int i = 0; i < length; i++) {
            if (! (values[i] instanceof XNumber)) {
                message =
                    "random expects numeric argument in position " + i;
                
                throw new ParseException(message, 0);
            }
        }
        
        XDouble x = null;
        double vx = 0;
        
        XDouble y = null;
        double vy = 0;
        
        double r  = Math.random();
        
        switch(length) {
            case 0:
                return new XDouble(r);
                
            case 1:
                x = ParserUtilities.toXDouble((XNumber) values[0]);
                vx = x.getValue();
                
                return new XDouble(r * vx);
                
            case 2:
                x = ParserUtilities.toXDouble((XNumber) values[0]);
                vx = x.getValue();
                
                y = ParserUtilities.toXDouble((XNumber) values[1]);
                vy = y.getValue();
                
                return new XDouble(vx + r * (vy - vx));
                
            default:
                return null;
        }
    }
    
    
    /**
     * <p>Controls whether or not the parser evaluates or
     * simply parses formally without evaluation.</p>
     * 
     * <p>Returns true if the internal variable named
     * <code>suspend</code> is zero or negative.</p>
     */
    protected final boolean evaluate() {
        return suspend <= 0;
    }
    
    
    /**
     * <p>Parses the next argument list in the data String
     * assuming that each argument is to be evaluated and
     * returns an array of objects representing the list.</p>
     *
     * <p>By default, an argument list:</p>
     * 
     * <ul>
     *   <li>begins with an argument list start token</li>
     *   <li>is followed by a possibly zero-length list of
     *       expressions to be evaluated that are separated
     *       by argument separator tokens</li>
     *   <li>ends with an argument list end token.</li>
     * </ul>
     * 
     * @throws ParseException if the argument list is
     *      malformed
     */
    protected final Object[] parseArgumentList()
        throws ParseException
    {
        Vector values = new Vector();
        
        // eat initial argument list start token
        if (!nextTokenIs(ARGUMENT_LIST_START))
        {
            String message = "Expected start of argument list";
            throw new ParseException(message, next);
        }
        
        next += ARGUMENT_LIST_START.length();
        
        skipWhitespace();
        
        // parse individual argument expressions
        // unless this list has zero arguments
        
        boolean expecting = (!nextTokenIs(ARGUMENT_LIST_END));
        
        while (expecting) {
            // parse the individual argument expression
            values.add(parseExpression());
            
            skipWhitespace();
            
            // eat argument separator token if present
            // otherwise prepare to exit loop
            if (nextTokenIs(ARGUMENT_SEPARATOR))
                next += ARGUMENT_SEPARATOR.length();
            else
                expecting = false;
            
            skipWhitespace();
        }
        
        // test for argument list end token
        if (!nextTokenIs(ARGUMENT_LIST_END))
        {
            String message = "Expected end of argument list";
            throw new ParseException(message, next);
        }

        // eat argument list end token
        next += ARGUMENT_LIST_END.length();
        
        skipWhitespace();
        
        // return the array of parsed argument values
        return values.toArray();
    }
    
    
    /**
     * <p>Parses the next argument list that consists of
     * an identifier that is to be returned literally
     * and a value expression to be evaluated.</p>
     * 
     * <p>If the argument list ends are parentheses
     * and the argument list separator is a comma
     * then the argument list should look like:</p>
     * 
     * <pre>    (identifier,expression)</pre>
     * 
     * <p>The method returns a 2-element array with
     * the identifier and the evaluated expression.</p>
     * 
     * <p>Throws <code>ParseException</code> if the
     * next position in the data does not contain a
     * list with the required properties.</p>
     * 
     * @throws ParseException
     */
    protected final Object[] parseIdentifierExpressionList()
        throws ParseException
    {
        // eat initial argument list start token
        if (!nextTokenIs(ARGUMENT_LIST_START))
        {
            String message = "Expected start of argument list";
            throw new ParseException(message, next);
        }
        
        next += ARGUMENT_LIST_START.length();
        
        skipWhitespace();
        
        // obtain identifier
        Object identifier = nextIdentifier();
        
        if (identifier.equals(""))
        {
            String message = "Expected identifier in list";
            throw new ParseException(message, next);
        }
        
        // eat argument separator token
        if (!nextTokenIs(ARGUMENT_SEPARATOR))
        {
            String message = "Expected argument separator in list";
            throw new ParseException(message, next);
        }
        
        next += ARGUMENT_SEPARATOR.length();
        
        skipWhitespace();
        
        // obtain expression
        Object expression = parseExpression();
        
        // eat argument list end token
        if (!nextTokenIs(ARGUMENT_LIST_END))
        {
            String message = "Expected end of argument list";
            throw new ParseException(message, next);
        }

        next += ARGUMENT_LIST_END.length();
        
        skipWhitespace();
        
        return new Object [] { identifier, expression };
    }
    
    
    /**
     * Initialize the operation and precedence structures
     * prior to any additions defined in derived classes
     * by adding the IDENTITY operation at precedence 0.
     */
    protected final void initializeStructures() {
        Hashtable hashtable = new Hashtable();
        precedence.add(hashtable);
        
        operations.put("", IDENTITY);
        hashtable.put("", IDENTITY);
    }
    
    
    /**
     * <p>Adds the given operation to the operation table 
     * at the given index in the precedence list.</p>
     * 
     * <p>If an operation with the same symbol is already
     * in the operation table, that operation is removed
     * from both the operation table and the precedence
     * list before the insertion.  This is a change in
     * policy introduced in 2.5.0 to permit users to make
     * changes to a given parser more easily.</p>
     * 
     * <p>Does nothing if the given operation is
     * <code>null</code>.</p>
     * 
     * <p>Does nothing if <code>pos</code> is negative or
     * is beyond the last valid index in the precedence
     * list.</p>
     * 
     * <p>Does nothing if the symbol for this operation
     * is "" or "\0" to prevent user defined operators
     * from overriding IDENTITY or OPERATION_PREFIX.</p>
     *
     * <p>Note that IDENTITY is installed directly in
     * the constructor and that OPERATION_PREFIX is a
     * flag object that is never installed.</p>
     * 
     * @param op an operation to be added
     * @param pos the position in the precedence list
     */
    protected final void addOperation(Operation op, int pos) {
        if (op == null)
            return;
        
        if ((pos < 0) || (pos >= precedence.size()))
            return;
        
        String symbol = op.symbol();
        
        if ((symbol.equals("")) || (symbol.equals("\0")))
            return;
        
        // if an operation with the same symbol is installed
        // remove it from the precedence list here and below
        // from the operation table
        Operation oldop = (Operation) operations.get(symbol);
        
        if (oldop != null) {
            for (int i = 0; i < precedence.size(); i++) {
                Hashtable hashtable = (Hashtable)precedence.get(i);
                
                if (hashtable.containsKey(symbol))
                    hashtable.remove(symbol);
            }
        }
        
        // add the operation to the operation table
        // this will remove oldop if present
        operations.put(symbol, op);
        
        // add the operation to the precedence list
        Hashtable hashtable = (Hashtable)precedence.get(pos);
        hashtable.put(symbol, op);
        
        // add all prefixes for this operation
        int n = symbol.length() - 1;
        
        for (int i = 1; i <= n; i++) {
            String prefix = symbol.substring(0, i);
            
            if (!prefixes.containsKey(prefix))
                prefixes.put(prefix, prefix);
        }
        
        // debug
        // console.out.println("Symbol: " + symbol + " @ " + pos);
    }
    
    
    /**
     * <p>Returns the index in the precedence list 
     * corresponding to the precedence of the given operation,
     * or -1 if the given operation is <CODE>null</CODE> or
     * is not in the precedence list.</p>
     *
     * @param op the operation whose precedence is needed
     */
    protected final int precedenceOf(Operation op) {
        if (op == null)
            return -1;
        
        for (int i = 0; i < precedence.size(); i++) {
            Hashtable hashtable = (Hashtable)precedence.get(i);
            
            if (hashtable.containsKey(op.symbol()))
                return i;
        }
        
        return -1;
    }
    
    
    /**
     * <p>Performs a lookup in the operation and prefix tables 
     * to determine if the given String symbol 
     * represents an existing operation 
     * or is a prefix of the symbol for an existing operation.</p>
     *
     * <p>The return value <code>OPERATION_PREFIX</code> acts as
     * a flag to signal that the symbol is a prefix but is not an
     * operation.</p>
     *
     * @param  symbol the symbol to look up in the operation table
     * @return the <code>Operation</code> object
     *         if it is represented by the given symbol, 
     *         or the flag <CODE>OPERATION_PREFIX</CODE>
     *         if the symbol is a prefix, 
     *         or <CODE>null</CODE> if the symbol is
     *         neither an operation nor a prefix
     */
    protected final Operation isOperationOrPrefix(String symbol) {

        // try to find the given symbol in the operations table
        if (operations.containsKey(symbol))
            return (Operation)operations.get(symbol);
        
        // try to find the given symbol in the prefixes table
        if (prefixes.containsKey(symbol))
            return OPERATION_PREFIX;
        
        return null;
    }
    
    
    /**
     * <p>If the next token in the data represents an operation
     * then return the corresponding Operation
     * otherwise return <CODE>null</CODE>.</p>
     */
    protected final Operation nextOperation() {
        skipWhitespace();
            
        int end = next;
        Operation temp = null;
        Operation last = null;

        // look for operation
        while (end < data.length()) {
            end++;
                
            // test if the current substring is an operation or prefix
            // and break from the loop if it is not
            temp = isOperationOrPrefix(data.substring(next, end));
                
            if (temp == null)
                break;
            
            // if we have an operation save it
            // but also seek a longer operation string that extends it
            if (temp != OPERATION_PREFIX)
                last = temp;
        }
        
        if (last != null)
            next += last.symbol().length();
        
        skipWhitespace();
        
        return last;
    }
    
    
    /**
     * <p>Returns <CODE>true</CODE> 
     * if the given String exactly matches
     * the next non-whitespace characters in the data,
     * or <CODE>false</CODE> if it does not.</p>
     */
    protected final boolean nextTokenIs(String pattern) {
        skipWhitespace();
        
        return nextTokenIs(pattern, next);
    }
    
    
    /**
     * <p>Returns <CODE>true</CODE> 
     * if the given String exactly matches
     * the characters in the data starting at the given index,
     * or <CODE>false</CODE> if it does not.</p>
     */
    protected final boolean nextTokenIs(String pattern, int start) {
        if ((pattern == null)
            || (pattern.length() == 0)
            || ((start + pattern.length()) > data.length()))
                return false;
    
        // pattern match by contradiction
        for (int i = 0; i < pattern.length(); i++)
            if (pattern.charAt(i) != data.charAt(start + i))
                return false;
        
        // return true if no contradiction found
        return true;    
    }
    
    
    /**
     * <p>Gets the next identifier in the data
     * String and returns it.</p>
     * 
     * <p>Returns "" if the next non-whitespace
     * entity in the data String does not begin
     * with the start of an identifier.</p>
     */
    protected final String nextIdentifier() {
        // return empty string if no identifier
        if (! startsIdentifier())
            return "";
        
        int start = next;
        next++;
        
        // skip over identifier
        while (withinIdentifier())
            next++;
        
        return data.substring(start, next);
    }
    
    
    /**
     * <p>Returns true if the next non-whitespace character
     * starts an identifier.</p>
     * 
     * <p>By convention, this means the character is
     * a letter or an underscore.</p>
     */
    protected final boolean startsIdentifier() {
        skipWhitespace();
        
        if (next >= data.length())
            return false;
        
        char c = data.charAt(next);
        
        return Character.isLetter(c)
            || (c == UNDERSCORE);
    }
    
    
    /**
     * <p>Returns true if the next character is within
     * an identifier.</p>
     * 
     * <p>By convention, this means the character is
     * a letter or a digit or an underscore.</p>
     */
    protected final boolean withinIdentifier() {
        if (next >= data.length())
            return false;
        
        char c = data.charAt(next);
        
        return Character.isLetter(c)
            || Character.isDigit(c)
            || (c == UNDERSCORE);
    }
    
    
    /**
     * <p>Parses the next numeric token 
     * in the data String
     * and returns an object representation
     * of its value.</p>
     * 
     * <p>By default, a number is either of the form
     * <CODE>s(d*)</CODE>, where
     * <CODE>s</CODE> is an optional sign in <CODE>{+-}</CODE>
     * and each <CODE>d</CODE> is in <CODE>{0 .. 9}</CODE>
     * or of the form
     * <CODE>s(d*).(d*)etx</CODE> where
     * <CODE>s</CODE> is an optional sign in <CODE>{+-}</CODE>,
     * each <CODE>d</CODE> is in <CODE>{0 .. 9}</CODE>,
     * <CODE>.</CODE> is the radix point token,
     * <CODE>E</CODE> is a character in <CODE>{Ee}</CODE>,
     * <CODE>t</CODE> is an optional sign in <CODE>{+-}</CODE>,
     * and <CODE>x</CODE> is one to three characters
     * in <CODE>{0 .. 9}</CODE>.</p>
     *
     * <p>Integral values are represented by
     * <CODE>XBigInteger</CODE> objects and
     * floating point values are represented
     * by <CODE>XDouble</CODE> objects.</p>
     * 
     * @throws ParseException if the number is malformed
     */
    protected final Object parseNumber()
        throws ParseException
    {
        skipWhitespace();
        
        int end  = next;
        int type = INTEGRAL;
  
        // optional leading sign
        // if sign is '+' advance both next and end
        // if sign is '-' advance only end
        if (isSignAt(next)) {
            if (data.charAt(next) == '+')
                next++;
            
            end++;
        }
        
        // optional digits before radix point
        end = afterDigits(end);
        
        // optional radix symbol
        if (nextTokenIs(RADIX_POINT, end)) {
            // if radix present then type must be floating
            type = FLOATING;
            end += RADIX_POINT.length();
            
            // optional digits after radix
            end = afterDigits(end);
        }
        
        // optional exponent symbol
        if (isExponentAt(end)) {
            // if exponent present then type must be floating
            type = FLOATING;
            end++;
            
            // optional leading sign for exponent
            end = afterSign(end);
            
            // digits after exponent symbol
            end = afterDigits(end);
        }
        
        // now extract the possible number string
        String number = data.substring(next, end);
        next = end;
        
        // if number is a string that looks integral
        // attempt to construct an XBigInteger
        if (type == INTEGRAL) {
            try {
                BigInteger i = new BigInteger(number);
                return new XBigInteger(i);
            }
            catch (NumberFormatException ex) {
                return new ParseException(
                    "Expected valid numeric value: "
                     + number
                     + " "
                     + ex.getMessage()
                     , next);
            }
        }
        
        // if number is a string that is in floating
        // point attempt to construct an XDouble
        try {
            Double d = new Double(number);
            return new XDouble(d.doubleValue());
        }
        catch (NumberFormatException ex) {
            return new ParseException(
                "Expected valid numeric value: "
                 + number
                 + " "
                 + ex.getMessage()
                 , next);
        }
    }
    
    
    /**
     * <p>Returns true if the next non-whitespace
     * character starts a number.</p>
     * 
     * <p>Although <code>parseNumber</code> can deal
     * with leading signs, this method tests whether
     * the next non-whitespace character is a digit
     * or the <code>RADIX_POINT</code>.  Therefore,
     * if this method is called, it is assumed that
     * any leading signs have been collected prior
     * to reaching this point in the data and will
     * be handled once the rest of the number is
     * parsed.</p>
     */
    protected final boolean startsNumber() {
        skipWhitespace();
        
        if (next >= data.length())
            return false;
        
        return Character.isDigit(data.charAt(next))
            || nextTokenIs(RADIX_POINT);
    }
    
    
    /**
     * <p>Return true if the character at start is
     * <CODE>'+'</CODE> or <CODE>'-'</CODE>.</p>
     */
    protected final boolean isSignAt(int start) {
        if (start >= data.length())
            return false;
        
        char c = data.charAt(start);
        
        return (c == '+') || (c == '-');
    }
    
    
    /**
     * <p>Return true if the character at start is
     * <CODE>'E'</CODE> or <CODE>'e'</CODE>.</p>
     */
    protected final boolean isExponentAt(int start) {
        if (start >= data.length())
            return false;
        
        char c = data.charAt(start);
        
        return (c == 'E') || (c == 'e');
    }
    
    
    /**
     * <p>If the character at position start is
     * <CODE>'+'</CODE> or <CODE>'-'</CODE>
     * return (start + 1) otherwise return start.</p>
     */
    protected final int afterSign(int start) {
        return isSignAt(start) ? (start + 1) : start;
    }
    
    
    /**
     * <p>Return the first character position
     * at or after start that is not a digit.</p>
     */
    protected final int afterDigits(int start) {
        while (start < data.length()) {
            char c = data.charAt(start);
            
            if (Character.isDigit(c))
                start++;
            else
                break;
        }
        
        return start;
    }
    
    
    /**
     * <p>Skips over whitespace characters 
     * starting at the current parse position, 
     * until a non-whitespace character 
     * or the end of the data String
     * is found.</p>
     *
     * <p>By default, whitespace is defined as
     * a character considered to be whitespace
     * by the Java <CODE>Character</CODE> class.</p>
     */
    protected final void skipWhitespace() {
        while ((next < data.length()) 
            && (Character.isWhitespace(data.charAt(next))))
                next++;
    }
    
    
    /**
     * <p>Sets the token 
     * representing the start of an argument list
     * in this parsing scheme
     * to the given String token.</p>
     *
     * @param token the desired String token
     */
    protected final void setLeftParenthesisToken(String token) {
        if ((token != null) && (token.length() > 0))
            ARGUMENT_LIST_START = token;
    }
    
    
    /**
     * <p>Sets the token 
     * representing the end of an argument list
     * in this parsing scheme
     * to the given String token.</p>
     *
     * @param token a String token
     */
    protected final void setRightParenthesisToken(String token) {
        if ((token != null) && (token.length() > 0))
            ARGUMENT_LIST_END = token;
    }
    
    
    /**
     * <p>Sets the token 
     * representing the radix point
     * in this parsing scheme
     * to the given String token.</p>
     *
     * @param token a String token
     */
    protected final void setRadixPointToken(String token) {
        if ((token != null) && (token.length() > 0))
            RADIX_POINT = token;
    }
    
    
    /**
     * <p>Sets the token 
     * representing the argument separator
     * in this parsing scheme
     * to the given String token.</p>
     *
     * @param token a String token
     */
    protected final void setArgumentSeparatorToken(String token) {
        if ((token != null) && (token.length() > 0))
            ARGUMENT_SEPARATOR = token;
    }
    
    
    /////////////////
    // Debug tools //
    /////////////////
    
    /*
    
    // Prints the state of the <code>ObjectOperationPair</code>
    // in the console.
    private void showState(ObjectOperationPair ooPair) {
        console.err.println("data:           " + data);
        console.err.println("length:         " + data.length());
        console.err.println("next:           " + next);
        
        if (ooPair == null) {
            console.err.println("pair is null");
        }
        else {
            console.err.println("pair.value:     " + toString(ooPair.value));
            console.err.println("pair.operation: " + toSymbol(ooPair.operation));
        }
        
        console.err.println();
    }
    
    // Returns the value of the toString method on the given object
    // or <code>"null"</code> if the object is <code>null</code>.
    private String toString(Object s) {
        return (s != null) ? s.toString() : "null";
    }
    
    // Returns the symbol() of the given operation 
    // or <code>"null"</code> if the operation is <code>null</code>.
    private String toSymbol(Operation operation) {
        return (operation != null) ? operation.symbol() : "null";
    }
    
    */
}
