diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 9b583f475..c6f762788 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -68,7 +68,9 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int callContext, String subroutineName) { // Track interpreter state for stack traces String framePackageName = code.packageName != null ? code.packageName : "main"; - String frameSubName = subroutineName != null ? subroutineName : (code.subName != null ? code.subName : "(eval)"); + // Prefer code.subName (set by set_subname) over passed subroutineName + // This ensures caller() returns the name set by set_subname() + String frameSubName = code.subName != null ? code.subName : (subroutineName != null ? subroutineName : "(eval)"); // Get PC holder for direct updates (avoids ThreadLocal lookups in hot loop) int[] pcHolder = InterpreterState.push(code, framePackageName, frameSubName); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 37e3108de..1bfcde5b3 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1217,7 +1217,12 @@ private static void visitLength(BytecodeCompiler bc, OperatorNode node) { } private static void visitDiamond(BytecodeCompiler bc, OperatorNode node) { - String argument = ((StringNode) ((ListNode) node.operand).elements.getFirst()).value; + // Defensive: ensure operand is a ListNode with a StringNode element + String argument = ""; + if (node.operand instanceof ListNode listNode && !listNode.elements.isEmpty() + && listNode.elements.getFirst() instanceof StringNode stringNode) { + argument = stringNode.value; + } if (argument.isEmpty() || argument.equals("<>")) { bc.compileNode(node.operand, -1, RuntimeContextType.SCALAR); int fhReg = bc.lastResultReg; diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java index d203e6e34..eacd4480f 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitControlFlow.java @@ -54,7 +54,14 @@ static void handleNextOperator(EmitterContext ctx, OperatorNode node) { // Initialize label string for labeled loops String labelStr = null; - ListNode labelNode = (ListNode) node.operand; + // Defensive: ensure operand is a ListNode (parser should always create ListNode here) + ListNode labelNode; + if (node.operand instanceof ListNode) { + labelNode = (ListNode) node.operand; + } else { + // Wrap non-ListNode in a ListNode to handle edge cases + labelNode = ListNode.makeList(node.operand); + } if (!labelNode.elements.isEmpty()) { // Handle 'next' with a label. Node arg = labelNode.elements.getFirst(); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index eb88f86ae..eea289b00 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -465,7 +465,13 @@ static void handleSystemBuiltin(EmitterVisitor emitterVisitor, OperatorNode node // static RuntimeBase reverse(RuntimeBase value, int ctx) if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("handleSystemBuiltin " + node); - ListNode operand = (ListNode) node.operand; + // Defensive: ensure operand is a ListNode + ListNode operand; + if (node.operand instanceof ListNode) { + operand = (ListNode) node.operand; + } else { + operand = ListNode.makeList(node.operand); + } boolean hasHandle = false; if (operand.handle != null) { // `system {handle} LIST` @@ -584,7 +590,12 @@ static void handleMapOperator(EmitterVisitor emitterVisitor, BinaryOperatorNode // Handles the 'diamond' operator, which reads input from a file or standard input. static void handleDiamondBuiltin(EmitterVisitor emitterVisitor, OperatorNode node) { MethodVisitor mv = emitterVisitor.ctx.mv; - String argument = ((StringNode) ((ListNode) node.operand).elements.getFirst()).value; + // Defensive: ensure operand is a ListNode with a StringNode element + String argument = ""; + if (node.operand instanceof ListNode listNode && !listNode.elements.isEmpty() + && listNode.elements.getFirst() instanceof StringNode stringNode) { + argument = stringNode.value; + } if (CompilerOptions.DEBUG_ENABLED) emitterVisitor.ctx.logDebug("visit diamond " + argument); if (argument.isEmpty() || argument.equals("<>")) { // Handle null filehandle: <> <<>> @@ -1030,6 +1041,40 @@ static void handleTimeRelatedOperator(EmitterVisitor emitterVisitor, OperatorNod emitOperator(node, emitterVisitor); } + /** + * Handle the caller() operator with __SUB__ support for set_subname. + * Pushes: args, context, __SUB__ + */ + static void handleCallerOperator(EmitterVisitor emitterVisitor, OperatorNode node) { + if (node.operand != null) { + node.operand.accept(emitterVisitor.with(RuntimeContextType.LIST)); + } + emitterVisitor.pushCallContext(); + + // Push __SUB__ for set_subname support + MethodVisitor mv = emitterVisitor.ctx.mv; + String className = emitterVisitor.ctx.javaClassInfo.javaClassName; + + // Load 'this' (the current RuntimeCode instance) + mv.visitVarInsn(Opcodes.ALOAD, 0); + + // Retrieve this.__SUB__ + mv.visitFieldInsn(Opcodes.GETFIELD, + className, + "__SUB__", + "Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); + + // Create Perl undef if null (for code not inside a subroutine) + mv.visitMethodInsn( + Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", + "selfReferenceMaybeNull", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", + false); + + emitOperator(node, emitterVisitor); + } + static void handlePrototypeOperator(EmitterVisitor emitterVisitor, OperatorNode node) { node.operand.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); emitterVisitor.pushCurrentPackage(); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java index 64d1c1c64..680944b8d 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorNode.java @@ -97,8 +97,9 @@ public static void emitOperatorNode(EmitterVisitor emitterVisitor, OperatorNode case "time", "wait" -> EmitOperator.handleTimeOperator(emitterVisitor, node); case "wantarray" -> EmitOperator.handleWantArrayOperator(emitterVisitor, node); case "undef" -> EmitOperator.handleUndefOperator(emitterVisitor, node); - case "gmtime", "localtime", "caller", "reset", "select", "times" -> + case "gmtime", "localtime", "reset", "select", "times" -> EmitOperator.handleTimeRelatedOperator(emitterVisitor, node); + case "caller" -> EmitOperator.handleCallerOperator(emitterVisitor, node); case "prototype" -> EmitOperator.handlePrototypeOperator(emitterVisitor, node); case "require" -> EmitOperator.handleRequireOperator(emitterVisitor, node); case "doFile" -> EmitOperator.handleDoFileOperator(emitterVisitor, node); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java b/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java index 9c386521e..2ea148a94 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java @@ -155,7 +155,10 @@ static void handleSystemCommand(EmitterVisitor emitterVisitor, OperatorNode node * Example: $string =~ tr/abc/def/ */ static void handleTransliterate(EmitterVisitor emitterVisitor, OperatorNode node) { - ListNode operand = (ListNode) node.operand; + // Defensive: ensure operand is a ListNode + ListNode operand = (node.operand instanceof ListNode) + ? (ListNode) node.operand + : ListNode.makeList(node.operand); EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR); // Process the three required components: source, target, and flags @@ -186,7 +189,10 @@ static void handleTransliterate(EmitterVisitor emitterVisitor, OperatorNode node * Example: $string =~ s/pattern/replacement/ */ static void handleReplaceRegex(EmitterVisitor emitterVisitor, OperatorNode node) { - ListNode operand = (ListNode) node.operand; + // Defensive: ensure operand is a ListNode + ListNode operand = (node.operand instanceof ListNode) + ? (ListNode) node.operand + : ListNode.makeList(node.operand); EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR); // Process pattern, replacement, and flags @@ -224,7 +230,10 @@ static void handleReplaceRegex(EmitterVisitor emitterVisitor, OperatorNode node) * Example: qr/pattern/ */ static void handleQuoteRegex(EmitterVisitor emitterVisitor, OperatorNode node) { - ListNode operand = (ListNode) node.operand; + // Defensive: ensure operand is a ListNode + ListNode operand = (node.operand instanceof ListNode) + ? (ListNode) node.operand + : ListNode.makeList(node.operand); EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR); // Process pattern and flags @@ -246,7 +255,10 @@ static void handleQuoteRegex(EmitterVisitor emitterVisitor, OperatorNode node) { * Example: $string =~ m/pattern/ */ static void handleMatchRegex(EmitterVisitor emitterVisitor, OperatorNode node) { - ListNode operand = (ListNode) node.operand; + // Defensive: ensure operand is a ListNode + ListNode operand = (node.operand instanceof ListNode) + ? (ListNode) node.operand + : ListNode.makeList(node.operand); EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR); // Check if /o modifier is present diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 0cc6af171..8c9c56bce 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 = "c4e439b01"; + public static final String gitCommitId = "4473efe87"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). diff --git a/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java b/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java index 035b2b06c..b14be2529 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java +++ b/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java @@ -186,9 +186,13 @@ private static Node parseIdentifier(Parser parser, int startIndex, LexerToken to OperatorNode codeRef = new OperatorNode("&", new IdentifierNode(coreGlobalName, startIndex), startIndex); + // Defensive: ensure operand is a ListNode + ListNode operandList = (requireOp.operand instanceof ListNode) + ? (ListNode) requireOp.operand + : ListNode.makeList(requireOp.operand); return new BinaryOperatorNode("(", codeRef, - (ListNode) requireOp.operand, + operandList, startIndex); } } diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java index d928501bb..5b2ee702b 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Operator.java +++ b/src/main/java/org/perlonjava/runtime/operators/Operator.java @@ -272,7 +272,8 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas int size = args.length; int offset = ((RuntimeScalar) args[1]).getInt(); // If length is not provided, use the rest of the string - int length = (size > 2) ? ((RuntimeScalar) args[2]).getInt() : strLength - offset; + boolean hasExplicitLength = size > 2; + int length = hasExplicitLength ? ((RuntimeScalar) args[2]).getInt() : strLength - offset; String replacement = (size > 3) ? args[3].toString() : null; // Store original offset and length for LValue creation @@ -282,9 +283,31 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas // Handle negative offsets (count from the end of the string) if (offset < 0) { offset = strLength + offset; + // When no explicit length is provided, Perl clips negative offsets to 0 (no warning) + // When explicit length IS provided, Perl warns and returns undef for too-negative offsets + if (offset < 0) { + if (hasExplicitLength) { + // Warn and return undef (same as positive offset out of bounds) + if (warnEnabled) { + WarnDie.warn(new RuntimeScalar("substr outside of string"), + RuntimeScalarCache.scalarEmptyString); + } + if (replacement != null) { + return new RuntimeScalar(); + } + var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", originalOffset, originalLength); + lvalue.type = RuntimeScalarType.UNDEF; + lvalue.value = null; + return lvalue; + } else { + // Clip to 0 without warning + offset = 0; + } + } } - if (offset < 0 || offset > strLength) { + // Only warn/error for positive offsets that exceed string length + if (offset > strLength) { if (warnEnabled) { WarnDie.warn(new RuntimeScalar("substr outside of string"), RuntimeScalarCache.scalarEmptyString); diff --git a/src/main/java/org/perlonjava/runtime/operators/OperatorHandler.java b/src/main/java/org/perlonjava/runtime/operators/OperatorHandler.java index 7b73f9b3c..75b0de55c 100644 --- a/src/main/java/org/perlonjava/runtime/operators/OperatorHandler.java +++ b/src/main/java/org/perlonjava/runtime/operators/OperatorHandler.java @@ -224,7 +224,7 @@ public record OperatorHandler(String className, String methodName, int methodTyp put("bless", "bless", "org/perlonjava/runtime/operators/ReferenceOperators", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); put("ref", "ref", "org/perlonjava/runtime/operators/ReferenceOperators", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); - put("caller", "caller", "org/perlonjava/runtime/runtimetypes/RuntimeCode", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;"); + put("caller", "callerWithSub", "org/perlonjava/runtime/runtimetypes/RuntimeCode", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;ILorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;"); put("reset", "reset", "org/perlonjava/runtime/operators/Operator", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;"); put("warn", "warn", "org/perlonjava/runtime/operators/WarnDie", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Ljava/lang/String;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;"); put("die", "die", "org/perlonjava/runtime/operators/WarnDie", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Ljava/lang/String;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;"); diff --git a/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java b/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java index 0181c5e62..78bde1471 100644 --- a/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java @@ -187,13 +187,17 @@ private static CommandResult executeCommand(String command, boolean captureOutpu if (captureOutput) { // For backticks: capture stdout only, stderr already goes to terminal + // Read raw bytes to preserve exact output (including or excluding trailing newlines) Thread stdoutThread = new Thread(() -> { - try (BufferedReader reader = new BufferedReader(new InputStreamReader(finalProcess.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - synchronized (finalOutput) { - finalOutput.append(line).append("\n"); - } + try (java.io.InputStream is = finalProcess.getInputStream()) { + byte[] buffer = new byte[8192]; + int bytesRead; + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + while ((bytesRead = is.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); + } + synchronized (finalOutput) { + finalOutput.append(baos.toString()); } } catch (IOException e) { // Stream closed - this is normal when process terminates @@ -438,15 +442,16 @@ private static RuntimeScalar completeForkOpen(List flattenedArgs, boolea Process process = processBuilder.start(); - // Read all output - StringBuilder output = new StringBuilder(); - try (BufferedReader reader = new BufferedReader( - new InputStreamReader(process.getInputStream()))) { - String line; - while ((line = reader.readLine()) != null) { - output.append(line).append("\n"); + // Read all output as raw bytes to preserve exact output + java.io.ByteArrayOutputStream baos = new java.io.ByteArrayOutputStream(); + try (java.io.InputStream is = process.getInputStream()) { + byte[] buffer = new byte[8192]; + int bytesRead; + while ((bytesRead = is.read(buffer)) != -1) { + baos.write(buffer, 0, bytesRead); } } + String capturedOutput = baos.toString(); // Wait for process to complete int exitCode = process.waitFor(); @@ -454,9 +459,6 @@ private static RuntimeScalar completeForkOpen(List flattenedArgs, boolea // Set $? to the exit status setGlobalVariable("main::?", String.valueOf(exitCode << 8)); - // Remove trailing newline if present (to match Perl behavior for single-line output) - String capturedOutput = output.toString(); - // Throw exception to return control to caller with captured output throw new ForkOpenCompleteException( process.pid(), diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java index b38f3fcc1..c408a3063 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java @@ -111,6 +111,11 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { GlobalVariable.getGlobalVariable(encodeSpecialVar("R")); // initialize $^R to "undef" - writable variable GlobalVariable.getGlobalVariable(encodeSpecialVar("A")).set(""); // initialize $^A to "" - format accumulator variable GlobalVariable.getGlobalVariable(encodeSpecialVar("P")).set(0); // initialize $^P to 0 - debugger flags + // Initialize $^I (in-place editing extension) from -i switch + if (compilerOptions.inPlaceEdit) { + GlobalVariable.getGlobalVariable(encodeSpecialVar("I")).set( + compilerOptions.inPlaceExtension != null ? compilerOptions.inPlaceExtension : ""); + } GlobalVariable.globalVariables.put(encodeSpecialVar("LAST_SUCCESSFUL_PATTERN"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_SUCCESSFUL_PATTERN)); GlobalVariable.globalVariables.put(encodeSpecialVar("LAST_FH"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_FH)); // $^LAST_FH GlobalVariable.getGlobalVariable(encodeSpecialVar("UNICODE")).set(0); // initialize $^UNICODE to 0 - `-C` unicode flags diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 9b089e819..b42ae4fa8 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -1600,7 +1600,23 @@ public static RuntimeList call(RuntimeScalar runtimeScalar, } } + /** + * Implementation of Perl's caller() builtin. + * This version doesn't have access to __SUB__, so it can't honor set_subname for JVM code. + */ public static RuntimeList caller(RuntimeList args, int ctx) { + return callerWithSub(args, ctx, null); + } + + /** + * Implementation of Perl's caller() builtin with __SUB__ support. + * When currentSub is provided, its subName is used for caller(0) to honor set_subname. + * + * @param args The arguments (frame number) + * @param ctx The calling context + * @param currentSub The __SUB__ reference from the calling subroutine (may be null) + */ + public static RuntimeList callerWithSub(RuntimeList args, int ctx, RuntimeScalar currentSub) { RuntimeList res = new RuntimeList(); int frame = 0; if (!args.isEmpty()) { @@ -1636,7 +1652,19 @@ public static RuntimeList caller(RuntimeList args, int ctx) { // The subroutine name at frame N is actually stored at frame N-1 // because it represents the sub that IS CALLING frame N String subName = null; - if (frame > 0 && frame - 1 < stackTraceSize) { + + // For the innermost frame (frame == 1 after skip), check currentSub first + // to honor set_subname() which modifies RuntimeCode.subName at runtime + if (frame == 1 && currentSub != null && currentSub.type == RuntimeScalarType.CODE) { + RuntimeCode code = (RuntimeCode) currentSub.value; + if (code.subName != null && !code.subName.isEmpty()) { + String pkg = code.packageName != null ? code.packageName : "main"; + subName = pkg + "::" + code.subName; + } + } + + // Fall back to stack trace info + if (subName == null && frame > 0 && frame - 1 < stackTraceSize) { ArrayList prevFrame = stackTrace.get(frame - 1); if (prevFrame.size() > 3) { subName = prevFrame.get(3); diff --git a/src/main/perl/lib/Try/Tiny.pm b/src/main/perl/lib/Try/Tiny.pm new file mode 100644 index 000000000..4dbc60d93 --- /dev/null +++ b/src/main/perl/lib/Try/Tiny.pm @@ -0,0 +1,202 @@ +package Try::Tiny; +use strict; +use warnings; + +# Bundled Try::Tiny implementation for PerlOnJava +# Based on Try::Tiny 0.32, simplified for compatibility + +our $VERSION = '0.32'; + +use Exporter 'import'; +our @EXPORT = our @EXPORT_OK = qw(try catch finally); + +use Carp; + +# Try to load Sub::Util or Sub::Name for naming blocks +BEGIN { + my $su = eval { require Sub::Util; defined &Sub::Util::set_subname }; + my $sn = !$su && eval { require Sub::Name; Sub::Name->VERSION(0.08) }; + + *_subname = $su ? \&Sub::Util::set_subname + : $sn ? \&Sub::Name::subname + : sub { $_[1] }; + *_HAS_SUBNAME = ($su || $sn) ? sub(){1} : sub(){0}; +} + +# Blessed wrapper for catch blocks +sub catch (&;@) { + my ($block, @rest) = @_; + # Detect bare catch() in void context + croak 'Useless bare catch()' unless wantarray; + # Name the block if we can + _subname(caller() . '::catch {...} ' => $block) if _HAS_SUBNAME; + return (bless(\$block, 'Try::Tiny::Catch'), @rest); +} + +# Blessed wrapper for finally blocks +sub finally (&;@) { + my ($block, @rest) = @_; + # Detect bare finally() in void context + croak 'Useless bare finally()' unless wantarray; + # Name the block if we can + _subname(caller() . '::finally {...} ' => $block) if _HAS_SUBNAME; + return (bless(\$block, 'Try::Tiny::Finally'), @rest); +} + +sub try (&;@) { + my ($try, @code_refs) = @_; + + # Name the try block if we can + _subname(caller() . '::try {...} ' => $try) if _HAS_SUBNAME; + + # Save calling context + my $wantarray = wantarray; + + # Parse catch and finally blocks + my ($catch, @finally); + for my $code_ref (@code_refs) { + if (ref($code_ref) eq 'Try::Tiny::Catch') { + if ($catch) { + require Carp; + Carp::croak('A try() may not be followed by multiple catch() blocks'); + } + $catch = ${$code_ref}; + } + elsif (ref($code_ref) eq 'Try::Tiny::Finally') { + push @finally, ${$code_ref}; + } + else { + require Carp; + Carp::croak( + 'try() encountered an unexpected argument (' + . (defined $code_ref ? $code_ref : 'undef') + . ') - perhaps a missing semi-colon before or' + ); + } + } + + # Name the try block if we can + _subname(caller() . '::try {...} ' => $try) if _HAS_SUBNAME; + + # Save $@ to restore later + my $prev_error = $@; + + # Execute try block + my ($failed, $error, @ret); + { + local $@; + $failed = not eval { + $@ = $prev_error; # Restore $@ inside eval for code that checks it + if ($wantarray) { + @ret = $try->(); + } + elsif (defined $wantarray) { + $ret[0] = $try->(); + } + else { + $try->(); + } + 1; + }; + $error = $@; + } + + # Restore $@ + $@ = $prev_error; + + # Execute catch block if we failed + my $catch_error; + my $catch_failed; + if ($failed && $catch) { + # Set up $_ and @_ for catch block + local $_ = $error; + my @catch_args = ($error); + + # Preserve $@ in catch block too, wrap in eval to catch exceptions + { + local $@; + $catch_failed = not eval { + $@ = $prev_error; + if ($wantarray) { + @ret = $catch->(@catch_args); + } + elsif (defined $wantarray) { + $ret[0] = $catch->(@catch_args); + } + else { + $catch->(@catch_args); + } + 1; + }; + $catch_error = $@ if $catch_failed; + } + $@ = $prev_error; + } + + # Execute finally blocks (always, in void context) + my @finally_errors; + for my $finally_block (@finally) { + local $@; + my @finally_args = $failed ? ($error) : (); + eval { + $finally_block->(@finally_args); + 1; + } or do { + # Collect errors from finally blocks, warn about them + push @finally_errors, $@; + }; + } + + # Warn about any finally block errors (match original Try::Tiny format) + for my $finally_error (@finally_errors) { + warn + "Execution of finally() block CODE(" + . sprintf("0x%x", 0 + \$finally_error) + . ") resulted in an exception, which " + . '*CAN NOT BE PROPAGATED* due to fundamental limitations of Perl. ' + . 'Your program will continue as if this event never took place. ' + . "Original exception text follows:\n\n" + . (defined $finally_error ? $finally_error : '$@ left undefined...') + . "\n"; + } + + # Restore $@ one more time + $@ = $prev_error; + + # Re-throw if catch block died (after finally blocks have run) + if ($catch_failed) { + die $catch_error; + } + + # Return based on context + return $wantarray ? @ret : $ret[0]; +} + +1; + +__END__ + +=head1 NAME + +Try::Tiny - Minimal try/catch with proper preservation of $@ + +=head1 SYNOPSIS + + use Try::Tiny; + + try { + die "foo"; + } + catch { + warn "caught error: $_"; + } + finally { + print "cleanup\n"; + }; + +=head1 DESCRIPTION + +This is a bundled implementation of Try::Tiny for PerlOnJava, +providing compatibility with the CPAN module. + +=cut