Skip to content

Commit

Permalink
Fail fast in REPL when parse is irrecoverable (#305)
Browse files Browse the repository at this point in the history
This change allows a ParseException to be identified as not recoverable,
avoiding further input in the REPL. Previously certain syntax errors
would cause the REPL to prompt "... " endlessly. Fixes #302.
  • Loading branch information
jeff5 committed Feb 18, 2024
1 parent 93911c7 commit 9e30624
Show file tree
Hide file tree
Showing 6 changed files with 89 additions and 20 deletions.
4 changes: 2 additions & 2 deletions NEWS
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ New Features

Jython 2.7.4a1 Bugs fixed
- [ GH-304 ] from java import * not working in Java 21
- [ GH-302 ] Interactive interpreter awaits input instead of raising syntax error
- [ GH-280 ] LineNumberTable deprecated for removal in 2.6 still there
- [ GH-277 ] Fix automatic conversions of function/method args for java
interfaces with default methods.
- [ GH-277 ] Argument coercion fails for Java interface with default methods
- [ GH-269 ] Upgrade Google Guava to 32.0.1 (CVE-2023-2976)
- [ GH-264 ] Create a security policy
- [ GH-254 ] Regression in socket.socket.sendall for sending Unicode
Expand Down
12 changes: 7 additions & 5 deletions grammar/Python.g
Original file line number Diff line number Diff line change
Expand Up @@ -549,7 +549,8 @@ defparameter
if ($ASSIGN != null) {
defaults.add($test.tree);
} else if (!defaults.isEmpty()) {
throw new ParseException("non-default argument follows default argument", $fpdef.tree);
throw new ParseException("non-default argument follows default argument",
$fpdef.tree, true);
}
}
;
Expand Down Expand Up @@ -878,7 +879,8 @@ continue_stmt
: CONTINUE
{
if (!$suite.isEmpty() && $suite::continueIllegal) {
errorHandler.error("'continue' not supported inside 'finally' clause", new PythonTree($continue_stmt.start));
errorHandler.error("'continue' not supported inside 'finally' clause",
new PythonTree($continue_stmt.start), true);
}
stype = new Continue($CONTINUE);
}
Expand Down Expand Up @@ -2129,7 +2131,7 @@ argument
Object oldkey = list.get(0);
if (oldkey instanceof Name && newkey instanceof Name) {
if (((Name)oldkey).getId().equals(((Name)newkey).getId())) {
errorHandler.error("keyword arguments repeated", $t1.tree);
errorHandler.error("keyword arguments repeated", $t1.tree, true);
}
}
}
Expand All @@ -2151,9 +2153,9 @@ argument
|
{
if (kws.size() > 0) {
errorHandler.error("non-keyword arg after keyword arg", $t1.tree);
errorHandler.error("non-keyword arg after keyword arg", $t1.tree, true);
} else if (afterStar) {
errorHandler.error("only named arguments may follow *expression", $t1.tree);
errorHandler.error("only named arguments may follow *expression", $t1.tree, true);
}
$arguments.add($t1.tree);
}
Expand Down
21 changes: 21 additions & 0 deletions src/org/python/antlr/ErrorHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -34,5 +34,26 @@ Object recoverFromMismatchedToken(BaseRecognizer br, IntStream input, int ttype,
stmt errorStmt(PythonTree t);

//exceptions

/**
* Handle (normally throw a {@code ParseException}) specifying whether to treat as a final
* decision. In the REPL, when {@code definite=true}, Jython will signal a definite syntax or
* semantic error, not recoverable by waiting for further input.
*
* @param message text to include.
* @param t not {@code null}, root of the {@link PythonTree} provoking the error.
* @param definite true if we cannot recover with more input.
*/
default void error(String message, PythonTree t, boolean definite) {
error(message, t);
}

/**
* Handle (normally throw a {@code ParseException}) possibly recoverable by waiting for further
* input.
*
* @param message text to include.
* @param t not {@code null}, root of the {@link PythonTree} provoking the error.
*/
void error(String message, PythonTree t);
}
15 changes: 15 additions & 0 deletions src/org/python/antlr/FailFastHandler.java
Original file line number Diff line number Diff line change
Expand Up @@ -13,49 +13,64 @@

