5

OK. ScriptEngine.eval(String string) evaluates a string in its entirety, and ScriptEngine.eval(Reader reader) evaluates the input from a Reader in its entirety.

So if I have a file, I can open a FileInputStream, wrap a Reader around it, and call scriptEngine.eval(reader).

If I have a complete statement as a string, I can call scriptEngine.eval(string).

What do I do if I need to implement an interactive interpreter? I have a user who is interactively typing in a multiline statement, e.g.

 function f() {
     return 3;
 }

If I read the input line by line, and use the String form of eval(), I'll end up passing it incomplete statements, e.g. function f() { and get an error.

If I pass in a Reader, the ScriptEngine will wait forever until the input is complete, and it's not interactive.

Help!


Just to clarify: the problem here is that I can only pass ScriptEngine.eval() complete statements, and as the customer of ScriptEngine, I don't know when an input line is complete without some help from the ScriptEngine itself.


Rhino's interactive shell uses Rhino's Context.stringIsCompilableUnit() (see LXR for usage and implementation).

2 Answers 2

4

I implemented something that works OK with Java SE 6 Rhino (Javascript) and Jython 1.5.2 (Python), using a fairly simple approach similar to Rhino's interactive shell (see my remark at the end of the question):

  • Keep a pending list of input lines not yet evaluated.
  • Try compiling (but not evaluating) the pending input lines.
    • If the compilation is OK, we may be able to execute pending input lines.
    • If the compilation throws an exception, and there is an indication of the position (line + column number) of the error, and this matches the end of the pending input, then that's a clue that we're expecting more input, so swallow the exception and wait for the next line.
    • Otherwise, we either don't know where the error is, or it happened prior to the end of the pending input, so rethrow the exception.
  • If we are not expecting any more input lines, and we only have one line of pending input, then evaluate it and restart.
  • If we are not expecting any more input lines, and the last one is a blank one (per @karakuricoder's answer) and we have more than one line of pending input, then evaluate it and restart. Python's interactive shell seems to do this.
  • Otherwise, keep reading input lines.

What I didn't want to happen is either:

  • users get annoyed having to enter extra blank lines after single-line inputs
  • users enter a long multi-line statement and only find out after the fact that there was a syntax error in the 2nd line.

Here's a helper class I wrote that implements my approach:

import java.lang.reflect.Method;
import javax.script.Bindings;
import javax.script.Compilable;
import javax.script.CompiledScript;
import javax.script.ScriptEngine;
import javax.script.ScriptException;

public class ScriptEngineInterpreter
{
    private static final boolean DEBUG = false;
    final private ScriptEngine engine;
    final private Bindings bindings;
    final private StringBuilder sb;
    private int lineNumber;
    private int pendingLineCount;
    private boolean expectingMoreInput;

    /**
     * @param engine ScriptingEngine to use in this interpreter
     * @param bindings Bindings to use in this interpreter
     */
    public ScriptEngineInterpreter(ScriptEngine engine, Bindings bindings) 
    { 
        this.engine = engine; 
        this.bindings = bindings;
        this.sb = new StringBuilder();
        this.lineNumber = 0;
        reset();
    }       
    /** @return ScriptEngine used by this interpreter */
    public ScriptEngine getEngine() { return this.engine; }
    protected void reset() { 
        this.sb.setLength(0);
        this.pendingLineCount = 0;
        setExpectingMoreInput(false);
    }
    /** @return whether the interpreter is ready for a brand new statement. */
    public boolean isReady() { return this.sb.length() == 0; }
    /**
     * @return whether the interpreter expects more input
     * 
     * A true value means there is definitely more input needed.
     * A false value means no more input is needed, but it may not yet
     * be appropriate to evaluate all the pending lines.
     * (there's some ambiguity depending on the language)
     */
    public boolean isExpectingMoreInput() { return this.expectingMoreInput; }
    protected void setExpectingMoreInput(boolean b) { this.expectingMoreInput = b; }
    /**
     * @return number of lines pending execution
     */
    protected int getPendingLineCount() { return this.pendingLineCount; }
    /**
     * @param lineIsEmpty whether the last line is empty
     * @return whether we should evaluate the pending input
     * The default behavior is to evaluate if we only have one line of input,
     * or if the user enters a blank line.
     * This behavior should be overridden where appropriate.
     */
    protected boolean shouldEvaluatePendingInput(boolean lineIsEmpty) 
    {
        if (isExpectingMoreInput())
            return false;
        else
            return (getPendingLineCount() == 1) || lineIsEmpty; 
    } 
    /**
     * @param line line to interpret
     * @return value of the line (or null if there is still pending input)
     * @throws ScriptException in case of an exception
     */
    public Object interpret(String line) throws ScriptException
    {
        ++this.lineNumber;
        if (line.isEmpty())
        {
            if (!shouldEvaluatePendingInput(true))
                return null;
        }

        ++this.pendingLineCount;        
        this.sb.append(line);
        this.sb.append("\n");
        CompiledScript cs = tryCompiling(this.sb.toString(), getPendingLineCount(), line.length());

        if (cs == null)
        {
            return null;
        }
        else if (shouldEvaluatePendingInput(line.isEmpty()))
        {
            try
            {
                Object result = cs.eval(this.bindings);
                return result;
            }
            finally
            {
                reset();
            }
        }
        else
        {
            return null;
        }
    }
    private CompiledScript tryCompiling(String string, int lineCount, int lastLineLength)
        throws ScriptException 
    {
        CompiledScript result = null;
        try
        {
            Compilable c = (Compilable)this.engine;
            result = c.compile(string);
        }
        catch (ScriptException se) {
            boolean rethrow = true;
            if (se.getCause() != null)
            {
                Integer col = columnNumber(se);
                Integer line = lineNumber(se);
                /* swallow the exception if it occurs at the last character
                 * of the input (we may need to wait for more lines)
                 */
                if (col != null
                 && line != null 
                 && line.intValue() == lineCount 
                 && col.intValue() == lastLineLength)
                {
                    rethrow = false;
                }
                else if (DEBUG)
                {
                    String msg = se.getCause().getMessage();
                    System.err.println("L"+line+" C"+col+"("+lineCount+","+lastLineLength+"): "+msg);
                    System.err.println("in '"+string+"'");
                }
            }

            if (rethrow)
            {
                reset();
                throw se;
            }
        }

        setExpectingMoreInput(result == null);
        return result;
    }
    private Integer columnNumber(ScriptException se)
    {       
        if (se.getColumnNumber() >= 0)
            return se.getColumnNumber();
        return callMethod(se.getCause(), "columnNumber", Integer.class);
    }
    private Integer lineNumber(ScriptException se)
    {       
        if (se.getLineNumber() >= 0)
            return se.getLineNumber();
        return callMethod(se.getCause(), "lineNumber", Integer.class);
    }
    static private Method getMethod(Object object, String methodName)
    {
        try {
            return object.getClass().getMethod(methodName);
        }
        catch (NoSuchMethodException e) {
            return null;
            /* gulp */ 
        }
    }
    static private <T> T callMethod(Object object, String methodName, Class<T> cl) {
        try {
            Method m = getMethod(object, methodName);
            if (m != null)
            {
                Object result = m.invoke(object); 
                return cl.cast(result);
            }
        }
        catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

}
Sign up to request clarification or add additional context in comments.

Comments

2

Create a method that reads from the keyboard (Scanner class) and creates a complete string from multiple lines of input. Enter on a blank line signals the end of user input. Pass the string into the eval method.

4 Comments

...and how does that help? How am I supposed to distinguish between an incomplete statement and a complete statement without help from the ScriptingEngine itself?
"Enter on a blank line signals the end of user input" -- ICK -- that would work, but it's annoying.
There's nothing to prevent an incomplete statement arriving from any stream be it a file, network connection or keyboard. You'll have to do some pre-verification or the eval impl will have to take the necessary actions to handle/verify the statement is not complete. Second, every stream has to have an end marker. Reading from a non-keyboard stream returns some flag value such as EOF, -1, null, etc. You can pick whatever means you want for the end of the input but there has to be a programatic way to determine the user is done.
+1 for providing an idea which has some merit. Javascript doesn't need a blank line to delimit, but Python does, since the end of a function depends on lack of continued indentation.

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.