diff --git a/dev/import-perl5/config.yaml b/dev/import-perl5/config.yaml index 61eca74ad..e8df5dd7e 100644 --- a/dev/import-perl5/config.yaml +++ b/dev/import-perl5/config.yaml @@ -400,15 +400,6 @@ imports: target: perl5_t/Term-Table type: directory - # From CPAN distribution - - source: perl5/cpan/Time-Local/lib/Time/Local.pm - target: src/main/perl/lib/Time/Local.pm - - # Tests for distribution - - source: perl5/cpan/Time-Local/t - target: perl5_t/Time-Local - type: directory - # Add more imports below as needed # Example with minimal fields: # - source: perl5/lib/SomeModule.pm diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index ba8e7eac5..b8c8fe0c6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -1,6 +1,5 @@ package org.perlonjava.backend.bytecode; -import org.perlonjava.frontend.analysis.FindDeclarationVisitor; import org.perlonjava.frontend.analysis.Visitor; import org.perlonjava.backend.jvm.EmitterMethodCreator; import org.perlonjava.backend.jvm.EmitterContext; @@ -494,7 +493,18 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { // Use the calling context from EmitterContext for top-level expressions // This is crucial for eval STRING to propagate context correctly currentCallContext = ctx.contextType; - + // Inherit package from the JVM compiler context so that eval STRING + // compiles unqualified names in the caller's package, not "main". + // This is compile-time package propagation — it sets the symbol table's + // current package so the parser/emitter qualify names correctly. + // Note: the *runtime* package (InterpreterState.currentPackage) is a + // separate concern used only by caller(); it must be scoped via + // DynamicVariableManager around eval execution to prevent leaking. + // See InterpreterState.currentPackage javadoc for details. + if (ctx.symbolTable != null) { + symbolTable.setCurrentPackage(ctx.symbolTable.getCurrentPackage(), + ctx.symbolTable.currentPackageIsClass()); + } } // If we have captured variables, allocate registers for them @@ -547,7 +557,7 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { } // Build InterpretedCode - InterpretedCode result = new InterpretedCode( + return new InterpretedCode( toShortArray(), constants.toArray(), stringPool.toArray(new String[0]), @@ -563,9 +573,6 @@ public InterpretedCode compile(Node node, EmitterContext ctx) { warningFlags, // Warning flags for eval STRING inheritance symbolTable.getCurrentPackage() // Compile-time package for eval STRING name resolution ); - result.containsRegex = FindDeclarationVisitor.findOperator(node, "matchRegex") != null - || FindDeclarationVisitor.findOperator(node, "replaceRegex") != null; - return result; } // ========================================================================= @@ -4107,12 +4114,6 @@ public void visit(For1Node node) { variableScopes.peek().put(varName, varReg); allDeclaredVariables.put(varName, varReg); } - } else if (varOp.operator.equals("$") && varOp.operand instanceof IdentifierNode - && globalLoopVarName == null) { - String varName = "$" + ((IdentifierNode) varOp.operand).name; - if (hasVariable(varName)) { - variableScopes.peek().put(varName, varReg); - } } } @@ -4208,6 +4209,17 @@ public void visit(For3Node node) { if (currentCallContext != RuntimeContextType.VOID) { outerResultReg = allocateRegister(); } + + int labelIdx = -1; + int exitPcPlaceholder = -1; + if (node.labelName != null) { + labelIdx = addToStringPool(node.labelName); + emit(Opcodes.PUSH_LABELED_BLOCK); + emit(labelIdx); + exitPcPlaceholder = bytecode.size(); + emitInt(0); + } + enterScope(); try { // Just execute the body once, no loop @@ -4224,6 +4236,13 @@ public void visit(For3Node node) { // Exit scope to clean up lexical variables exitScope(); } + + if (node.labelName != null) { + emit(Opcodes.POP_LABELED_BLOCK); + int exitPc = bytecode.size(); + patchJump(exitPcPlaceholder, exitPc); + } + lastResultReg = outerResultReg; return; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index ab4e39927..f327efb6d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -48,12 +48,6 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c String frameSubName = subroutineName != null ? subroutineName : (code.subName != null ? code.subName : "(eval)"); InterpreterState.push(code, framePackageName, frameSubName); - int regexLocalLevel = -1; - if (code.containsRegex) { - regexLocalLevel = DynamicVariableManager.getLocalLevel(); - RuntimeRegexState.pushLocal(); - } - // Pure register file (NOT stack-based - matches compiler for control flow correctness) RuntimeBase[] registers = new RuntimeBase[code.maxRegisters]; @@ -75,6 +69,15 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // When exception occurs, pop from stack and jump to catch PC java.util.Stack evalCatchStack = new java.util.Stack<>(); + // Labeled block stack for non-local last/next/redo handling. + // When a function call returns a RuntimeControlFlowList, we check this stack + // to see if the label matches an enclosing labeled block. + java.util.Stack labeledBlockStack = new java.util.Stack<>(); + // Each entry is [labelStringPoolIdx, exitPc] + + try { + outer: + while (true) { try { // Main dispatch loop - JVM JIT optimizes switch to tableswitch (O(1) jump) while (pc < bytecode.length) { @@ -94,16 +97,14 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; case Opcodes.RETURN: { + // Return from subroutine: return rd int retReg = bytecode[pc++]; RuntimeBase retVal = registers[retReg]; + if (retVal == null) { return new RuntimeList(); } - RuntimeList retList = retVal.getList(); - if (code.containsRegex) { - RuntimeList.resolveMatchProxies(retList); - } - return retList; + return retVal.getList(); } case Opcodes.GOTO: { @@ -989,8 +990,25 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Check for control flow (last/next/redo/goto/tail-call) if (result.isNonLocalGoto()) { - // Propagate control flow up the call stack - return result; + RuntimeControlFlowList flow = (RuntimeControlFlowList) result; + // Check labeled block stack for a matching label + boolean handled = false; + for (int i = labeledBlockStack.size() - 1; i >= 0; i--) { + int[] entry = labeledBlockStack.get(i); + String blockLabel = code.stringPool[entry[0]]; + if (flow.matchesLabel(blockLabel)) { + // Pop entries down to and including the match + while (labeledBlockStack.size() > i) { + labeledBlockStack.pop(); + } + pc = entry[1]; // jump to block exit + handled = true; + break; + } + } + if (!handled) { + return result; + } } break; } @@ -1034,8 +1052,23 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Check for control flow (last/next/redo/goto/tail-call) if (result.isNonLocalGoto()) { - // Propagate control flow up the call stack - return result; + RuntimeControlFlowList flow = (RuntimeControlFlowList) result; + boolean handled = false; + for (int i = labeledBlockStack.size() - 1; i >= 0; i--) { + int[] entry = labeledBlockStack.get(i); + String blockLabel = code.stringPool[entry[0]]; + if (flow.matchesLabel(blockLabel)) { + while (labeledBlockStack.size() > i) { + labeledBlockStack.pop(); + } + pc = entry[1]; + handled = true; + break; + } + } + if (!handled) { + return result; + } } break; } @@ -1252,14 +1285,6 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c pc = OpcodeHandlerExtended.executeLstat(bytecode, pc, registers); break; - case Opcodes.STAT_LASTHANDLE: - pc = OpcodeHandlerExtended.executeStatLastHandle(bytecode, pc, registers); - break; - - case Opcodes.LSTAT_LASTHANDLE: - pc = OpcodeHandlerExtended.executeLstatLastHandle(bytecode, pc, registers); - break; - // File test operations (opcodes 190-216) - delegated to handler case Opcodes.FILETEST_R: case Opcodes.FILETEST_W: @@ -1526,6 +1551,25 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + // ================================================================= + // LABELED BLOCK SUPPORT + // ================================================================= + + case Opcodes.PUSH_LABELED_BLOCK: { + int labelIdx = bytecode[pc++]; + int exitPc = readInt(bytecode, pc); + pc += 1; + labeledBlockStack.push(new int[]{labelIdx, exitPc}); + break; + } + + case Opcodes.POP_LABELED_BLOCK: { + if (!labeledBlockStack.isEmpty()) { + labeledBlockStack.pop(); + } + break; + } + // ================================================================= // LIST OPERATIONS // ================================================================= @@ -2196,9 +2240,10 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Special handling for ClassCastException to show which opcode is failing // Check if we're inside an eval block first if (!evalCatchStack.isEmpty()) { - evalCatchStack.pop(); + int catchPc = evalCatchStack.pop(); WarnDie.catchEval(e); - return new RuntimeList(); + pc = catchPc; + continue outer; } // Not in eval - show detailed error with bytecode context @@ -2224,14 +2269,13 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Check if we're inside an eval block if (!evalCatchStack.isEmpty()) { // Inside eval block - catch the exception - evalCatchStack.pop(); // Pop the catch handler + int catchPc = evalCatchStack.pop(); // Pop the catch handler // Call WarnDie.catchEval() to set $@ WarnDie.catchEval(e); - // Eval block failed - return empty list - // (The result will be undef in scalar context, empty in list context) - return new RuntimeList(); + pc = catchPc; + continue outer; } // Not in eval block - propagate exception @@ -2252,10 +2296,10 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c // Wrap other exceptions with interpreter context including bytecode context String errorMessage = formatInterpreterError(code, pc, e); throw new RuntimeException(errorMessage, e); + } + } // end outer while } finally { - if (regexLocalLevel >= 0) { - DynamicVariableManager.popToLocalLevel(regexLocalLevel); - } + // Always pop the interpreter state InterpreterState.pop(); } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java index 576357ec7..98010eb1e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperator.java @@ -1,44 +1,9 @@ package org.perlonjava.backend.bytecode; -import org.perlonjava.frontend.analysis.FindDeclarationVisitor; import org.perlonjava.frontend.astnode.*; import org.perlonjava.runtime.runtimetypes.RuntimeContextType; public class CompileBinaryOperator { - - private static class DeclRewrite { - final OperatorNode declaration; - final String savedOperator; - final Node savedOperand; - - DeclRewrite(OperatorNode declaration, String savedOperator, Node savedOperand) { - this.declaration = declaration; - this.savedOperator = savedOperator; - this.savedOperand = savedOperand; - } - - void restore() { - declaration.operator = savedOperator; - declaration.operand = savedOperand; - } - } - - private static DeclRewrite extractMyDeclaration(BytecodeCompiler bc, Node rightNode) { - OperatorNode decl = FindDeclarationVisitor.findOperator(rightNode, "my"); - if (decl != null && decl.operand instanceof OperatorNode innerOp) { - String savedOp = decl.operator; - Node savedOperand = decl.operand; - int savedCtx = bc.currentCallContext; - bc.currentCallContext = RuntimeContextType.VOID; - decl.accept(bc); - bc.currentCallContext = savedCtx; - decl.operator = innerOp.operator; - decl.operand = innerOp.operand; - return new DeclRewrite(decl, savedOp, savedOperand); - } - return null; - } - static void visitBinaryOperator(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) { // Track token index for error reporting bytecodeCompiler.currentTokenIndex = node.getIndex(); @@ -465,115 +430,134 @@ else if (node.right instanceof BinaryOperatorNode) { // Handle short-circuit operators specially - don't compile right operand yet! if (node.operator.equals("&&") || node.operator.equals("and")) { - DeclRewrite rewrite = extractMyDeclaration(bytecodeCompiler, node.right); - try { - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(bytecodeCompiler); - int rs1 = bytecodeCompiler.lastResultReg; - bytecodeCompiler.currentCallContext = savedContext; + // Logical AND with short-circuit evaluation + // Only evaluate right side if left side is true - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs1); + // Compile left operand in scalar context (need boolean value) + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; - int skipRightPos = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.emit(Opcodes.GOTO_IF_FALSE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitInt(0); + // Allocate result register and move left value to it + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); - node.right.accept(bytecodeCompiler); - int rs2 = bytecodeCompiler.lastResultReg; + // Mark position for forward jump + int skipRightPos = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs2); + // Emit conditional jump: if (!rd) skip right evaluation + bytecodeCompiler.emit(Opcodes.GOTO_IF_FALSE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitInt(0); // Placeholder for offset (will be patched) + + // NOW compile right operand (only executed if left was true) + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; - int skipRightTarget = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + // Move right result to rd (overwriting left value) + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); - bytecodeCompiler.lastResultReg = rd; - } finally { - if (rewrite != null) rewrite.restore(); - } + // Patch the forward jump offset + int skipRightTarget = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + + bytecodeCompiler.lastResultReg = rd; return; } if (node.operator.equals("||") || node.operator.equals("or")) { - DeclRewrite rewrite = extractMyDeclaration(bytecodeCompiler, node.right); - try { - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(bytecodeCompiler); - int rs1 = bytecodeCompiler.lastResultReg; - bytecodeCompiler.currentCallContext = savedContext; + // Logical OR with short-circuit evaluation + // Only evaluate right side if left side is false - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs1); + // Compile left operand in scalar context (need boolean value) + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; - int skipRightPos = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitInt(0); + // Allocate result register and move left value to it + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); - node.right.accept(bytecodeCompiler); - int rs2 = bytecodeCompiler.lastResultReg; + // Mark position for forward jump + int skipRightPos = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs2); + // Emit conditional jump: if (rd) skip right evaluation + bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitInt(0); // Placeholder for offset (will be patched) - int skipRightTarget = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + // NOW compile right operand (only executed if left was false) + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; - bytecodeCompiler.lastResultReg = rd; - } finally { - if (rewrite != null) rewrite.restore(); - } + // Move right result to rd (overwriting left value) + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); + + // Patch the forward jump offset + int skipRightTarget = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + + bytecodeCompiler.lastResultReg = rd; return; } if (node.operator.equals("//")) { - DeclRewrite rewrite = extractMyDeclaration(bytecodeCompiler, node.right); - try { - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(bytecodeCompiler); - int rs1 = bytecodeCompiler.lastResultReg; - bytecodeCompiler.currentCallContext = savedContext; + // Defined-OR with short-circuit evaluation + // Only evaluate right side if left side is undefined - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs1); + // Compile left operand in scalar context (need to test definedness) + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; - int definedReg = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(Opcodes.DEFINED); - bytecodeCompiler.emitReg(definedReg); - bytecodeCompiler.emitReg(rd); + // Allocate result register and move left value to it + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + + // Check if left is defined + int definedReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.DEFINED); + bytecodeCompiler.emitReg(definedReg); + bytecodeCompiler.emitReg(rd); - int skipRightPos = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); - bytecodeCompiler.emitReg(definedReg); - bytecodeCompiler.emitInt(0); + // Mark position for forward jump + int skipRightPos = bytecodeCompiler.bytecode.size(); - node.right.accept(bytecodeCompiler); - int rs2 = bytecodeCompiler.lastResultReg; + // Emit conditional jump: if (defined) skip right evaluation + bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); + bytecodeCompiler.emitReg(definedReg); + bytecodeCompiler.emitInt(0); // Placeholder for offset (will be patched) - bytecodeCompiler.emit(Opcodes.MOVE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(rs2); + // NOW compile right operand (only executed if left was undefined) + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; - int skipRightTarget = bytecodeCompiler.bytecode.size(); - bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + // Move right result to rd (overwriting left value) + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); - bytecodeCompiler.lastResultReg = rd; - } finally { - if (rewrite != null) rewrite.restore(); - } + // Patch the forward jump offset + int skipRightTarget = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + + bytecodeCompiler.lastResultReg = rd; return; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index bc387b96a..4374be4fe 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -554,37 +554,21 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode } } else if (op.equals("stat") || op.equals("lstat")) { // stat FILE or lstat FILE - boolean isUnderscoreOperand = (node.operand instanceof IdentifierNode) - && ((IdentifierNode) node.operand).name.equals("_"); - - if (isUnderscoreOperand) { - int savedContext = bytecodeCompiler.currentCallContext; - try { - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(op.equals("stat") ? Opcodes.STAT_LASTHANDLE : Opcodes.LSTAT_LASTHANDLE); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emit(savedContext); - bytecodeCompiler.lastResultReg = rd; - } finally { - bytecodeCompiler.currentCallContext = savedContext; - } - } else { - int savedContext = bytecodeCompiler.currentCallContext; - bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; - try { - node.operand.accept(bytecodeCompiler); - int operandReg = bytecodeCompiler.lastResultReg; + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + try { + node.operand.accept(bytecodeCompiler); + int operandReg = bytecodeCompiler.lastResultReg; - int rd = bytecodeCompiler.allocateRegister(); - bytecodeCompiler.emit(op.equals("stat") ? Opcodes.STAT : Opcodes.LSTAT); - bytecodeCompiler.emitReg(rd); - bytecodeCompiler.emitReg(operandReg); - bytecodeCompiler.emit(savedContext); + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(op.equals("stat") ? Opcodes.STAT : Opcodes.LSTAT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(operandReg); + bytecodeCompiler.emit(savedContext); // Pass calling context - bytecodeCompiler.lastResultReg = rd; - } finally { - bytecodeCompiler.currentCallContext = savedContext; - } + bytecodeCompiler.lastResultReg = rd; + } finally { + bytecodeCompiler.currentCallContext = savedContext; } } else if (op.startsWith("-") && op.length() == 2) { // File test operators: -r, -w, -x, etc. diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 6259c44b0..1c5c619f8 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -217,9 +217,25 @@ public static RuntimeList evalStringList(String perlCode, evalCode = evalCode.withCapturedVars(currentCode.capturedVars); } - // Step 6: Execute the compiled code + // Step 6: Execute the compiled code. + // IMPORTANT: Scope InterpreterState.currentPackage around eval execution. + // currentPackage is a runtime-only field used by caller() — it does NOT + // affect name resolution (which is fully compile-time). However, if the + // eval contains SET_PACKAGE opcodes (e.g. "package Foo;"), those would + // permanently mutate the caller's currentPackage without this scoping. + // We use DynamicVariableManager (same mechanism as PUSH_PACKAGE/POP_LOCAL_LEVEL) + // to save and restore it automatically. + int pkgLevel = DynamicVariableManager.getLocalLevel(); + String savedPkg = InterpreterState.currentPackage.get().toString(); + DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); + InterpreterState.currentPackage.get().set(savedPkg); RuntimeArray args = new RuntimeArray(); // Empty @_ - RuntimeList result = evalCode.apply(args, callContext); + RuntimeList result; + try { + result = evalCode.apply(args, callContext); + } finally { + DynamicVariableManager.popToLocalLevel(pkgLevel); + } evalTrace("EvalStringHandler exec ok ctx=" + callContext + " resultScalar=" + (result != null ? result.scalar().toString() : "null") + " resultBool=" + (result != null && result.scalar() != null ? result.scalar().getBoolean() : false) + @@ -295,9 +311,18 @@ public static RuntimeScalar evalString(String perlCode, // Attach captured variables evalCode = evalCode.withCapturedVars(capturedVars); - // Execute + // Scope currentPackage around eval — see Step 6 comment in evalStringHelper above. + int pkgLevel = DynamicVariableManager.getLocalLevel(); + String savedPkg = InterpreterState.currentPackage.get().toString(); + DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); + InterpreterState.currentPackage.get().set(savedPkg); RuntimeArray args = new RuntimeArray(); - RuntimeList result = evalCode.apply(args, RuntimeContextType.SCALAR); + RuntimeList result; + try { + result = evalCode.apply(args, RuntimeContextType.SCALAR); + } finally { + DynamicVariableManager.popToLocalLevel(pkgLevel); + } return result.scalar(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index b644bc460..b39ab962f 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -35,8 +35,6 @@ public class InterpretedCode extends RuntimeCode { public final BitSet warningFlags; // Warning flags at compile time public final String compilePackage; // Package at compile time (for eval STRING name resolution) - public boolean containsRegex; // Whether this code contains regex ops (for match var scoping) - // Debug information (optional) public final String sourceName; // Source file name (for stack traces) public final int sourceLine; // Source line number @@ -1536,6 +1534,15 @@ public String disassemble() { case Opcodes.DO_FILE: sb.append("DO_FILE r").append(bytecode[pc++]).append(" = doFile(r").append(bytecode[pc++]).append(") ctx=").append(bytecode[pc++]).append("\n"); break; + case Opcodes.PUSH_LABELED_BLOCK: { + int labelIdx = bytecode[pc++]; + int exitPc = bytecode[pc++]; + sb.append("PUSH_LABELED_BLOCK \"").append(stringPool[labelIdx]).append("\" exitPc=").append(exitPc).append("\n"); + break; + } + case Opcodes.POP_LABELED_BLOCK: + sb.append("POP_LABELED_BLOCK\n"); + break; default: sb.append("UNKNOWN(").append(opcode).append(")\n"); break; diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java b/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java index 60e102066..2e1494914 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpreterState.java @@ -24,15 +24,29 @@ public class InterpreterState { /** * Thread-local RuntimeScalar holding the runtime current package name. * - * This is the single source of truth for the current package at runtime. - * It is used by: - * - caller() to return the correct calling package - * - eval STRING to compile code in the right package - * - SET_PACKAGE opcode (non-scoped: package Foo;) — sets it directly - * - PUSH_PACKAGE opcode (scoped: package Foo { }) — saves via DynamicVariableManager then sets + *

