Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion src/main/java/org/perlonjava/app/cli/Main.java
Original file line number Diff line number Diff line change
Expand Up @@ -57,7 +57,7 @@ public static void main(String[] args) {
}

String errorMessage = ErrorMessageUtil.stringifyException(t);
System.out.println(errorMessage);
System.err.print(errorMessage);

// Match system perl behavior for unhandled die:
// Prefer $! (errno) if non-zero, else prefer ($? >> 8), else 255.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -799,18 +799,18 @@ static BinaryOperatorNode parseSplit(Parser parser, LexerToken token, int curren
static BinaryOperatorNode parseJoin(Parser parser, LexerToken token, String operatorName, int currentIndex) {
Node separator;
ListNode operand;
int firstArgIndex = parser.tokenIndex;
// Handle operators with a RuntimeList operand
operand = ListParser.parseZeroOrMoreList(parser, 1, false, true, false, false);
separator = operand.elements.removeFirst();

if (token.text.equals("push") || token.text.equals("unshift")) {
// assert that separator is an `@array` or `my @array`
var op = separator;
if (op instanceof OperatorNode operatorNode && operatorNode.operator.equals("my")) {
op = operatorNode.operand;
}
if (!(op instanceof OperatorNode operatorNode && operatorNode.operator.equals("@"))) {
parser.throwError("Type of arg 1 to " + operatorName + " must be array (not constant item)");
parser.throwError(firstArgIndex, "Type of arg 1 to " + operatorName + " must be array (not constant item)");
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -305,6 +305,9 @@ static Node parseOperator(Parser parser, LexerToken token, String operator) {

return new OperatorNode(token.text, operand, parser.tokenIndex);

case "$*":
parser.throwCleanError("$* is no longer supported as of Perl 5.30");

case "$", "$#", "@", "%", "*":
// Variable sigils: $scalar, @array, %hash, *glob, $#array
return Variable.parseVariable(parser, token.text);
Expand Down
11 changes: 7 additions & 4 deletions src/main/java/org/perlonjava/frontend/parser/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import org.perlonjava.backend.jvm.EmitterContext;
import org.perlonjava.frontend.lexer.LexerToken;
import org.perlonjava.frontend.lexer.LexerTokenType;
import org.perlonjava.runtime.runtimetypes.ErrorMessageUtil;
import org.perlonjava.runtime.runtimetypes.PerlParserException;
import org.perlonjava.runtime.runtimetypes.PerlCompilerException;

Expand Down Expand Up @@ -221,15 +222,17 @@ public void throwError(String message) {
throw new PerlCompilerException(this.tokenIndex, message, this.ctx.errorUtil);
}

public void throwError(int index, String message) {
throw new PerlCompilerException(index, message, this.ctx.errorUtil);
}

/**
* Throws a clean parser error that matches Perl's exact error message format
* without additional context or stack traces.
*/
public void throwCleanError(String message) {
// Get current line number for clean error message
int lineNumber = this.ctx.errorUtil.getLineNumber(this.tokenIndex);
String fileName = this.ctx.errorUtil.getFileName();
String cleanMessage = message + " at " + fileName + " line " + lineNumber + ".";
ErrorMessageUtil.SourceLocation loc = this.ctx.errorUtil.getSourceLocationAccurate(this.tokenIndex);
String cleanMessage = message + " at " + loc.fileName() + " line " + loc.lineNumber() + ".";
throw new PerlParserException(cleanMessage);
}

Expand Down
36 changes: 30 additions & 6 deletions src/main/java/org/perlonjava/frontend/parser/SignatureParser.java
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ public class SignatureParser {
private int minParams = 0;
private int maxParams = 0;
private boolean hasSlurpy = false;
private boolean hasOptional = false;
private String namedArgsHashName = null; // Track the hash name for named parameters
private String subroutineName = null; // Optional subroutine name for error messages
private boolean isMethod = false; // True if parsing method signature (has implicit $self)
Expand Down Expand Up @@ -131,6 +132,8 @@ private ListNode parse() {
}

private void parseParameter() {
int paramStartIndex = parser.tokenIndex;

// Check for named parameter (starts with :)
boolean isNamed = false;
if (peekToken().text.equals(":")) {
Expand All @@ -144,7 +147,7 @@ private void parseParameter() {
validateSigil(sigil);

if (hasSlurpy) {
parser.throwError("Slurpy parameter not last");
parser.throwError(paramStartIndex, "Slurpy parameter not last");
}

// Check if this is a slurpy parameter
Expand All @@ -161,9 +164,23 @@ private void parseParameter() {
paramName = consumeToken().text;
}

if (paramName != null && paramName.equals("_")) {
parser.throwError(paramStartIndex, "Can't use global " + sigil + "_ in subroutine signature");
}

// Named parameters must have a name
if (isNamed && paramName == null) {
parser.throwError("Named parameter must have a name");
parser.throwError("Named parameters must actually have a name");
}

// Check for illegal operator after parameter (e.g. $b += 1)
LexerToken afterParam = peekToken();
if (paramName != null && !afterParam.text.equals(",") && !afterParam.text.equals(")")
&& !afterParam.text.equals("=") && !afterParam.text.equals("//=") && !afterParam.text.equals("||=")
&& !afterParam.text.equals("$") && !afterParam.text.equals("@") && !afterParam.text.equals("%")) {
if (afterParam.type == LexerTokenType.OPERATOR) {
parser.throwError("Illegal operator following parameter in a subroutine signature");
}
}

// Create parameter variable or undef placeholder
Expand All @@ -178,7 +195,7 @@ private void parseParameter() {
if (isSlurpy) {
handleSlurpyParameter();
} else {
handleScalarParameter(paramVariable);
handleScalarParameter(paramVariable, paramStartIndex);
}
}
}
Expand Down Expand Up @@ -215,8 +232,12 @@ private void handleSlurpyParameter() {
hasSlurpy = true;
maxParams = Integer.MAX_VALUE;

// Verify no more parameters after slurpy
LexerToken next = peekToken();
if (next.text.equals("=") || next.text.equals("//=") || next.text.equals("||=")) {
parser.throwError("A slurpy parameter may not have a default value");
}

// Verify no more parameters after slurpy
if (next.text.equals(",")) {
consumeToken(); // consume comma
next = peekToken();
Expand All @@ -230,19 +251,22 @@ private void handleSlurpyParameter() {
}
}

private void handleScalarParameter(Node paramVariable) {
private void handleScalarParameter(Node paramVariable, int paramStartIndex) {
LexerToken next = peekToken();

// Check for default value
if (next.text.equals("=") || next.text.equals("||=") || next.text.equals("//=")) {
hasOptional = true;
String defaultOp = consumeToken().text;
Node defaultValue = parseDefaultValue(paramVariable);

if (defaultValue != null) {
astNodes.add(generateDefaultAssignment(paramVariable, defaultValue, defaultOp, maxParams));
}
} else {
// Mandatory parameter
if (hasOptional) {
parser.throwError(paramStartIndex, "Mandatory parameter follows optional parameter");
}
minParams++;
}

Expand Down
16 changes: 16 additions & 0 deletions src/main/java/org/perlonjava/frontend/parser/Variable.java
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,22 @@ public static Node parseVariable(Parser parser, String sigil) {
return parseBracedVariable(parser, sigil, false);
}

// Detect bare $# (no array name follows) — deprecated since Perl 5.30
if (sigil.equals("$#")) {
LexerToken afterSigil = nextNonWsToken;
if (afterSigil.type != LexerTokenType.IDENTIFIER
&& afterSigil.type != LexerTokenType.NUMBER
&& !afterSigil.text.equals("{")
&& !afterSigil.text.equals("[")
&& !afterSigil.text.equals("$")
&& !afterSigil.text.equals("'")
&& !afterSigil.text.equals("+")
&& !afterSigil.text.equals("-")
&& !afterSigil.text.equals("^")) {
parser.throwCleanError("$# is no longer supported as of Perl 5.30");
}
}

// Normal variable parsing: try to parse an identifier
// Store the current position before parsing the identifier
int startIndex = parser.tokenIndex;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -90,10 +90,21 @@ public static String stringifyException(Throwable t, int skipLevels) {
return message != null ? message : "\n";
}

// Check if the innermost cause is a PerlCompilerException — these already
// contain fully-formatted error messages with file/line/near context and
// never have Perl stack frames, so they'd get JVM stack traces appended.
Throwable innermostCause = findInnermostCause(t);
if (innermostCause instanceof PerlCompilerException) {
String message = innermostCause.getMessage();
if (message != null && !message.endsWith("\n")) {
message += "\n";
}
return message != null ? message : "\n";
}

// Use the custom formatter to print the Perl message and stack trace
StringBuilder sb = new StringBuilder();

Throwable innermostCause = findInnermostCause(t);
String message = innermostCause.getMessage();

// Use this for debugging
Expand Down Expand Up @@ -195,14 +206,30 @@ public void setTokenIndex(int index) {
* @return the formatted error message with context
*/
public String errorMessage(int index, String message) {
// Retrieve the line number by counting newlines up to the specified index
int line = getLineNumber(index);
SourceLocation loc = getSourceLocationAccurate(index);

// Retrieve the string context around the error by collecting tokens near the specified index
String nearString = TokenUtils.toText(tokens, index - 4, index + 2);
String nearString = buildNearString(index);

// Return the formatted error message with the file name, line number, and context
return message + " at " + fileName + " line " + line + ", near " + errorMessageQuote(nearString) + "\n";
return message + " at " + loc.fileName() + " line " + loc.lineNumber() + ", near " + errorMessageQuote(nearString) + "\n";
}

private String buildNearString(int index) {
int end = Math.min(tokens.size() - 1, index + 5);
StringBuilder sb = new StringBuilder();
int nonWsCount = 0;
for (int i = index; i <= end; i++) {
LexerToken tok = tokens.get(i);
if (tok.type == LexerTokenType.EOF || tok.type == LexerTokenType.NEWLINE) break;
if (tok.text.equals("{") || tok.text.equals("}")) break;
if (tok.type != LexerTokenType.WHITESPACE) {
nonWsCount++;
if (nonWsCount > 3) break;
}
sb.append(tok.text);
}
String near = sb.toString();
near = near.replaceAll("^\\s+", "");
return near;
}

/**
Expand Down