diff --git a/.gitignore b/.gitignore index e424fb4ca..6f83dcd0a 100644 --- a/.gitignore +++ b/.gitignore @@ -49,6 +49,11 @@ logs/ .perlonjava_env_ready *.diff *.patch + +.windsurf/ +Image-ExifTool-13.44/ +dev/examples/DiagnoseBytecodeEstimation.pl +dev/prompts/fix_pat_advanced_verifyerror.md # But allow patch files in import-perl5/patches/ !dev/import-perl5/patches/*.patch diff --git a/dev/import-perl5/config.yaml b/dev/import-perl5/config.yaml index d16c1d618..fc2eb5e9f 100644 --- a/dev/import-perl5/config.yaml +++ b/dev/import-perl5/config.yaml @@ -62,10 +62,6 @@ imports: type: directory # Specific patched files (applied after directory import above) - - source: perl5/t/test.pl - target: perl5_t/t/test.pl - patch: test.pl.patch - - source: perl5/t/re/pat.t target: perl5_t/t/re/pat.t patch: pat.t.patch diff --git a/dev/import-perl5/patches/test.pl.patch b/dev/import-perl5/patches/test.pl.patch deleted file mode 100644 index 5eab8f277..000000000 --- a/dev/import-perl5/patches/test.pl.patch +++ /dev/null @@ -1,64 +0,0 @@ ---- perl5/t/test.pl -+++ t/test.pl -@@ -1,3 +1,10 @@ -+# -------------------------------------------- -+# Modified t/test.pl for running Perl test suite with PerlOnJava: -+# -+# - added subroutine `skip_internal` to workaround the use of non-local goto (`last SKIP`). -+# - no other changes. -+# -------------------------------------------- -+ - # - # t/test.pl - most of Test::More functionality without the fuss - -@@ -587,16 +594,44 @@ - last SKIP; - } - -+sub skip_internal { -+ my $why = shift; -+ my $n = @_ ? shift : 1; -+ my $bad_swap; -+ my $both_zero; -+ { -+ local $^W = 0; -+ $bad_swap = $why > 0 && $n == 0; -+ $both_zero = $why == 0 && $n == 0; -+ } -+ if ($bad_swap || $both_zero || @_) { -+ my $arg = "'$why', '$n'"; -+ if (@_) { -+ $arg .= join(", ", '', map { qq['$_'] } @_); -+ } -+ die qq[$0: expected skip(why, count), got skip($arg)\n]; -+ } -+ for (1..$n) { -+ _print "ok $test # skip $why\n"; -+ $test = $test + 1; -+ } -+ local $^W = 0; -+ # last SKIP; -+ 1; -+} -+ - sub skip_if_miniperl { -- skip(@_) if is_miniperl(); -+ ## PerlOnJava is not miniperl -+ # skip(@_) if is_miniperl(); - } - - sub skip_without_dynamic_extension { -- my $extension = shift; -- skip("no dynamic loading on miniperl, no extension $extension", @_) -- if is_miniperl(); -- return if &_have_dynamic_extension($extension); -- skip("extension $extension was not built", @_); -+ ## PerlOnJava has dynamic extension -+ # my $extension = shift; -+ # skip("no dynamic loading on miniperl, no extension $extension", @_) -+ # if is_miniperl(); -+ # return if &_have_dynamic_extension($extension); -+ # skip("extension $extension was not built", @_); - } - - sub todo_skip { diff --git a/src/main/java/org/perlonjava/parser/StatementParser.java b/src/main/java/org/perlonjava/parser/StatementParser.java index 18fcb22e7..f19332eb5 100644 --- a/src/main/java/org/perlonjava/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/parser/StatementParser.java @@ -237,9 +237,6 @@ public static Node parseIfStatement(Parser parser) { elseBranch = parseIfStatement(parser); } - // Use a macro to emulate Test::More SKIP blocks - TestMoreHelper.handleSkipTest(parser, thenBranch); - return new IfNode(operator.text, condition, thenBranch, elseBranch, parser.tokenIndex); } diff --git a/src/main/java/org/perlonjava/parser/StatementResolver.java b/src/main/java/org/perlonjava/parser/StatementResolver.java index fbca51260..d8a62a5d3 100644 --- a/src/main/java/org/perlonjava/parser/StatementResolver.java +++ b/src/main/java/org/perlonjava/parser/StatementResolver.java @@ -572,11 +572,6 @@ yield dieWarnNode(parser, "die", new ListNode(List.of( parser.ctx.symbolTable.exitScope(scopeIndex); - if (label != null && label.equals("SKIP")) { - // Use a macro to emulate Test::More SKIP blocks - TestMoreHelper.handleSkipTest(parser, block); - } - yield new For3Node(label, true, null, null, diff --git a/src/main/java/org/perlonjava/parser/TestMoreHelper.java b/src/main/java/org/perlonjava/parser/TestMoreHelper.java deleted file mode 100644 index 75d775021..000000000 --- a/src/main/java/org/perlonjava/parser/TestMoreHelper.java +++ /dev/null @@ -1,52 +0,0 @@ -package org.perlonjava.parser; - -import org.perlonjava.astnode.*; -import org.perlonjava.runtime.GlobalVariable; -import org.perlonjava.runtime.NameNormalizer; - -import java.util.List; - -public class TestMoreHelper { - - // Use a macro to emulate Test::More SKIP blocks - static void handleSkipTest(Parser parser, BlockNode block) { - // Locate skip statements - // TODO create skip visitor - for (Node node : block.elements) { - if (node instanceof BinaryOperatorNode op) { - if (!op.operator.equals("(")) { - // Possible if-modifier - if (op.left instanceof BinaryOperatorNode left) { - handleSkipTestInner(parser, left); - } - if (op.right instanceof BinaryOperatorNode right) { - handleSkipTestInner(parser, right); - } - } else { - handleSkipTestInner(parser, op); - } - } - } - } - - private static void handleSkipTestInner(Parser parser, BinaryOperatorNode op) { - if (op.operator.equals("(")) { - int index = op.tokenIndex; - if (op.left instanceof OperatorNode sub && sub.operator.equals("&") && sub.operand instanceof IdentifierNode subName && subName.name.equals("skip")) { - // skip() call - // op.right contains the arguments - - // Becomes: `skip_internal() && last SKIP` - // But first, test if the subroutine exists - String fullName = NameNormalizer.normalizeVariableName(subName.name + "_internal", parser.ctx.symbolTable.getCurrentPackage()); - if (GlobalVariable.existsGlobalCodeRef(fullName)) { - subName.name = fullName; - op.operator = "&&"; - op.left = new BinaryOperatorNode("(", op.left, op.right, index); - op.right = new OperatorNode("last", - new ListNode(List.of(new IdentifierNode("SKIP", index)), index), index); - } - } - } - } -} diff --git a/src/main/java/org/perlonjava/runtime/RuntimeControlFlowRegistry.java b/src/main/java/org/perlonjava/runtime/RuntimeControlFlowRegistry.java index 7a162e5d8..f28529b6d 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimeControlFlowRegistry.java +++ b/src/main/java/org/perlonjava/runtime/RuntimeControlFlowRegistry.java @@ -104,11 +104,40 @@ public static boolean checkGoto(String label) { } /** - * Check if there's a control flow marker that matches this loop, and return an action code. - * This is an ultra-simplified version that does all checking in one call to avoid ASM issues. + * Check if the current marker (if any) matches the given label. + * Does NOT clear the marker - just checks if it matches. * - * @param labelName The loop's label (null for unlabeled) - * @return 0=no action, 1=LAST, 2=NEXT, 3=REDO, 4=GOTO (leave in registry) + * @param labelName The label to check against + * @return true if there's a marker and it matches this label + */ + public static boolean markerMatchesLabel(String labelName) { + ControlFlowMarker marker = currentMarker.get(); + if (marker == null) { + return false; + } + + // Check if marker's label matches (Perl semantics) + if (marker.label == null) { + // Unlabeled control flow matches any loop + return true; + } else if (labelName == null) { + // Labeled control flow doesn't match unlabeled loop + return false; + } else { + // Both labeled - must match exactly + return marker.label.equals(labelName); + } + } + + /** + * Check if there's a pending control flow marker for a specific loop label. + * If the marker matches, clear it and return the action code. + * If it doesn't match, leave it for an outer loop. + * + * This is called at loop boundaries to check for non-local control flow. + * + * @param labelName The label of the current loop (null for unlabeled loops) + * @return Action code: 0=no match, 1=LAST, 2=NEXT, 3=REDO */ public static int checkLoopAndGetAction(String labelName) { ControlFlowMarker marker = currentMarker.get(); diff --git a/src/main/perl/lib/Test/More.pm b/src/main/perl/lib/Test/More.pm index 6ef2e2be9..8a57a2069 100644 --- a/src/main/perl/lib/Test/More.pm +++ b/src/main/perl/lib/Test/More.pm @@ -286,8 +286,15 @@ sub BAIL_OUT { exit 255; } -sub skip { - die "Test::More::skip() is not implemented"; +sub skip($;$) { + my ($name, $count) = @_; + $count ||= 1; + for (1..$count) { + $Test_Count++; + my $result = "ok"; + print "$Test_Indent$result $Test_Count # skip $name\n"; + } + last SKIP; } # Workaround to avoid non-local goto (last SKIP). diff --git a/src/test/resources/unit/skip_control_flow.t b/src/test/resources/unit/skip_control_flow.t new file mode 100644 index 000000000..c5ddf0ee3 --- /dev/null +++ b/src/test/resources/unit/skip_control_flow.t @@ -0,0 +1,185 @@ +#!/usr/bin/env perl +use strict; +use warnings; + +# Minimal TAP without Test::More (we need this to work even when skip()/TODO are broken) +my $t = 0; +sub ok_tap { + my ($cond, $name) = @_; + $t++; + print(($cond ? "ok" : "not ok"), " $t - $name\n"); +} + +# 1) Single frame - MYLABEL +{ + my $out = ''; + sub test_once { last MYLABEL } + MYLABEL: { + $out .= 'A'; + test_once(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last MYLABEL exits MYLABEL block (single frame)'); +} + +# 2) Two frames, scalar context - MYLABEL +{ + my $out = ''; + sub inner2 { last MYLABEL } + sub outer2 { my $x = inner2(); return $x; } + MYLABEL: { + $out .= 'A'; + my $r = outer2(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last MYLABEL exits MYLABEL block (2 frames, scalar context)'); +} + +# 3) Two frames, void context - MYLABEL +{ + my $out = ''; + sub innerv { last MYLABEL } + sub outerv { innerv(); } + MYLABEL: { + $out .= 'A'; + outerv(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last MYLABEL exits MYLABEL block (2 frames, void context)'); +} + +# 4) Single frame - LABEL2 +{ + my $out = ''; + sub test2_once { last LABEL2 } + LABEL2: { + $out .= 'A'; + test2_once(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last LABEL2 exits LABEL2 block (single frame)'); +} + +# 5) Two frames, scalar context - LABEL2 +{ + my $out = ''; + sub inner_label2 { last LABEL2 } + sub outer_label2 { my $x = inner_label2(); return $x; } + LABEL2: { + $out .= 'A'; + my $r = outer_label2(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last LABEL2 exits LABEL2 block (2 frames, scalar context)'); +} + +# 6) Two frames, void context - LABEL2 +{ + my $out = ''; + sub innerv_label2 { last LABEL2 } + sub outerv_label2 { innerv_label2(); } + LABEL2: { + $out .= 'A'; + outerv_label2(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last LABEL2 exits LABEL2 block (2 frames, void context)'); +} + +# 7) Single frame - LABEL3 +{ + my $out = ''; + sub test3_once { last LABEL3 } + LABEL3: { + $out .= 'A'; + test3_once(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last LABEL3 exits LABEL3 block (single frame)'); +} + +# 8) Two frames, scalar context - LABEL3 +{ + my $out = ''; + sub inner_label3 { last LABEL3 } + sub outer_label3 { my $x = inner_label3(); return $x; } + LABEL3: { + $out .= 'A'; + my $r = outer_label3(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last LABEL3 exits LABEL3 block (2 frames, scalar context)'); +} + +# 9) Two frames, void context - LABEL3 +{ + my $out = ''; + sub innerv_label3 { last LABEL3 } + sub outerv_label3 { innerv_label3(); } + LABEL3: { + $out .= 'A'; + outerv_label3(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last LABEL3 exits LABEL3 block (2 frames, void context)'); +} + +# 10) Stale marker bug - labeled block in eval leaves marker +{ + my $out = ''; + # This eval creates a labeled block that might leave a stale marker + eval "\${\x{30cd}single:\x{30cd}colon} = 'test'"; + $out .= 'A'; + + # This SKIP block should work normally, not be affected by stale marker + MYLABEL: { + $out .= 'B'; + $out .= 'C'; + } + $out .= 'D'; + ok_tap($out eq 'ABCD', 'labeled block in eval does not leave stale marker'); +} + +# 11) Registry clearing bug - large SKIP block (>3 statements) with skip() +{ + my $out = ''; + my $count = 0; + + # SKIP block with >3 statements (so registry check won't run inside) + # But registry clearing at exit WILL run + SKIP: { + my $a = 1; # statement 1 + my $b = 2; # statement 2 + my $c = 3; # statement 3 + my $d = 4; # statement 4 + $out .= 'S'; + last SKIP; # This sets a marker, but block has >3 statements so no check + $out .= 'X'; + } + # When SKIP exits, registry is cleared unconditionally + # This removes the marker that was correctly set by last SKIP + + $out .= 'A'; + + # This loop should run 3 times + for my $i (1..3) { + INNER: { + $out .= 'L'; + $count++; + } + } + + $out .= 'B'; + ok_tap($out eq 'SALLLB' && $count == 3, 'large SKIP block does not break subsequent loops'); +} + +print "1..$t\n";