Design principle: Package is a compile-time concept for name + * resolution. All variable and subroutine names are fully qualified at compile time + * by the ScopedSymbolTable / BytecodeCompiler. This field exists only for + * runtime introspection — it does NOT affect name resolution.

* - * Scoped package blocks are automatically restored when the scope exits via - * the existing POP_LOCAL_LEVEL opcode (DynamicVariableManager.popToLocalLevel). + *

Used by:

+ * + * + *

Eval scoping: Both eval STRING paths (EvalStringHandler for JVM bytecode, + * RuntimeCode for interpreter) must push/pop this field via DynamicVariableManager + * around eval execution. Without this, SET_PACKAGE opcodes inside the eval leak + * into the caller's package state, breaking caller() and subsequent eval compilations. + * This was the root cause of the signatures.t regression (601→446).

+ * + *

Scoped package blocks ({@code package Foo { }}) are automatically restored + * when the scope exits via POP_LOCAL_LEVEL (DynamicVariableManager.popToLocalLevel).

*/ public static final ThreadLocal currentPackage = ThreadLocal.withInitial(() -> new RuntimeScalar("main")); @@ -114,4 +128,5 @@ public static List getStack() { public static List getPcStack() { return new ArrayList<>(pcStack.get()); } + } diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index 5b543dc6f..4849678ec 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -513,20 +513,6 @@ public static int executeLstat(int[] bytecode, int pc, RuntimeBase[] registers) return pc; } - public static int executeStatLastHandle(int[] bytecode, int pc, RuntimeBase[] registers) { - int rd = bytecode[pc++]; - int ctx = bytecode[pc++]; - registers[rd] = Stat.statLastHandle(ctx); - return pc; - } - - public static int executeLstatLastHandle(int[] bytecode, int pc, RuntimeBase[] registers) { - int rd = bytecode[pc++]; - int ctx = bytecode[pc++]; - registers[rd] = Stat.lstatLastHandle(ctx); - return pc; - } - /** * Execute print operation. * Format: PRINT contentReg filehandleReg diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 315919082..83b1f0062 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1157,5 +1157,21 @@ public class Opcodes { * Format: LSTAT_LASTHANDLE rd ctx */ public static final short LSTAT_LASTHANDLE = 351; + /** Mutable scalar assignment: rd = new RuntimeScalar(); rd.set(rs) + * Superinstruction combining LOAD_UNDEF + SET_SCALAR for lexical scalar assignment. + * Format: MY_SCALAR rd rs */ + public static final short MY_SCALAR = 352; + + /** Undefine a scalar variable in-place: rd.undefine(). Used by `undef $x`. */ + public static final short UNDEFINE_SCALAR = 353; + + /** Push a labeled block entry for non-local last/next/redo handling. + * Format: PUSH_LABELED_BLOCK label_string_idx exit_pc(int) */ + public static final short PUSH_LABELED_BLOCK = 354; + + /** Pop a labeled block entry. + * Format: POP_LABELED_BLOCK */ + public static final short POP_LABELED_BLOCK = 355; + private Opcodes() {} // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java index e2a553861..8430e8082 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/SlowOpcodeHandler.java @@ -730,7 +730,10 @@ public static int executeSplit( int ctx = bytecode[pc++]; RuntimeScalar pattern = (RuntimeScalar) registers[patternReg]; - RuntimeList args = (RuntimeList) registers[argsReg]; + RuntimeBase argsBase = registers[argsReg]; + RuntimeList args = (argsBase instanceof RuntimeList) + ? (RuntimeList) argsBase + : new RuntimeList(argsBase.scalar()); RuntimeList result = Operator.split(pattern, args, ctx); diff --git a/src/main/java/org/perlonjava/backend/jvm/Dereference.java b/src/main/java/org/perlonjava/backend/jvm/Dereference.java index 6d4131340..830fffabc 100644 --- a/src/main/java/org/perlonjava/backend/jvm/Dereference.java +++ b/src/main/java/org/perlonjava/backend/jvm/Dereference.java @@ -794,11 +794,20 @@ public static void handleArrowArrayDeref(EmitterVisitor emitterVisitor, BinaryOp ArrayLiteralNode right = (ArrayLiteralNode) node.right; + // Check if this is a true array literal (contains only literal elements like strings and numbers) + // and has a single range operator in the indices + boolean isArrayLiteral = node.left instanceof ArrayLiteralNode leftArray && + leftArray.elements.stream().allMatch(elem -> + elem instanceof StringNode || + elem instanceof NumberNode) && + leftArray.elements.size() > 1; // Must have multiple literal elements + boolean isSingleRange = right.elements.size() == 1 && right.elements.getFirst() instanceof BinaryOperatorNode binOp && "..".equals(binOp.operator); - - if (right.elements.size() == 1 && !isSingleRange) { + + // Only apply the fix to true array literals with range operators + if (right.elements.size() == 1 && !(isArrayLiteral && isSingleRange)) { // Single index: use get/delete/exists methods Node elem = right.elements.getFirst(); elem.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index c7b436290..f8cde803e 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -864,20 +864,24 @@ static void handleStatOperator(EmitterVisitor emitterVisitor, OperatorNode node, if (node.operand instanceof IdentifierNode identNode && identNode.name.equals("_")) { - // stat _ or lstat _ - use cached stat buffer with context - emitterVisitor.pushCallContext(); + // stat _ or lstat _ - still use the old methods since they don't take args emitterVisitor.ctx.mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/runtime/operators/Stat", operator + "LastHandle", - "(I)Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;", + "()Lorg/perlonjava/runtime/runtimetypes/RuntimeList;", false); + // Handle context - treat as list that needs conversion if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { handleVoidContext(emitterVisitor); } else if (emitterVisitor.ctx.contextType == RuntimeContextType.SCALAR) { - emitterVisitor.ctx.mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/runtimetypes/RuntimeScalar"); - } else if (emitterVisitor.ctx.contextType == RuntimeContextType.LIST) { - emitterVisitor.ctx.mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/runtimetypes/RuntimeList"); + // Convert with stat's special semantics + emitterVisitor.ctx.mv.visitMethodInsn( + Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/runtimetypes/RuntimeList", + "statScalar", + "()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", + false); } } else { // stat EXPR or lstat EXPR - use context-aware methods diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index 5226e0844..6151612e7 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -741,15 +741,6 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeBase", "getList", "()Lorg/perlonjava/runtime/runtimetypes/RuntimeList;", false); mv.visitVarInsn(Opcodes.ASTORE, returnListSlot); - if (localRecord.containsRegex()) { - mv.visitVarInsn(Opcodes.ALOAD, returnListSlot); - mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/runtimetypes/RuntimeList", - "resolveMatchProxies", - "(Lorg/perlonjava/runtime/runtimetypes/RuntimeList;)V", - false); - } - // Phase 3: Check for control flow markers // RuntimeList is on stack after getList() @@ -1628,12 +1619,6 @@ private static InterpretedCode compileToInterpreter( ctx.errorUtil ); - // Inherit package from the JVM compiler context so unqualified sub calls - // resolve in the correct package (not main) in the interpreter fallback. - if (ctx.symbolTable != null) { - compiler.setCompilePackage(ctx.symbolTable.getCurrentPackage()); - } - // Compile AST to interpreter bytecode (pass ctx for package context and closure detection) InterpretedCode code = compiler.compile(ast, ctx); diff --git a/src/main/java/org/perlonjava/backend/jvm/Local.java b/src/main/java/org/perlonjava/backend/jvm/Local.java index 10065f9f9..e2fb232f5 100644 --- a/src/main/java/org/perlonjava/backend/jvm/Local.java +++ b/src/main/java/org/perlonjava/backend/jvm/Local.java @@ -22,13 +22,13 @@ public class Local { * and the index of the dynamic variable stack. */ static localRecord localSetup(EmitterContext ctx, Node ast, MethodVisitor mv) { + // Check if the code contains a 'local' operator boolean containsLocalOperator = FindDeclarationVisitor.findOperator(ast, "local") != null; - boolean containsRegex = FindDeclarationVisitor.findOperator(ast, "matchRegex") != null - || FindDeclarationVisitor.findOperator(ast, "replaceRegex") != null; - boolean needsDynamicSave = containsLocalOperator || containsRegex; int dynamicIndex = -1; - if (needsDynamicSave) { + if (containsLocalOperator) { + // Allocate a local variable to store the dynamic variable stack index dynamicIndex = ctx.symbolTable.allocateLocalVariable(); + // Get the current level of the dynamic variable stack and store it mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/DynamicVariableManager", "getLocalLevel", @@ -36,18 +36,21 @@ static localRecord localSetup(EmitterContext ctx, Node ast, MethodVisitor mv) { false); mv.visitVarInsn(Opcodes.ISTORE, dynamicIndex); } - if (containsRegex) { - mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/runtimetypes/RuntimeRegexState", - "pushLocal", - "()V", - false); - } - return new localRecord(needsDynamicSave, containsRegex, dynamicIndex); + return new localRecord(containsLocalOperator, dynamicIndex); } + /** + * Tears down the local variable setup by restoring the dynamic variable stack + * to its previous level if a 'local' operator was present. + * + * @param localRecord The record containing information about the 'local' operator + * and the dynamic variable stack index. + * @param mv The method visitor used to generate bytecode instructions. + */ static void localTeardown(localRecord localRecord, MethodVisitor mv) { - if (localRecord.needsDynamicSave()) { + // Add `local` teardown logic + if (localRecord.containsLocalOperator()) { + // Restore the dynamic variable stack to the recorded level mv.visitVarInsn(Opcodes.ILOAD, localRecord.dynamicIndex()); mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/DynamicVariableManager", @@ -57,6 +60,13 @@ static void localTeardown(localRecord localRecord, MethodVisitor mv) { } } - record localRecord(boolean needsDynamicSave, boolean containsRegex, int dynamicIndex) { + /** + * A record to store information about the presence of a 'local' operator + * and the index of the dynamic variable stack. + * + * @param containsLocalOperator Indicates if a 'local' operator is present. + * @param dynamicIndex The index of the dynamic variable stack. + */ + record localRecord(boolean containsLocalOperator, int dynamicIndex) { } } diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index a5920c3c2..6a8a059de 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -694,22 +694,13 @@ static OperatorNode parseStat(Parser parser, LexerToken token, int currentIndex) paren = true; } - if (nextToken.text.equals("_")) { - // Handle `stat _` - TokenUtils.consume(parser); - if (paren) { - TokenUtils.consume(parser, OPERATOR, ")"); - } - return new OperatorNode(token.text, - new IdentifierNode("_", parser.tokenIndex), parser.tokenIndex); - } - // stat/lstat: bareword filehandle (typically ALLCAPS) should be treated as a typeglob. // Consume it here, before generic expression parsing can turn it into a subroutine call. if (nextToken.type == IDENTIFIER) { String name = nextToken.text; if (name.matches("^[A-Z_][A-Z0-9_]*$")) { TokenUtils.consume(parser); + // autovivify filehandle and convert to globref GlobalVariable.getGlobalIO(FileHandle.normalizeBarewordHandle(parser, name)); Node fh = FileHandle.parseBarewordHandle(parser, name); Node operand = fh != null ? fh : new IdentifierNode(name, parser.tokenIndex); @@ -719,6 +710,15 @@ static OperatorNode parseStat(Parser parser, LexerToken token, int currentIndex) return new OperatorNode(token.text, operand, currentIndex); } } + if (nextToken.text.equals("_")) { + // Handle `stat _` + TokenUtils.consume(parser); + if (paren) { + TokenUtils.consume(parser, OPERATOR, ")"); + } + return new OperatorNode(token.text, + new IdentifierNode("_", parser.tokenIndex), parser.tokenIndex); + } // Parse optional single argument (or default to $_) // If we've already consumed '(', we must parse a full expression up to ')'. diff --git a/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java b/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java index b3c5b424e..bb45a56c7 100644 --- a/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java +++ b/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java @@ -482,7 +482,7 @@ private static void handleListOrHashArgument(Parser parser, ListNode args, boole parser.tokenIndex = saveIndex; } - ListNode argList = ListParser.parseZeroOrMoreList(parser, 0, false, false, false, false); + ListNode argList = ListParser.parseZeroOrMoreList(parser, 0, false, true, false, false); // @ and % consume remaining arguments in LIST context // for (Node element : argList.elements) { // element.setAnnotation("context", "LIST"); diff --git a/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java b/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java index 1fda20e8a..cb9e8dc84 100644 --- a/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/CustomFileChannel.java @@ -354,10 +354,14 @@ public RuntimeScalar sysread(int length) { return new RuntimeScalar(""); } + // Convert bytes to string representation buffer.flip(); - byte[] readBytes = new byte[buffer.remaining()]; - buffer.get(readBytes); - return new RuntimeScalar(readBytes); + StringBuilder result = new StringBuilder(bytesRead); + while (buffer.hasRemaining()) { + result.append((char) (buffer.get() & 0xFF)); + } + + return new RuntimeScalar(result.toString()); } catch (IOException e) { getGlobalVariable("main::!").set(e.getMessage()); return new RuntimeScalar(); // undef diff --git a/src/main/java/org/perlonjava/runtime/io/InternalPipeHandle.java b/src/main/java/org/perlonjava/runtime/io/InternalPipeHandle.java index 47487f898..c2f176e90 100644 --- a/src/main/java/org/perlonjava/runtime/io/InternalPipeHandle.java +++ b/src/main/java/org/perlonjava/runtime/io/InternalPipeHandle.java @@ -178,9 +178,13 @@ public RuntimeScalar sysread(int length) { return new RuntimeScalar(""); } - byte[] readBytes = new byte[bytesRead]; - System.arraycopy(buffer, 0, readBytes, 0, bytesRead); - return new RuntimeScalar(readBytes); + // Convert bytes to string representation + StringBuilder result = new StringBuilder(bytesRead); + for (int i = 0; i < bytesRead; i++) { + result.append((char) (buffer[i] & 0xFF)); + } + + return new RuntimeScalar(result.toString()); } catch (IOException e) { isEOF = true; getGlobalVariable("main::!").set(e.getMessage()); diff --git a/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java b/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java index 05e1dbce2..5f10b345c 100644 --- a/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java @@ -332,9 +332,13 @@ public RuntimeScalar sysread(int length) { return new RuntimeScalar(""); } - byte[] readBytes = new byte[bytesRead]; - System.arraycopy(buffer, 0, readBytes, 0, bytesRead); - return new RuntimeScalar(readBytes); + // Convert bytes to string representation + StringBuilder result = new StringBuilder(bytesRead); + for (int i = 0; i < bytesRead; i++) { + result.append((char) (buffer[i] & 0xFF)); + } + + return new RuntimeScalar(result.toString()); } catch (IOException e) { getGlobalVariable("main::!").set(e.getMessage()); return new RuntimeScalar(); // undef diff --git a/src/main/java/org/perlonjava/runtime/io/StandardIO.java b/src/main/java/org/perlonjava/runtime/io/StandardIO.java index 705a40fcf..a246558d3 100644 --- a/src/main/java/org/perlonjava/runtime/io/StandardIO.java +++ b/src/main/java/org/perlonjava/runtime/io/StandardIO.java @@ -285,9 +285,13 @@ public RuntimeScalar sysread(int length) { return new RuntimeScalar(""); } - byte[] readBytes = new byte[bytesRead]; - System.arraycopy(buffer, 0, readBytes, 0, bytesRead); - return new RuntimeScalar(readBytes); + // Convert bytes to string representation + StringBuilder result = new StringBuilder(bytesRead); + for (int i = 0; i < bytesRead; i++) { + result.append((char) (buffer[i] & 0xFF)); + } + + return new RuntimeScalar(result.toString()); } catch (IOException e) { getGlobalVariable("main::!").set(e.getMessage()); return new RuntimeScalar(); // undef diff --git a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java index eccbb790a..dae5aa00b 100644 --- a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java +++ b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java @@ -318,18 +318,18 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl if (GlobalVariable.existsGlobalCodeRef(normalizedClassMethodName)) { RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(normalizedClassMethodName); // Perl method lookup should ignore undefined CODE slots (e.g. after `undef *pkg::method`). - // But don't skip to next class — fall through to AUTOLOAD check for this class. - if (codeRef.getDefinedBoolean()) { - // Cache the found method - cacheMethod(cacheKey, codeRef); - - if (TRACE_METHOD_RESOLUTION) { - System.err.println(" FOUND method!"); - System.err.flush(); - } - - return codeRef; + if (!codeRef.getDefinedBoolean()) { + continue; + } + // Cache the found method + cacheMethod(cacheKey, codeRef); + + if (TRACE_METHOD_RESOLUTION) { + System.err.println(" FOUND method!"); + System.err.flush(); } + + return codeRef; } // Method not found in current class, check AUTOLOAD diff --git a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java index 635ab2770..5037fc64a 100644 --- a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java @@ -259,21 +259,7 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle) return scalarUndef; } - // Try to use the stored file path for file test - if (fh.filePath != null) { - Path path = fh.filePath; - try { - boolean lstat = operator.equals("-l"); - statForFileTest(fileHandle, path, lstat); - return evaluateFileTest(operator, path, fileHandle); - } catch (Exception e) { - getGlobalVariable("main::!").set(5); - updateLastStat(fileHandle, false, 5); - return scalarUndef; - } - } - - // For non-file handles (stdin, in-memory, socket), return undef and set EBADF + // For file test operators on file handles, return undef and set EBADF getGlobalVariable("main::!").set(9); updateLastStat(fileHandle, false, 9); return scalarUndef; @@ -320,211 +306,232 @@ public static RuntimeScalar fileTest(String operator, RuntimeScalar fileHandle) try { boolean lstat = operator.equals("-l"); statForFileTest(fileHandle, path, lstat); - return evaluateFileTest(operator, path, fileHandle); - } catch (IOException e) { - getGlobalVariable("main::!").set(2); - updateLastStat(fileHandle, false, 2); - return scalarUndef; - } - } - - private static RuntimeScalar evaluateFileTest(String operator, Path path, RuntimeScalar fileHandle) throws IOException { - String filename = path.toString(); - return switch (operator) { - case "-r" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + return switch (operator) { + case "-r" -> { + // Check if file is readable + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isReadable(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isReadable(path)); - } - case "-w" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-w" -> { + // Check if file is writable + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isWritable(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isWritable(path)); - } - case "-x" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-x" -> { + // Check if file is executable + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isExecutable(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isExecutable(path)); - } - case "-e" -> { - boolean exists = Files.exists(path); - if (!exists) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-e" -> { + // Check if file exists + boolean exists = Files.exists(path); + if (!exists) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield scalarTrue; } - getGlobalVariable("main::!").set(0); - yield scalarTrue; - } - case "-z" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-z" -> { + // Check if file is empty (zero size) + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.size(path) == 0); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.size(path) == 0); - } - case "-s" -> { - if (!lastStatOk) { - yield scalarUndef; + case "-s" -> { + // Return file size if non-zero, otherwise return false + if (!lastStatOk) { + yield scalarUndef; + } + long size = lastBasicAttr.size(); + yield size > 0 ? new RuntimeScalar(size) : RuntimeScalarCache.scalarZero; } - long size = lastBasicAttr.size(); - yield size > 0 ? new RuntimeScalar(size) : RuntimeScalarCache.scalarZero; - } - case "-f" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-f" -> { + // Check if path is a regular file + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isRegularFile(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isRegularFile(path)); - } - case "-d" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-d" -> { + // Check if path is a directory + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isDirectory(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isDirectory(path)); - } - case "-l" -> { - if (!lastStatOk) { - yield scalarUndef; + case "-l" -> { + // Check if path is a symbolic link + if (!lastStatOk) { + yield scalarUndef; + } + yield getScalarBoolean(lastBasicAttr.isSymbolicLink()); } - yield getScalarBoolean(lastBasicAttr.isSymbolicLink()); - } - case "-o" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-o" -> { + // Check if file is owned by the effective user id (approximate with current user) + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + UserPrincipal owner = Files.getOwner(path); + UserPrincipal currentUser = path.getFileSystem().getUserPrincipalLookupService() + .lookupPrincipalByName(System.getProperty("user.name")); + yield getScalarBoolean(owner.equals(currentUser)); } - getGlobalVariable("main::!").set(0); - UserPrincipal owner = Files.getOwner(path); - UserPrincipal currentUser = path.getFileSystem().getUserPrincipalLookupService() - .lookupPrincipalByName(System.getProperty("user.name")); - yield getScalarBoolean(owner.equals(currentUser)); - } - case "-p" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-p" -> { + // Approximate check for named pipe (FIFO) + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isRegularFile(path) && filename.endsWith(".fifo")); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isRegularFile(path) && filename.endsWith(".fifo")); - } - case "-S" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-S" -> { + // Approximate check for socket + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isRegularFile(path) && filename.endsWith(".sock")); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isRegularFile(path) && filename.endsWith(".sock")); - } - case "-b" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-b" -> { + // Approximate check for block special file + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isRegularFile(path) && filename.startsWith("/dev/")); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isRegularFile(path) && filename.startsWith("/dev/")); - } - case "-c" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-c" -> { + // Approximate check for character special file + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isRegularFile(path) && filename.startsWith("/dev/")); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isRegularFile(path) && filename.startsWith("/dev/")); - } - case "-u" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-u" -> { + // Check if setuid bit is set + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.OWNER_EXECUTE))); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.OWNER_EXECUTE))); - } - case "-g" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-g" -> { + // Check if setgid bit is set + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.GROUP_EXECUTE))); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.GROUP_EXECUTE))); - } - case "-k" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-k" -> { + // Approximate check for sticky bit (using others execute permission) + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.OTHERS_EXECUTE))); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean((Files.getPosixFilePermissions(path).contains(PosixFilePermission.OTHERS_EXECUTE))); - } - case "-T", "-B" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-T", "-B" -> { + // Check if file is text (-T) or binary (-B) + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield isTextOrBinary(path, operator.equals("-T")); } - getGlobalVariable("main::!").set(0); - yield isTextOrBinary(path, operator.equals("-T")); - } - case "-M", "-A", "-C" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-M", "-A", "-C" -> { + // Get file time difference for modification (-M), access (-A), or creation (-C) time + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getFileTimeDifference(path, operator); } - getGlobalVariable("main::!").set(0); - yield getFileTimeDifference(path, operator); - } - case "-R" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-R" -> { + // Check if file is readable by the real user ID + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isReadable(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isReadable(path)); - } - case "-W" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-W" -> { + // Check if file is writable by the real user ID + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isWritable(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isWritable(path)); - } - case "-X" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); - yield scalarUndef; + case "-X" -> { + // Check if file is executable by the real user ID + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + yield getScalarBoolean(Files.isExecutable(path)); } - getGlobalVariable("main::!").set(0); - yield getScalarBoolean(Files.isExecutable(path)); - } - case "-O" -> { - if (!Files.exists(path)) { - getGlobalVariable("main::!").set(2); + case "-O" -> { + // Check if file is owned by the current user + if (!Files.exists(path)) { + getGlobalVariable("main::!").set(2); // ENOENT + yield scalarUndef; + } + getGlobalVariable("main::!").set(0); // Clear error + UserPrincipal owner = Files.getOwner(path); + UserPrincipal currentUser = path.getFileSystem().getUserPrincipalLookupService() + .lookupPrincipalByName(System.getProperty("user.name")); + yield getScalarBoolean(owner.equals(currentUser)); + } + case "-t" -> { + // -t on a string filename is an error in Perl (expects a filehandle) + // Set $! = EBADF and return undef + getGlobalVariable("main::!").set(9); // EBADF yield scalarUndef; } - getGlobalVariable("main::!").set(0); - UserPrincipal owner = Files.getOwner(path); - UserPrincipal currentUser = path.getFileSystem().getUserPrincipalLookupService() - .lookupPrincipalByName(System.getProperty("user.name")); - yield getScalarBoolean(owner.equals(currentUser)); - } - case "-t" -> { - getGlobalVariable("main::!").set(9); - yield scalarUndef; - } - default -> throw new UnsupportedOperationException("Unsupported file test operator: " + operator); - }; + default -> throw new UnsupportedOperationException("Unsupported file test operator: " + operator); + }; + } catch (IOException e) { + // Set error message in global variable and return false/undef + getGlobalVariable("main::!").set(2); // ENOENT for most file operations + updateLastStat(fileHandle, false, 2); + return scalarUndef; + } } public static RuntimeScalar chainedFileTest(String[] operators, RuntimeScalar fileHandle) { diff --git a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java index a0f8e610a..78aeb24d1 100644 --- a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java @@ -726,9 +726,6 @@ public static RuntimeScalar sysread(int ctx, RuntimeBase... args) { newValue.append(data); target.set(newValue.toString()); - if (result.type == RuntimeScalarType.BYTE_STRING && target.type != RuntimeScalarType.TIED_SCALAR) { - target.type = RuntimeScalarType.BYTE_STRING; - } return new RuntimeScalar(bytesRead); } @@ -877,7 +874,7 @@ public static RuntimeScalar syswrite(int ctx, RuntimeBase... args) { /** * Checks if the handle has a :utf8 layer. */ - public static boolean hasUtf8Layer(RuntimeIO fh) { + private static boolean hasUtf8Layer(RuntimeIO fh) { IOHandle handle = fh.ioHandle; while (handle instanceof LayeredIOHandle layered) { String layers = layered.getCurrentLayers(); @@ -2142,18 +2139,7 @@ public static RuntimeScalar sysseek(int ctx, RuntimeBase... args) { } public static RuntimeScalar read(int ctx, RuntimeBase... args) { - RuntimeScalar result = sysread(ctx, args); - if (args.length >= 2) { - RuntimeScalar fileHandle = args[0].scalar(); - RuntimeIO fh = fileHandle.getRuntimeIO(); - if (fh != null && hasUtf8Layer(fh)) { - RuntimeScalar target = args[1].scalar().scalarDeref(); - if (target.type != RuntimeScalarType.TIED_SCALAR) { - target.type = RuntimeScalarType.STRING; - } - } - } - return result; + return sysread(ctx, args); } } diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java index 7160304ba..d6f8716ad 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Operator.java +++ b/src/main/java/org/perlonjava/runtime/operators/Operator.java @@ -290,11 +290,7 @@ public static RuntimeScalar substr(int ctx, RuntimeBase... args) { String extractedSubstring = result; lvalue.set(replacement); // Return the extracted substring, not the lvalue (which now contains the replacement) - RuntimeScalar extracted = new RuntimeScalar(extractedSubstring); - if (((RuntimeScalar) args[0]).type == RuntimeScalarType.BYTE_STRING) { - extracted.type = RuntimeScalarType.BYTE_STRING; - } - return extracted; + return new RuntimeScalar(extractedSubstring); } return lvalue; diff --git a/src/main/java/org/perlonjava/runtime/operators/Readline.java b/src/main/java/org/perlonjava/runtime/operators/Readline.java index 69da9d1cc..8d5b7ba65 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Readline.java +++ b/src/main/java/org/perlonjava/runtime/operators/Readline.java @@ -360,9 +360,6 @@ public static RuntimeScalar read(RuntimeList args) { // Update the scalar with the new value scalar.set(scalarValue.toString()); - if (!IOOperator.hasUtf8Layer(fh)) { - scalar.type = RuntimeScalarType.BYTE_STRING; - } // Return the number of characters read return new RuntimeScalar(charsRead); diff --git a/src/main/java/org/perlonjava/runtime/operators/Stat.java b/src/main/java/org/perlonjava/runtime/operators/Stat.java index 8dea5f768..cfce73aa6 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Stat.java +++ b/src/main/java/org/perlonjava/runtime/operators/Stat.java @@ -21,7 +21,6 @@ import static org.perlonjava.runtime.operators.FileTestOperator.lastBasicAttr; import static org.perlonjava.runtime.operators.FileTestOperator.lastFileHandle; import static org.perlonjava.runtime.operators.FileTestOperator.lastPosixAttr; -import static org.perlonjava.runtime.operators.FileTestOperator.lastStatOk; import static org.perlonjava.runtime.operators.FileTestOperator.updateLastStat; import static org.perlonjava.runtime.runtimetypes.GlobalVariable.getGlobalVariable; import static org.perlonjava.runtime.runtimetypes.RuntimeIO.resolvePath; @@ -64,38 +63,12 @@ private static int getPermissionsOctal(BasicFileAttributes basicAttr, PosixFileA return permissions; } - public static RuntimeBase statLastHandle(int ctx) { - if (!lastStatOk) { - getGlobalVariable("main::!").set(9); - RuntimeList empty = new RuntimeList(); - if (ctx == RuntimeContextType.SCALAR) { - return empty.statScalar(); - } - return empty; - } - RuntimeList res = new RuntimeList(); - statInternal(res, lastBasicAttr, lastPosixAttr); - if (ctx == RuntimeContextType.SCALAR) { - return res.statScalar(); - } - return res; + public static RuntimeList statLastHandle() { + return stat(lastFileHandle); } - public static RuntimeBase lstatLastHandle(int ctx) { - if (!lastStatOk) { - getGlobalVariable("main::!").set(9); - RuntimeList empty = new RuntimeList(); - if (ctx == RuntimeContextType.SCALAR) { - return empty.statScalar(); - } - return empty; - } - RuntimeList res = new RuntimeList(); - statInternal(res, lastBasicAttr, lastPosixAttr); - if (ctx == RuntimeContextType.SCALAR) { - return res.statScalar(); - } - return res; + public static RuntimeList lstatLastHandle() { + return lstat(lastFileHandle); } /** @@ -153,12 +126,8 @@ public static RuntimeList stat(RuntimeScalar arg) { return res; // Return empty list } - // Try to stat via the stored file path - if (fh.filePath != null) { - return statPath(arg, fh.filePath, res, false); - } - // For in-memory file handles (like PerlIO::scalar), we can't stat them + // They should return EBADF getGlobalVariable("main::!").set(9); updateLastStat(arg, false, 9, false); return res; @@ -166,34 +135,33 @@ public static RuntimeList stat(RuntimeScalar arg) { // Handle string arguments String filename = arg.toString(); - Path path = resolvePath(filename); - return statPath(arg, path, res, false); - } - private static RuntimeList statPath(RuntimeScalar arg, Path path, RuntimeList res, boolean lstat) { + // Handle regular filenames try { - BasicFileAttributes basicAttr; - PosixFileAttributes posixAttr; - if (lstat) { - basicAttr = Files.readAttributes(path, BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); - posixAttr = Files.readAttributes(path, PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS); - } else { - basicAttr = Files.readAttributes(path, BasicFileAttributes.class); - posixAttr = Files.readAttributes(path, PosixFileAttributes.class); - } + Path path = resolvePath(filename); + + // Basic file attributes (similar to some Perl stat fields) + BasicFileAttributes basicAttr = Files.readAttributes(path, BasicFileAttributes.class); + + // POSIX file attributes (for Unix-like systems) + PosixFileAttributes posixAttr = Files.readAttributes(path, PosixFileAttributes.class); lastBasicAttr = basicAttr; lastPosixAttr = posixAttr; statInternal(res, basicAttr, posixAttr); + // Clear $! on success getGlobalVariable("main::!").set(0); - updateLastStat(arg, true, 0, lstat); + updateLastStat(arg, true, 0, false); } catch (NoSuchFileException e) { + // Set $! to ENOENT (No such file or directory) = 2 getGlobalVariable("main::!").set(2); - updateLastStat(arg, false, 2, lstat); + updateLastStat(arg, false, 2, false); } catch (IOException e) { - getGlobalVariable("main::!").set(5); - updateLastStat(arg, false, 5, lstat); + // Returns the empty list if "stat" fails. + // Set a generic error code for other IO errors + getGlobalVariable("main::!").set(5); // EIO (Input/output error) + updateLastStat(arg, false, 5, false); } return res; } @@ -223,12 +191,8 @@ public static RuntimeList lstat(RuntimeScalar arg) { return res; // Return empty list } - // Try to lstat via the stored file path - if (fh.filePath != null) { - return statPath(arg, fh.filePath, res, true); - } - // For in-memory file handles (like PerlIO::scalar), we can't lstat them + // They should return EBADF getGlobalVariable("main::!").set(9); updateLastStat(arg, false, 9, true); return res; @@ -236,8 +200,37 @@ public static RuntimeList lstat(RuntimeScalar arg) { // Handle string arguments String filename = arg.toString(); - Path path = resolvePath(filename); - return statPath(arg, path, res, true); + + // Handle regular filenames + try { + Path path = resolvePath(filename); + + // Basic attributes without following symlink + BasicFileAttributes basicAttr = Files.readAttributes(path, + BasicFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + + // POSIX attributes without following symlink + PosixFileAttributes posixAttr = Files.readAttributes(path, + PosixFileAttributes.class, LinkOption.NOFOLLOW_LINKS); + + lastBasicAttr = basicAttr; + lastPosixAttr = posixAttr; + + statInternal(res, basicAttr, posixAttr); + // Clear $! on success + getGlobalVariable("main::!").set(0); + updateLastStat(arg, true, 0, true); + } catch (NoSuchFileException e) { + // Set $! to ENOENT (No such file or directory) = 2 + getGlobalVariable("main::!").set(2); + updateLastStat(arg, false, 2, true); + } catch (IOException e) { + // Returns the empty list if "lstat" fails. + // Set a generic error code for other IO errors + getGlobalVariable("main::!").set(5); // EIO (Input/output error) + updateLastStat(arg, false, 5, true); + } + return res; } public static void statInternal(RuntimeList res, BasicFileAttributes basicAttr, PosixFileAttributes posixAttr) { diff --git a/src/main/java/org/perlonjava/runtime/operators/Time.java b/src/main/java/org/perlonjava/runtime/operators/Time.java index c4a7dd499..85b5931da 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Time.java +++ b/src/main/java/org/perlonjava/runtime/operators/Time.java @@ -8,7 +8,7 @@ import java.time.ZoneId; import java.time.ZoneOffset; import java.time.ZonedDateTime; -import org.perlonjava.runtime.runtimetypes.RuntimeScalar; +import java.time.format.DateTimeFormatter; import java.util.concurrent.Executors; import java.util.concurrent.ScheduledExecutorService; import java.util.concurrent.ScheduledFuture; @@ -89,8 +89,8 @@ public static RuntimeList localtime(RuntimeList args, int ctx) { if (args.isEmpty()) { date = ZonedDateTime.now(); } else { - long epoch = (long) Math.floor(args.getFirst().getDouble()); - date = Instant.ofEpochSecond(epoch).atZone(ZoneId.systemDefault()); + long arg = args.getFirst().getInt(); + date = Instant.ofEpochSecond(arg).atZone(ZoneId.systemDefault()); } return getTimeComponents(ctx, date); } @@ -107,8 +107,8 @@ public static RuntimeList gmtime(RuntimeList args, int ctx) { if (args.isEmpty()) { date = ZonedDateTime.now(ZoneOffset.UTC); } else { - long epoch = (long) Math.floor(args.getFirst().getDouble()); - date = Instant.ofEpochSecond(epoch).atZone(ZoneId.of("UTC")); + long arg = args.getFirst().getInt(); + date = Instant.ofEpochSecond(arg).atZone(ZoneId.of("UTC")); } return getTimeComponents(ctx, date); } @@ -116,15 +116,7 @@ public static RuntimeList gmtime(RuntimeList args, int ctx) { private static RuntimeList getTimeComponents(int ctx, ZonedDateTime date) { RuntimeList res = new RuntimeList(); if (ctx == RuntimeContextType.SCALAR) { - String[] days = {"Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"}; - String[] months = {"Jan", "Feb", "Mar", "Apr", "May", "Jun", - "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"}; - int dow = date.getDayOfWeek().getValue(); // Mon=1..Sun=7 - String dayStr = days[dow - 1]; - String monStr = months[date.getMonth().getValue() - 1]; - int mday = date.getDayOfMonth(); - String timeStr = String.format("%02d:%02d:%02d", date.getHour(), date.getMinute(), date.getSecond()); - res.add(new RuntimeScalar(String.format("%s %s %2d %s %d", dayStr, monStr, mday, timeStr, date.getYear()))); + res.add(date.format(DateTimeFormatter.RFC_1123_DATE_TIME)); return res; } // 0 1 2 3 4 5 6 7 8 @@ -135,8 +127,7 @@ private static RuntimeList getTimeComponents(int ctx, ZonedDateTime date) { res.add(date.getDayOfMonth()); res.add(date.getMonth().getValue() - 1); res.add(date.getYear() - 1900); - int dow = date.getDayOfWeek().getValue(); // Mon=1..Sun=7 - res.add(dow == 7 ? 0 : dow); // Sun=0, Mon=1..Sat=6 + res.add(date.getDayOfWeek().getValue()); res.add(date.getDayOfYear() - 1); res.add(date.getZone().getRules().isDaylightSavings(date.toInstant()) ? 1 : 0); return res; diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index c7f1e0de8..48cf2a194 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -56,29 +56,6 @@ protected boolean removeEldestEntry(Map.Entry eldest) { public static String lastSuccessfulMatchString = null; // ${^LAST_SUCCESSFUL_PATTERN} public static RuntimeRegex lastSuccessfulPattern = null; - - public static Object[] saveMatchState() { - return new Object[]{ - globalMatcher, globalMatchString, - lastMatchedString, lastMatchStart, lastMatchEnd, - lastSuccessfulMatchedString, lastSuccessfulMatchStart, lastSuccessfulMatchEnd, - lastSuccessfulMatchString, lastSuccessfulPattern - }; - } - - public static void restoreMatchState(Object[] state) { - globalMatcher = (Matcher) state[0]; - globalMatchString = (String) state[1]; - lastMatchedString = (String) state[2]; - lastMatchStart = (Integer) state[3]; - lastMatchEnd = (Integer) state[4]; - lastSuccessfulMatchedString = (String) state[5]; - lastSuccessfulMatchStart = (Integer) state[6]; - lastSuccessfulMatchEnd = (Integer) state[7]; - lastSuccessfulMatchString = (String) state[8]; - lastSuccessfulPattern = (RuntimeRegex) state[9]; - } - // Indicates if \G assertion is used private final boolean useGAssertion = false; // Compiled regex pattern diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java b/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java index 4df66404d..b46d02d90 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java @@ -49,11 +49,6 @@ public static RuntimeGlob pushLocalVariable(RuntimeGlob variable) { return variable; } - public static void pushLocalDynamicState(DynamicState state) { - state.dynamicSaveState(); - variableStack.push(state); - } - /** * Pops dynamic variables from the stack until the stack size matches the specified target local level. * This is useful for restoring the stack to a previous state by removing any variables added after that state. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 950cf708e..43f16e70b 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -17,7 +17,6 @@ import org.perlonjava.backend.bytecode.BytecodeCompiler; import org.perlonjava.backend.bytecode.InterpretedCode; import org.perlonjava.backend.bytecode.InterpreterState; -import org.perlonjava.runtime.regex.RuntimeRegex; import java.lang.invoke.MethodHandle; import java.lang.invoke.MethodHandles; @@ -415,7 +414,6 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // Then when "say @arr" is parsed in the BEGIN, it resolves to BEGIN_PKG_x::@arr // which is aliased to the runtime array with values (a, b). Map capturedVars = capturedSymbolTable.getAllVisibleVariables(); - List evalAliasKeys = new ArrayList<>(); for (SymbolTable.SymbolEntry entry : capturedVars.values()) { if (!entry.name().equals("@_") && !entry.decl().isEmpty() && !entry.name().startsWith("&")) { if (!entry.decl().equals("our")) { @@ -435,7 +433,6 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // entry.name() is "@arr" but the key should be "packageName::arr" String varNameWithoutSigil = entry.name().substring(1); // Remove the sigil String fullName = packageName + "::" + varNameWithoutSigil; - evalAliasKeys.add(fullName); // Alias the global to the runtime value if (runtimeValue instanceof RuntimeArray) { @@ -568,17 +565,6 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje setCurrentScope(capturedSymbolTable); - // Clean up eval STRING aliases from global namespace. - // These aliases were created before parsing so BEGIN blocks inside the eval - // could access outer lexicals. After compilation, they are no longer needed. - // Leaving them would cause `my` re-declarations in loops to pick up stale - // values via retrieveBeginScalar instead of creating fresh objects. - for (String key : evalAliasKeys) { - GlobalVariable.globalVariables.remove(key); - GlobalVariable.globalArrays.remove(key); - GlobalVariable.globalHashes.remove(key); - } - // Store source lines in symbol table if $^P flags are set // Do this on both success and failure paths when flags require retention // Use the original evalString and actualFileName; AST may be null on failure @@ -755,9 +741,17 @@ public static RuntimeList evalStringWithInterpreter( Node ast = null; List tokens = null; - // Save dynamic variable level to restore after eval + // Save dynamic variable level to restore after eval. + // IMPORTANT: Scope InterpreterState.currentPackage around eval execution. + // This is the interpreter-side equivalent of the same scoping done in + // EvalStringHandler for the JVM bytecode path. Without this, SET_PACKAGE + // opcodes inside the eval permanently mutate the caller's package state, + // breaking caller() and subsequent eval compilations. + // See InterpreterState.currentPackage javadoc for the full design rationale. int dynamicVarLevel = DynamicVariableManager.getLocalLevel(); - List evalAliasKeys = new ArrayList<>(); + String savedPkg = InterpreterState.currentPackage.get().toString(); + DynamicVariableManager.pushLocalVariable(InterpreterState.currentPackage.get()); + InterpreterState.currentPackage.get().set(savedPkg); try { String evalString = code.toString(); @@ -806,7 +800,6 @@ public static RuntimeList evalStringWithInterpreter( String packageName = PersistentVariable.beginPackage(operatorAst.id); String varNameWithoutSigil = entry.name().substring(1); String fullName = packageName + "::" + varNameWithoutSigil; - evalAliasKeys.add(fullName); if (runtimeValue instanceof RuntimeArray) { GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue); @@ -1034,13 +1027,6 @@ public static RuntimeList evalStringWithInterpreter( // Restore dynamic variables (local) to their state before eval DynamicVariableManager.popToLocalLevel(dynamicVarLevel); - // Clean up eval STRING aliases from global namespace - for (String key : evalAliasKeys) { - GlobalVariable.globalVariables.remove(key); - GlobalVariable.globalArrays.remove(key); - GlobalVariable.globalHashes.remove(key); - } - // Store source lines in debugger symbol table if $^P flags are set // Do this on both success and failure paths when flags require retention // ast and tokens may be null if parsing failed early, but storeSourceLines handles that @@ -1714,11 +1700,13 @@ public boolean defined() { */ public RuntimeList apply(RuntimeArray a, int callContext) { if (constantValue != null) { + // Alternative way to create constants like: `$constant::{_CAN_PCS} = \$const` return new RuntimeList(constantValue); } try { + // Wait for the compilerThread to finish if it exists if (this.compilerSupplier != null) { - this.compilerSupplier.get(); + this.compilerSupplier.get(); // Wait for the task to finish } if (isStatic) { @@ -1727,13 +1715,17 @@ public RuntimeList apply(RuntimeArray a, int callContext) { return (RuntimeList) this.methodHandle.invoke(this.codeObject, a, callContext); } } catch (NullPointerException e) { + if (this.methodHandle == null) { throw new PerlCompilerException("Subroutine exists but has null method handle (possible compilation or registration error) at "); } else if (this.codeObject == null && !isStatic) { throw new PerlCompilerException("Subroutine exists but has null code object at "); } else { + // Original NPE from somewhere else throw new PerlCompilerException("Null pointer exception in subroutine call: " + e.getMessage() + " at "); } + + //throw new PerlCompilerException("Undefined subroutine called at "); } catch (InvocationTargetException e) { Throwable targetException = e.getTargetException(); if (!(targetException instanceof RuntimeException)) { @@ -1747,11 +1739,14 @@ public RuntimeList apply(RuntimeArray a, int callContext) { public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) { if (constantValue != null) { + // Alternative way to create constants like: `$constant::{_CAN_PCS} = \$const` return new RuntimeList(constantValue); } try { + // Wait for the compilerThread to finish if it exists if (this.compilerSupplier != null) { - this.compilerSupplier.get(); + // System.out.println("Waiting for compiler thread to finish..."); + this.compilerSupplier.get(); // Wait for the task to finish } if (isStatic) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java index 4021d2bbf..1d8d006ff 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java @@ -154,13 +154,6 @@ protected boolean removeEldestEntry(Map.Entry eldest) { */ public DirectoryIO directoryIO; - /** - * The file system path associated with this I/O handle, if opened from a file. - * Used by stat() and file test operators (-f, -s, etc.) on filehandles. - * Null for non-file handles (STDIN/STDOUT/STDERR, pipes, sockets, in-memory). - */ - public Path filePath; - /** * The name of the glob that owns this IO handle (e.g., "main::STDOUT"). * Used for stringification when the filehandle is used in string context. @@ -380,7 +373,6 @@ public static RuntimeIO open(String fileName, String mode) { // Initialize ioHandle with CustomFileChannel fh.ioHandle = new CustomFileChannel(filePath, options); - fh.filePath = filePath; // Add the handle to the LRU cache addHandle(fh.ioHandle); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java index bc6383058..9c020d76e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java @@ -240,15 +240,6 @@ public RuntimeList getList() { return this; } - public static void resolveMatchProxies(RuntimeList list) { - for (int i = 0; i < list.elements.size(); i++) { - RuntimeBase elem = list.elements.get(i); - if (elem instanceof ScalarSpecialVariable ssv) { - list.elements.set(i, ssv.getValueAsScalar()); - } - } - } - /** * Evaluates the boolean representation of the list. * diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeRegexState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeRegexState.java index ad6806052..e69de29bb 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeRegexState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeRegexState.java @@ -1,25 +0,0 @@ -package org.perlonjava.runtime.runtimetypes; - -import org.perlonjava.runtime.regex.RuntimeRegex; - -public class RuntimeRegexState implements DynamicState { - - private Object[] savedState; - - public static void pushLocal() { - DynamicVariableManager.pushLocalDynamicState(new RuntimeRegexState()); - } - - @Override - public void dynamicSaveState() { - savedState = RuntimeRegex.saveMatchState(); - } - - @Override - public void dynamicRestoreState() { - if (savedState != null) { - RuntimeRegex.restoreMatchState(savedState); - savedState = null; - } - } -} diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java index 85f898f3e..5fe125bc3 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeSubstrLvalue.java @@ -28,7 +28,7 @@ public RuntimeSubstrLvalue(RuntimeScalar parent, String str, int offset, int len this.offset = offset; this.length = length; - this.type = (parent.type == RuntimeScalarType.BYTE_STRING) ? RuntimeScalarType.BYTE_STRING : RuntimeScalarType.STRING; + this.type = RuntimeScalarType.STRING; this.value = str; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java index d7ff69f65..cfea89ab4 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java @@ -88,13 +88,7 @@ public RuntimeScalar set(RuntimeScalar value) { return super.set(value); } - @Override - public RuntimeList getList() { - RuntimeList list = new RuntimeList(); - this.addToList(list); - return list; - } - + // Add itself to a RuntimeArray. public void addToArray(RuntimeArray array) { array.elements.add(new RuntimeScalar(this.getValueAsScalar())); } @@ -114,7 +108,7 @@ public RuntimeScalar addToScalar(RuntimeScalar var) { * * @return The RuntimeScalar value of the special variable, or null if not available. */ - RuntimeScalar getValueAsScalar() { + private RuntimeScalar getValueAsScalar() { try { RuntimeScalar result = switch (variableId) { case CAPTURE -> { diff --git a/src/main/perl/lib/Time/Local.pm b/src/main/perl/lib/Time/Local.pm index 7cd8c5204..773414f72 100644 --- a/src/main/perl/lib/Time/Local.pm +++ b/src/main/perl/lib/Time/Local.pm @@ -5,7 +5,7 @@ use strict; use Carp (); use Exporter; -our $VERSION = '1.35'; +our $VERSION = '1.30'; use parent 'Exporter'; @@ -58,18 +58,6 @@ if ( $] < 5.012000 ) { else { # recent localtime()'s limit is the year 2**31 $MaxDay = 365 * ( 2**31 ); - - # On (some?) 32-bit platforms this overflows and we end up with a negative - # $MaxDay, which totally breaks this module. This is the old calculation - # we used from the days before Perl always had 64-bit time_t. - if ( $MaxDay < 0 ) { - require Config; - ## no critic (Variables::ProhibitPackageVars) - my $max_int - = ( ( 1 << ( 8 * $Config::Config{intsize} - 2 ) ) - 1 ) * 2 + 1; - $MaxDay - = int( ( $max_int - ( SECS_PER_DAY / 2 ) ) / SECS_PER_DAY ) - 1; - } } # Determine the EPOC day for this machine @@ -92,7 +80,7 @@ else { $Epoc = _daygm( gmtime(0) ); } -%Cheat = (); # clear the cache as epoc has changed +%Cheat = (); # clear the cache as epoc has changed sub _daygm { @@ -107,8 +95,7 @@ sub _daygm { + int( $year / 4 ) - int( $year / 100 ) + int( $year / 400 ) - + int( ( ( $month * 306 ) + 5 ) / 10 ) ) - - $Epoc; + + int( ( ( $month * 306 ) + 5 ) / 10 ) ) - $Epoc; } ); } @@ -124,8 +111,6 @@ sub _timegm { sub timegm { my ( $sec, $min, $hour, $mday, $month, $year ) = @_; - my $subsec = $sec - int($sec); - $sec = int($sec); if ( $Options{no_year_munging} ) { $year -= 1900; @@ -160,8 +145,9 @@ sub timegm { my $days = _daygm( undef, undef, undef, $mday, $month, $year ); - if ( abs($days) > $MaxDay && !$Options{no_range_check} ) { - my $msg = "Day too big - abs($days) > $MaxDay\n"; + unless ( $Options{no_range_check} or abs($days) < $MaxDay ) { + my $msg = q{}; + $msg .= "Day too big - $days > $MaxDay\n" if $days > $MaxDay; $year += 1900; $msg @@ -170,16 +156,11 @@ sub timegm { Carp::croak($msg); } - # Adding in the $subsec value last seems to prevent floating point errors - # from creeping in. - return ( - ( - $sec + $SecOff - + ( SECS_PER_MINUTE * $min ) - + ( SECS_PER_HOUR * $hour ) - + ( SECS_PER_DAY * $days ) - ) + $subsec - ); + return + $sec + $SecOff + + ( SECS_PER_MINUTE * $min ) + + ( SECS_PER_HOUR * $hour ) + + ( SECS_PER_DAY * $days ); } sub _is_leap_year { @@ -206,16 +187,11 @@ sub timegm_posix { } sub timelocal { - my $sec = shift; - my $subsec = $sec - int($sec); - $sec = int($sec); - unshift @_, $sec; - my $ref_t = &timegm; my $loc_for_ref_t = _timegm( localtime($ref_t) ); my $zone_off = $loc_for_ref_t - $ref_t - or return $loc_for_ref_t + $subsec; + or return $loc_for_ref_t; # Adjust for timezone my $loc_t = $ref_t - $zone_off; @@ -231,20 +207,20 @@ sub timelocal { && ( ( $ref_t - SECS_PER_HOUR ) - _timegm( localtime( $loc_t - SECS_PER_HOUR ) ) < 0 ) ) { - return ( $loc_t - SECS_PER_HOUR ) + $subsec; + return $loc_t - SECS_PER_HOUR; } # Adjust for DST change $loc_t += $dst_off; - return $loc_t + $subsec if $dst_off > 0; + return $loc_t if $dst_off > 0; # If the original date was a non-existent gap in a forward DST jump, we # should now have the wrong answer - undo the DST adjustment my ( $s, $m, $h ) = localtime($loc_t); $loc_t -= $dst_off if $s != $_[0] || $m != $_[1] || $h != $_[2]; - return $loc_t + $subsec; + return $loc_t; } sub timelocal_nocheck { @@ -278,7 +254,7 @@ Time::Local - Efficiently compute time from local and GMT time =head1 VERSION -version 1.35 +version 1.30 =head1 SYNOPSIS @@ -305,8 +281,6 @@ consistent with the values returned from C and C. =head2 C and C -I - These functions are the exact inverse of Perl's built-in C and C functions. That means that calling C<< timelocal_posix( localtime($value) ) >> will always give you the same C<$value> you started @@ -319,9 +293,9 @@ more details. These functions expect the year value to be the number of years since 1900, which is what the C and C built-ins returns. -They perform range checking by default on the input C<$sec>, C<$min>, C<$hour>, -C<$mday>, and C<$mon> values and will croak (using C) if given a -value outside the allowed ranges. +They perform range checking by default on the input C<$sec>, C<$min>, +C<$hour>, C<$mday>, and C<$mon> values and will croak (using C) +if given a value outside the allowed ranges. While it would be nice to make this the default behavior, that would almost certainly break a lot of code, so you must explicitly import these functions @@ -333,8 +307,6 @@ surprising. =head2 C and C -I - When C was first written, it was a common practice to represent years as a two-digit value like C<99> for C<1999> or C<1> for C<2001>. This caused all sorts of problems (google "Y2K problem" if you're very young) and @@ -344,26 +316,26 @@ The default exports of C and C do a complicated calculation when given a year value less than 1000. This leads to surprising results in many cases. See L for details. -The C functions do not do this year munging and simply take the -year value as provided. +The C functions do not do this year munging and simply take +the year value as provided. -They perform range checking by default on the input C<$sec>, C<$min>, C<$hour>, -C<$mday>, and C<$mon> values and will croak (using C) if given a -value outside the allowed ranges. +They perform range checking by default on the input C<$sec>, C<$min>, +C<$hour>, C<$mday>, and C<$mon> values and will croak (using C) +if given a value outside the allowed ranges. =head2 C and C This module exports two functions by default, C and C. -They perform range checking by default on the input C<$sec>, C<$min>, C<$hour>, -C<$mday>, and C<$mon> values and will croak (using C) if given a -value outside the allowed ranges. +They perform range checking by default on the input C<$sec>, C<$min>, +C<$hour>, C<$mday>, and C<$mon> values and will croak (using C) +if given a value outside the allowed ranges. -B or C<*_modern> -functions if possible.> +B or +C<*_modern> functions if possible.> =head2 C and C @@ -371,8 +343,8 @@ If you are working with data you know to be valid, you can use the "nocheck" variants, C and C. These variants must be explicitly imported. -If you supply data which is not valid (month 27, second 1,000) the results will -be unpredictable (so don't do that). +If you supply data which is not valid (month 27, second 1,000) the results +will be unpredictable (so don't do that). Note that my benchmarks show that this is just a 3% speed increase over the checked versions, so unless calling C is the hottest spot in your @@ -386,16 +358,17 @@ exports if you want to ensure consistent behavior as your code ages.> Strictly speaking, the year should be specified in a form consistent with C, i.e. the offset from 1900. In order to make the interpretation -of the year easier for humans, however, who are more accustomed to seeing years -as two-digit or four-digit values, the following conventions are followed: +of the year easier for humans, however, who are more accustomed to seeing +years as two-digit or four-digit values, the following conventions are +followed: =over 4 =item * Years greater than 999 are interpreted as being the actual year, rather than -the offset from 1900. Thus, 1964 would indicate the year Martin Luther King won -the Nobel prize, not the year 3864. +the offset from 1900. Thus, 1964 would indicate the year Martin Luther King +won the Nobel prize, not the year 3864. =item * @@ -406,11 +379,11 @@ below regarding date range). =item * Years in the range 0..99 are interpreted as shorthand for years in the rolling -"current century," defined as 50 years on either side of the current year. -Thus, today, in 1999, 0 would refer to 2000, and 45 to 2045, but 55 would refer -to 1955. Twenty years from now, 55 would instead refer to 2055. This is messy, -but matches the way people currently think about two digit dates. Whenever -possible, use an absolute four digit year instead. +"current century," defined as 50 years on either side of the current +year. Thus, today, in 1999, 0 would refer to 2000, and 45 to 2045, but 55 +would refer to 1955. Twenty years from now, 55 would instead refer to +2055. This is messy, but matches the way people currently think about two +digit dates. Whenever possible, use an absolute four digit year instead. =back @@ -441,15 +414,15 @@ occurs for two different GMT times on the same day. For example, in the "Europe/Paris" time zone, the local time of 2001-10-28 02:30:00 can represent either 2001-10-28 00:30:00 GMT, B 2001-10-28 01:30:00 GMT. -When given an ambiguous local time, the timelocal() function will always return -the epoch for the I of the two possible GMT times. +When given an ambiguous local time, the timelocal() function will always +return the epoch for the I of the two possible GMT times. =head2 Non-Existent Local Times (DST) -When a DST change causes a locale clock to skip one hour forward, there will be -an hour's worth of local times that don't exist. Again, for the "Europe/Paris" -time zone, the local clock jumped from 2001-03-25 01:59:59 to 2001-03-25 -03:00:00. +When a DST change causes a locale clock to skip one hour forward, there will +be an hour's worth of local times that don't exist. Again, for the +"Europe/Paris" time zone, the local clock jumped from 2001-03-25 01:59:59 to +2001-03-25 03:00:00. If the C function is given a non-existent local time, it will simply return an epoch value for the time one hour later. @@ -472,20 +445,21 @@ These routines are quite efficient and yet are always guaranteed to agree with C and C. We manage this by caching the start times of any months we've seen before. If we know the start time of the month, we can always calculate any time within the month. The start times are calculated -using a mathematical formula. Unlike other algorithms that do multiple calls to -C. +using a mathematical formula. Unlike other algorithms that do multiple calls +to C. -The C function is implemented using the same cache. We just assume -that we're translating a GMT time, and then fudge it when we're done for the -timezone and daylight savings arguments. Note that the timezone is evaluated -for each date because countries occasionally change their official timezones. -Assuming that C corrects for these changes, this routine will also -be correct. +The C function is implemented using the same cache. We just +assume that we're translating a GMT time, and then fudge it when we're done +for the timezone and daylight savings arguments. Note that the timezone is +evaluated for each date because countries occasionally change their official +timezones. Assuming that C corrects for these changes, this +routine will also be correct. =head1 AUTHORS EMERITUS -This module is based on a Perl 4 library, timelocal.pl, that was included with -Perl 4.036, and was most likely written by Tom Christiansen. +This module is based on a Perl 4 library, timelocal.pl, that was +included with Perl 4.036, and was most likely written by Tom +Christiansen. The current version was written by Graham Barr. @@ -498,6 +472,8 @@ Bugs may be submitted at L. There is a mailing list available for users of this distribution, L. +I am also usually active on IRC as 'autarch' on C. + =head1 SOURCE The source code repository for Time-Local can be found at L. @@ -508,7 +484,7 @@ Dave Rolsky =head1 CONTRIBUTORS -=for stopwords Florian Ragwitz Gregory Oschwald J. Nick Koston Tom Wyant Unknown +=for stopwords Florian Ragwitz J. Nick Koston Unknown =over 4 @@ -518,25 +494,17 @@ Florian Ragwitz =item * -Gregory Oschwald - -=item * - J. Nick Koston =item * -Tom Wyant - -=item * - Unknown =back =head1 COPYRIGHT AND LICENSE -This software is copyright (c) 1997 - 2023 by Graham Barr & Dave Rolsky. +This software is copyright (c) 1997 - 2020 by Graham Barr & Dave Rolsky. This is free software; you can redistribute it and/or modify it under the same terms as the Perl 5 programming language system itself.