From 491927d450e1481c483ee08c9e04a9f26911bd57 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 24 Apr 2026 13:49:01 +0200 Subject: [PATCH] fix(parser): process pending heredocs before `use`/`no` BEGIN-time evaluation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `use Module LIST;` compiles and executes LIST immediately via runSpecialBlock("BEGIN", ...) inside parseUseDeclaration. If LIST contained a heredoc operator (e.g. `use constant FOO => <<'EOT';`), the heredoc body was still un-resolved at emit time because heredoc bodies are normally filled in only when whitespace processing consumes the following NEWLINE — which had not happened yet. The emitter then threw "HEREDOC marker not found". Mirror the existing fix for explicit BEGIN { ... } blocks in SpecialBlockParser: before runSpecialBlock, if heredocs are pending, seek to the next NEWLINE, call ParseHeredoc.parseHeredocAfterNewline to fill bodies, record heredocSkipToIndex/heredocNewlineIndex so the outer whitespace handler does not re-process them, then restore tokenIndex. Observed via `jcpan -t Text::Table` where t/10_Table.t failed all 166 subtests at a `use constant T_SINGLE => <<'EOT2';` declaration. After the fix all 175 Text::Table tests pass across 7 files. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../org/perlonjava/core/Configuration.java | 4 ++-- .../frontend/parser/StatementParser.java | 23 +++++++++++++++++++ 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 74bebc7a5..7cf7384ed 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "4623fa856"; + public static final String gitCommitId = "6f96f1c74"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 24 2026 13:01:40"; + public static final String buildTimestamp = "Apr 24 2026 13:47:17"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java index 60ddc0cd6..d4587d620 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java @@ -743,6 +743,29 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) { // call Module->import( LIST ) // or Module->unimport( LIST ) + // Before executing the argument list, process any pending heredocs. + // This handles cases like: use constant FOO => <<'EOT'; ... \n heredoc content \n EOT + // The heredoc content comes after the newline that follows the ';', but the + // BEGIN-equivalent evaluation of the import list happens immediately, so we + // must fill in heredoc bodies before the list is compiled and executed. + if (!parser.getHeredocNodes().isEmpty()) { + int savedIndex = parser.tokenIndex; + int newlineIndex = -1; + for (int i = savedIndex; i < parser.tokens.size(); i++) { + if (parser.tokens.get(i).type == LexerTokenType.NEWLINE) { + newlineIndex = i; + break; + } + } + if (newlineIndex >= 0) { + parser.tokenIndex = newlineIndex; + ParseHeredoc.parseHeredocAfterNewline(parser); + parser.heredocSkipToIndex = parser.tokenIndex; + parser.heredocNewlineIndex = newlineIndex; + parser.tokenIndex = savedIndex; + } + } + // Execute the argument list immediately in LIST context // This is necessary for expressions like: use lib ($path =~ /^(.*)$/); // where the regex match must return captured groups, not just success/failure