jsr223 + написание интерпретатора скрипта

В ПОРЯДКЕ. ScriptEngine.eval(String string) оценивает строку целиком и ScriptEngine.eval(Reader reader) полностью оценивает ввод от Reader.

Итак, если у меня есть файл, я могу открыть FileInputStream, обернуть вокруг него Reader и вызвать scriptEngine.eval(reader).

Если у меня есть полный оператор в виде строки, я могу вызвать scriptEngine.eval(string).

Что делать, если мне нужно реализовать интерактивный интерпретатор? У меня есть пользователь, который интерактивно печатает многострочный оператор, например.

 function f() {
     return 3;
 }

Если я прочитаю ввод построчно и использую строковую форму eval(), я в конечном итоге передам ему неполные операторы, например. function f() { и получите ошибку.

Если я передам Reader, ScriptEngine будет ждать вечно, пока ввод не будет завершен, и это не будет интерактивным.

Помощь!


Просто поясню: проблема здесь в том, что я могу передать только ScriptEngine.eval() полных операторов, и, как клиент ScriptEngine, я не знаю, когда строка ввода завершена, без какой-либо помощи со стороны самого ScriptEngine.


Интерактивная оболочка Rhino использует Rhino. Context.stringIsCompilableUnit() (см. LXR для использование и реализация).


person Jason S    schedule 07.04.2011    source источник


Ответы (2)


Я реализовал то, что нормально работает с Java SE 6 Rhino (Javascript) и Jython 1.5.2 (Python), используя довольно простой подход, аналогичный интерактивной оболочке Rhino (см. мое замечание в конце вопроса):

  • Держите ожидающий список входных строк, которые еще не оценены.
  • Try compiling (but not evaluating) the pending input lines.
    • If the compilation is OK, we may be able to execute pending input lines.
    • Если компиляция выдает исключение, и есть указание на позицию (номер строки + столбца) ошибки, и это соответствует концу ожидающего ввода, то это признак того, что мы ожидаем больше ввода, так что проглотите исключение и дождаться следующей строки.
    • В противном случае мы либо не знаем, где ошибка, либо она произошла до окончания ожидающего ввода, поэтому повторно сгенерируйте исключение.
  • Если мы не ожидаем больше строк ввода и у нас есть только одна строка ожидающих ввода, оцените ее и перезапустите.
  • Если мы не ожидаем больше строк ввода, а последняя пустая (согласно ответу @karakuricoder), и у нас есть более одной строки ожидающих ввода, оцените ее и перезапустите. Интерактивная оболочка Python, кажется, делает это.
  • В противном случае продолжайте читать входные строки.

Чего я не хотел, так это:

  • пользователей раздражает необходимость вводить лишние пустые строки после однострочного ввода
  • пользователи вводят длинную многострочную инструкцию и узнают только после того, как была синтаксическая ошибка во 2-й строке.

Вот вспомогательный класс, который я написал, который реализует мой подход:

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;
    }

}
person Jason S    schedule 08.04.2011

Создайте метод, который читает с клавиатуры (класс Scanner) и создает полную строку из нескольких строк ввода. Ввод на пустой строке сигнализирует об окончании пользовательского ввода. Передайте строку в метод eval.

person karakuricoder    schedule 07.04.2011
comment
...и чем это поможет? Как я должен отличить неполное утверждение от полного без помощи самого ScriptingEngine? - person Jason S; 07.04.2011
comment
Ввод на пустой строке сигнализирует об окончании пользовательского ввода — ICK — это сработает, но раздражает. - person Jason S; 07.04.2011
comment
Ничто не препятствует получению неполного оператора из любого потока, будь то файл, сетевое соединение или клавиатура. Вам нужно будет выполнить некоторую предварительную проверку, или eval impl должен будет предпринять необходимые действия для обработки/проверки того, что оператор не завершен. Во-вторых, каждый поток должен иметь конечный маркер. Чтение из потока без клавиатуры возвращает некоторое значение флага, такое как EOF, -1, null и т. д. Вы можете выбрать любое значение, которое хотите для конца ввода, но должен быть программный способ определить, что пользователь сделал. - person karakuricoder; 08.04.2011
comment
+1 за идею, которая имеет некоторые достоинства. Javascript не нуждается в пустой строке для разграничения, в отличие от Python, поскольку конец функции зависит от отсутствия продолжающегося отступа. - person Jason S; 08.04.2011