From b58f828da1d7018b688ce3c38a070f09e1a1c9bc Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 15 May 2026 10:45:31 +0200 Subject: [PATCH] fix(parser): parse `` as readline on numeric filehandles (open 0) Perl lexes `<0>` as NUMBER not IDENTIFIER, so the diamond parser fell through to quote-like parsing and produced the string "0". That broke Acme::Buffy (and any `open 0; ... <0>` idiom): the script body was never read. Treat all-digit glob names (e.g. main::0) as bareword handles and handle `` in parseDiamondOperator. Generated with [Cursor](https://cursor.com/docs) Co-Authored-By: Cursor Co-authored-by: Cursor --- .../frontend/parser/FileHandle.java | 21 ++++++++++++++++++- .../frontend/parser/OperatorParser.java | 18 ++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/src/main/java/org/perlonjava/frontend/parser/FileHandle.java b/src/main/java/org/perlonjava/frontend/parser/FileHandle.java index 93112f692..6edc378a4 100644 --- a/src/main/java/org/perlonjava/frontend/parser/FileHandle.java +++ b/src/main/java/org/perlonjava/frontend/parser/FileHandle.java @@ -271,7 +271,8 @@ public static Node parseBarewordHandle(Parser parser, String name) { // Check if this is a known file handle in the global I/O table // This helps distinguish between file handles and other barewords - if (GlobalVariable.existsGlobalIO(name) || isStandardFilehandle(name)) { + if (GlobalVariable.existsGlobalIO(name) || isStandardFilehandle(name) + || isAllDigitGlobName(name)) { // Create a GLOB reference for the file handle, like `\*FH` return new OperatorNode("\\", new OperatorNode("*", @@ -280,6 +281,24 @@ public static Node parseBarewordHandle(Parser parser, String name) { return null; } + /** + * Perl allows numeric typeglob names such as {@code open 0; print <0>}, where filehandle + * {@code 0} shares the {@code *0} stash entry with {@code $0} (program name). + */ + private static boolean isAllDigitGlobName(String normalizedName) { + int idx = normalizedName.lastIndexOf("::"); + String base = idx >= 0 ? normalizedName.substring(idx + 2) : normalizedName; + if (base.isEmpty()) { + return false; + } + for (int i = 0; i < base.length(); i++) { + if (!Character.isDigit(base.charAt(i))) { + return false; + } + } + return true; + } + /** * Checks if a normalized name represents a standard filehandle. * diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index cdd09961f..278657407 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -139,6 +139,24 @@ static Node parseDiamondOperator(Parser parser, LexerToken token) { return diamond; } + // <0>, <1>, … — numeric filehandles (e.g. Acme::Buffy "open 0" reads $0) + if (operand.type == NUMBER && tokenText.matches("[0-9]+") + && parser.tokens.get(parser.tokenIndex + 1).text.equals(">")) { + parser.tokenIndex++; // consume NUMBER + TokenUtils.consume(parser); // consume '>' + String digitName = operand.text; + GlobalVariable.getGlobalIO(FileHandle.normalizeBarewordHandle(parser, digitName)); + Node globRef = FileHandle.parseBarewordHandle(parser, digitName); + if (globRef != null) { + BinaryOperatorNode readlineNode = new BinaryOperatorNode("readline", + globRef, + new ListNode(parser.tokenIndex), currentTokenIndex); + readlineNode.setAnnotation("handleName", digitName); + return readlineNode; + } + parser.tokenIndex = currentTokenIndex; + } + // Check if the token looks like a Bareword file handle if (operand.type == IDENTIFIER) { Node fileHandle = FileHandle.parseFileHandle(parser);