diff --git a/src/main/java/org/perlonjava/app/cli/Main.java b/src/main/java/org/perlonjava/app/cli/Main.java index 2a77d42a4..1674305aa 100644 --- a/src/main/java/org/perlonjava/app/cli/Main.java +++ b/src/main/java/org/perlonjava/app/cli/Main.java @@ -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. diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index 57aea0258..3bf28c7a6 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -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)"); } } diff --git a/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java b/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java index e0071c4d9..440f67512 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java +++ b/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java @@ -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); diff --git a/src/main/java/org/perlonjava/frontend/parser/Parser.java b/src/main/java/org/perlonjava/frontend/parser/Parser.java index 5e89e4cda..7aea5ce21 100644 --- a/src/main/java/org/perlonjava/frontend/parser/Parser.java +++ b/src/main/java/org/perlonjava/frontend/parser/Parser.java @@ -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; @@ -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); } diff --git a/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java b/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java index 7f88d7934..aee0dba46 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SignatureParser.java @@ -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) @@ -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(":")) { @@ -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 @@ -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 @@ -178,7 +195,7 @@ private void parseParameter() { if (isSlurpy) { handleSlurpyParameter(); } else { - handleScalarParameter(paramVariable); + handleScalarParameter(paramVariable, paramStartIndex); } } } @@ -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(); @@ -230,11 +251,12 @@ 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); @@ -242,7 +264,9 @@ private void handleScalarParameter(Node paramVariable) { astNodes.add(generateDefaultAssignment(paramVariable, defaultValue, defaultOp, maxParams)); } } else { - // Mandatory parameter + if (hasOptional) { + parser.throwError(paramStartIndex, "Mandatory parameter follows optional parameter"); + } minParams++; } diff --git a/src/main/java/org/perlonjava/frontend/parser/Variable.java b/src/main/java/org/perlonjava/frontend/parser/Variable.java index 9b076f0a0..205dde18e 100644 --- a/src/main/java/org/perlonjava/frontend/parser/Variable.java +++ b/src/main/java/org/perlonjava/frontend/parser/Variable.java @@ -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; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java index b049aad4e..55823abaa 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java @@ -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 @@ -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; } /**