diff --git a/.github/workflows/gradle.yml b/.github/workflows/gradle.yml index 45cfeb8c4..6affe109b 100644 --- a/.github/workflows/gradle.yml +++ b/.github/workflows/gradle.yml @@ -28,8 +28,8 @@ jobs: uses: gradle/actions/setup-gradle@v4 # Use Makefile's 'ci' target which handles platform differences - # Windows: builds without tests to avoid Gradle daemon socket errors - # Linux: full build with tests + # Windows: builds shadowJar and runs unit tests with jperl.bat + # Linux: full Gradle build with tests - name: Build with Make (Windows) if: runner.os == 'Windows' shell: cmd diff --git a/Makefile b/Makefile index d96a25f0c..7dccce6e0 100644 --- a/Makefile +++ b/Makefile @@ -5,7 +5,12 @@ all: build # CI build - optimized for CI/CD environments ci: wrapper ifeq ($(OS),Windows_NT) - mvn clean test -B + gradlew.bat clean compileJava shadowJar --no-daemon --stacktrace + @echo "Running unit tests with jperl.bat..." + @for test in src/test/resources/unit/*.t; do \ + echo "Testing $$test" && \ + ./jperl.bat "$$test" || exit 1; \ + done else ./gradlew build --no-daemon --stacktrace endif diff --git a/src/main/java/org/perlonjava/codegen/EmitBlock.java b/src/main/java/org/perlonjava/codegen/EmitBlock.java index 2dbf29cfb..166ce3ec1 100644 --- a/src/main/java/org/perlonjava/codegen/EmitBlock.java +++ b/src/main/java/org/perlonjava/codegen/EmitBlock.java @@ -99,6 +99,44 @@ public static void emitBlock(EmitterVisitor emitterVisitor, BlockNode node) { element.accept(voidVisitor); } + // Check for non-local control flow after each statement in labeled blocks + // Only for simple blocks to avoid ASM VerifyError + if (node.isLoop && node.labelName != null && i < list.size() - 1 && list.size() <= 3) { + // Check if block contains loop constructs (they handle their own control flow) + boolean hasLoopConstruct = false; + for (Node elem : list) { + if (elem instanceof For1Node || elem instanceof For3Node) { + hasLoopConstruct = true; + break; + } + } + + if (!hasLoopConstruct) { + Label continueBlock = new Label(); + + // if (!RuntimeControlFlowRegistry.hasMarker()) continue + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/RuntimeControlFlowRegistry", + "hasMarker", + "()Z", + false); + mv.visitJumpInsn(Opcodes.IFEQ, continueBlock); + + // Has marker: check if it matches this loop + mv.visitLdcInsn(node.labelName); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/RuntimeControlFlowRegistry", + "checkLoopAndGetAction", + "(Ljava/lang/String;)I", + false); + + // If action != 0, jump to nextLabel (exit block) + mv.visitJumpInsn(Opcodes.IFNE, nextLabel); + + mv.visitLabel(continueBlock); + } + } + // NOTE: Registry checks are DISABLED in EmitBlock because: // 1. They cause ASM frame computation errors in nested/refactored code // 2. Bare labeled blocks (like TODO:) don't need non-local control flow diff --git a/src/main/java/org/perlonjava/operators/ModuleOperators.java b/src/main/java/org/perlonjava/operators/ModuleOperators.java index 2c94a7c12..6b89bba1a 100644 --- a/src/main/java/org/perlonjava/operators/ModuleOperators.java +++ b/src/main/java/org/perlonjava/operators/ModuleOperators.java @@ -519,13 +519,10 @@ else if (code == null) { String resourcePath = "/lib/" + fileName; URL resource = RuntimeScalar.class.getResource(resourcePath); if (resource != null) { - String path = resource.getPath(); - // Remove leading slash if on Windows - if (SystemUtils.osIsWindows() && path.startsWith("/")) { - path = path.substring(1); - } - fullName = Paths.get(path); - actualFileName = fullName.toString(); + // For JAR resources, use the resource path directly instead of converting to filesystem path + // This avoids Windows path issues with colons in JAR URLs like "file:/D:/..." + actualFileName = resourcePath; + fullName = null; // No filesystem path for JAR resources try (InputStream is = resource.openStream(); BufferedReader reader = new BufferedReader(new InputStreamReader(is))) { 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/perl/lib/Test/More.pm b/src/main/perl/lib/Test/More.pm index 6ef2e2be9..a7745949d 100644 --- a/src/main/perl/lib/Test/More.pm +++ b/src/main/perl/lib/Test/More.pm @@ -16,7 +16,6 @@ our @EXPORT = qw( pass fail diag note done_testing is_deeply subtest use_ok require_ok BAIL_OUT skip - skip_internal eq_array eq_hash eq_set ); @@ -287,20 +286,27 @@ sub BAIL_OUT { } sub skip { - die "Test::More::skip() is not implemented"; -} - -# Workaround to avoid non-local goto (last SKIP). -# The skip_internal subroutine is called from a macro in TestMoreHelper.java -# -sub skip_internal { - my ($name, $count) = @_; - for (1..$count) { - $Test_Count++; - my $result = "ok"; - print "$Test_Indent$result $Test_Count # skip $name\n"; + 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) { + ok(1, "# skip $why"); } - return 1; + local $^W = 0; + last SKIP; } # Legacy comparison functions - simple implementations using is_deeply 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..ec521b15a --- /dev/null +++ b/src/test/resources/unit/skip_control_flow.t @@ -0,0 +1,54 @@ +#!/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 +{ + my $out = ''; + sub skip_once { last SKIP } + SKIP: { + $out .= 'A'; + skip_once(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last SKIP exits SKIP block (single frame)'); +} + +# 2) Two frames, scalar context +{ + my $out = ''; + sub inner2 { last SKIP } + sub outer2 { my $x = inner2(); return $x; } + SKIP: { + $out .= 'A'; + my $r = outer2(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last SKIP exits SKIP block (2 frames, scalar context)'); +} + +# 3) Two frames, void context +{ + my $out = ''; + sub innerv { last SKIP } + sub outerv { innerv(); } + SKIP: { + $out .= 'A'; + outerv(); + $out .= 'B'; + } + $out .= 'C'; + ok_tap($out eq 'AC', 'last SKIP exits SKIP block (2 frames, void context)'); +} + +print "1..$t\n";