public class FailFastHandler implements ErrorHandler {

@Override
public void reportError(BaseRecognizer br, RecognitionException re) {
throw new ParseException(message(br,re), re);
}

@Override
public void recover(Lexer lex, RecognitionException re) {
throw new ParseException(message(lex,re), re);
}

@Override
public void recover(BaseRecognizer br, IntStream input, RecognitionException re) {
throw new ParseException(message(br,re), re);
}

@Override
public boolean mismatch(BaseRecognizer br, IntStream input, int ttype, BitSet follow)
throws RecognitionException {

throw new MismatchedTokenException(ttype, input);
}

@Override
public Object recoverFromMismatchedToken(BaseRecognizer br, IntStream input, int ttype,
BitSet follow) throws RecognitionException {
throw new MismatchedTokenException(ttype, input);
}

@Override
public expr errorExpr(PythonTree t) {
throw new ParseException("Bad Expr Node", t);
}

@Override
public mod errorMod(PythonTree t) {
throw new ParseException("Bad Mod Node", t);
}

@Override
public slice errorSlice(PythonTree t) {
throw new ParseException("Bad Slice Node", t);
}

@Override
public stmt errorStmt(PythonTree t) {
throw new ParseException("Bad Stmt Node", t);
}

@Override
public void error(String message, PythonTree t) {
throw new ParseException(message, t);
}

@Override
public void error(String message, PythonTree t, boolean definite) {
throw new ParseException(message, t, definite);
}

private String message(BaseRecognizer br, RecognitionException re) {
return br.getErrorMessage(re, br.getTokenNames());
}
Expand Down
48 changes: 36 additions & 12 deletions src/org/python/antlr/ParseException.java
Original file line number Diff line number Diff line change
@@ -1,19 +1,22 @@
package org.python.antlr;

import org.antlr.runtime.IntStream;
import org.antlr.runtime.RecognitionException;
import org.antlr.runtime.Token;
import org.python.core.Py;
import org.python.core.PyObject;

import org.antlr.runtime.*;

public class ParseException extends RuntimeException {
public transient IntStream input;
public int index;
public Token token;
public Object node;
public int c;
public int line;
public int charPositionInLine;
public boolean approximateLineInfo;

public transient IntStream input;
public int index;
public Token token;
public Object node;
public int c;
public int line;
public int charPositionInLine;
public boolean approximateLineInfo;
public boolean definite;

private PyObject type = Py.SyntaxError;

Expand All @@ -32,12 +35,33 @@ public ParseException(String message) {
}

/**
* n must not be null to use this constructor
* Construct a {@code ParseException} specifying whether to treat as a final decision. When we
* use this constructor in the REPL, and {@code definite=true}, Jython will take it that the
* problem is a definite syntax or semantic error, not that we just haven't finished a
* multi-line construct that will be valid eventually.
*
* @param message text to include.
* @param n not {@code null}, root of the {@link PythonTree} provoking the error.
* @param definite true if we cannot recover with more input.
*/
public ParseException(String message, PythonTree n) {
public ParseException(String message, PythonTree n, boolean definite) {
this(message, n.getLineno(), n.getCol_offset());
this.node = n;
this.token = n.getToken();
this.definite = definite;
}

/**
* Construct a {@code ParseException} specifying message and tree. When we use this constructor
* in the REPL, Jython will assume the problem is an incomplete input (as when you wrap a line
* inside parentheses or are inside another nested construct), and a "... " prompt is likely to
* be produced.
*
* @param message text to include.
* @param n not {@code null}, root of the {@link PythonTree} provoking the error.
*/
public ParseException(String message, PythonTree n) {
this(message, n, false);
}

public ParseException(String message, RecognitionException r) {
Expand Down
9 changes: 8 additions & 1 deletion src/org/python/core/ParserFacade.java
Original file line number Diff line number Diff line change
Expand Up @@ -92,7 +92,14 @@ public static PyException fixParseError(ExpectedEncodingBufferedReader reader,
if (e.getType() == Py.IndentationError) {
return new PyIndentationError(msg, line, col, text, filename);
}
return new PySyntaxError(msg, line, col, text, filename);
PyException pye = new PySyntaxError(msg, line, col, text, filename);
if (e.definite) {
// The error cannot be fixed by reading more input
throw pye;
} else {
// The error might be fixed by reading more input
return pye;
}
} else if (t instanceof CharacterCodingException) {
String msg;
if (reader.encoding == null) {
Expand Down

0 comments on commit 9e30624

Please sign in to comment.