diff --git a/README.md b/README.md index 29314e4a2..e10aa727e 100644 --- a/README.md +++ b/README.md @@ -17,7 +17,7 @@ A Perl compiler and runtime for the JVM that: - Compiles Perl scripts to Java bytecode - Integrates with Java libraries (JDBC databases, Maven dependencies) - Supports most Perl 5.42 features -- Includes 150+ core Perl modules (DBI, HTTP::Tiny, JSON, YAML, Text::CSV, Time::Piece) +- Includes 150+ core Perl modules (DBI, HTTP::Tiny, JSON, YAML, Text::CSV) ## Quick Start diff --git a/docs/about/changelog.md b/docs/about/changelog.md index c3b11a41b..a78315038 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -8,7 +8,7 @@ Release history of PerlOnJava. See [Roadmap](roadmap.md) for future plans. - Perl debugger with `-d` - Non-local control flow: `last`/`next`/`redo`/`goto LABEL` - Tail call with trampoline for `goto &NAME` and `goto __SUB__` -- Add modules: `Time::Piece`, `TOML`. +- Add modules: `TOML`. - Bugfix: operator override in Time::Hires now works. - Bugfix: internal temp variables are now pre-initialized. - Optimization: faster list assignment. diff --git a/docs/reference/feature-matrix.md b/docs/reference/feature-matrix.md index 2b2b99794..02fde7de9 100644 --- a/docs/reference/feature-matrix.md +++ b/docs/reference/feature-matrix.md @@ -706,8 +706,6 @@ The `:encoding()` layer supports all encodings provided by Java's `Charset.forNa - ✅ **Tie::Scalar** module. - ✅ **Time::HiRes** module. - ✅ **Time::Local** module. -- ✅ **Time::Piece** module. -- ✅ **Time::Seconds** module. - ✅ **UNIVERSAL**: `isa`, `can`, `DOES`, `VERSION` are implemented. `isa` operator is implemented. - ✅ **URI::Escape** module. - ✅ **Socket** module: socket constants and functions (`pack_sockaddr_in`, `unpack_sockaddr_in`, `sockaddr_in`, `inet_aton`, `inet_ntoa`, `gethostbyname`). diff --git a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java index 9694839ba..68a107051 100644 --- a/src/main/java/org/perlonjava/app/cli/ArgumentParser.java +++ b/src/main/java/org/perlonjava/app/cli/ArgumentParser.java @@ -318,10 +318,8 @@ private static int processClusteredSwitches(String[] args, CompilerOptions parse return index; case 'w': - // enable many useful warnings via $^W (old-style global warnings) - // Note: This intentionally does NOT add "use warnings" - $^W=1 at initialization - // is sufficient and avoids line number offset issues - parsedArgs.warnFlag = true; + // enable many useful warnings + parsedArgs.moduleUseStatements.add(new ModuleUseStatement(switchChar, "warnings", null, false)); break; case 'W': // enable all warnings diff --git a/src/main/java/org/perlonjava/app/cli/CompilerOptions.java b/src/main/java/org/perlonjava/app/cli/CompilerOptions.java index 960f56311..447096908 100644 --- a/src/main/java/org/perlonjava/app/cli/CompilerOptions.java +++ b/src/main/java/org/perlonjava/app/cli/CompilerOptions.java @@ -67,7 +67,6 @@ public class CompilerOptions implements Cloneable { public boolean allowUnsafeOperations = false; // For -U public boolean runUnderDebugger = false; // For -d public boolean taintWarnings = false; // For -t - public boolean warnFlag = false; // For -w (sets $^W = 1) public String debugFlags = ""; // For -D // Unicode/encoding flags for -C switches public boolean unicodeStdin = false; // -CS or -CI diff --git a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java index bd7119461..c046501f2 100644 --- a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java @@ -80,6 +80,10 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions, boolean isTopLevelScript, int callerContext) throws Exception { + // Save the current scope so we can restore it after execution. + // This is critical because require/do should not leak their scope to the caller. + ScopedSymbolTable savedCurrentScope = SpecialBlockParser.getCurrentScope(); + // Store the isMainProgram flag in CompilerOptions for use during code generation compilerOptions.isMainProgram = isTopLevelScript; @@ -184,11 +188,19 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions, ctx.symbolTable = ctx.symbolTable.snapShot(); SpecialBlockParser.setCurrentScope(ctx.symbolTable); - // Compile to executable (compiler or interpreter based on flag) - RuntimeCode runtimeCode = compileToExecutable(ast, ctx); + try { + // Compile to executable (compiler or interpreter based on flag) + RuntimeCode runtimeCode = compileToExecutable(ast, ctx); - // Execute (unified path for both backends) - return executeCode(runtimeCode, ctx, isTopLevelScript, callerContext); + // Execute (unified path for both backends) + return executeCode(runtimeCode, ctx, isTopLevelScript, callerContext); + } finally { + // Restore the caller's scope so require/do doesn't leak its scope to the caller. + // But do NOT restore for top-level scripts - we want the main script's pragmas to persist. + if (savedCurrentScope != null && !isTopLevelScript) { + SpecialBlockParser.setCurrentScope(savedCurrentScope); + } + } } /** @@ -203,6 +215,9 @@ public static RuntimeList executePerlAST(Node ast, List tokens, CompilerOptions compilerOptions) throws Exception { + // Save the current scope so we can restore it after execution. + ScopedSymbolTable savedCurrentScope = SpecialBlockParser.getCurrentScope(); + ScopedSymbolTable globalSymbolTable = new ScopedSymbolTable(); globalSymbolTable.enterScope(); globalSymbolTable.addVariable("this", "", null); @@ -236,11 +251,18 @@ public static RuntimeList executePerlAST(Node ast, ctx.symbolTable = ctx.symbolTable.snapShot(); SpecialBlockParser.setCurrentScope(ctx.symbolTable); - // Compile to executable (compiler or interpreter based on flag) - RuntimeCode runtimeCode = compileToExecutable(ast, ctx); + try { + // Compile to executable (compiler or interpreter based on flag) + RuntimeCode runtimeCode = compileToExecutable(ast, ctx); - // executePerlAST is always called from special blocks which use VOID context - return executeCode(runtimeCode, ctx, false, RuntimeContextType.VOID); + // executePerlAST is always called from special blocks which use VOID context + return executeCode(runtimeCode, ctx, false, RuntimeContextType.VOID); + } finally { + // Restore the caller's scope + if (savedCurrentScope != null) { + SpecialBlockParser.setCurrentScope(savedCurrentScope); + } + } } /** diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java b/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java index 55f21ee95..0da0c6502 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java @@ -7,8 +7,6 @@ import org.perlonjava.frontend.analysis.RegexUsageDetector; import org.perlonjava.frontend.astnode.*; import org.perlonjava.runtime.perlmodule.Warnings; -import org.perlonjava.runtime.runtimetypes.GlobalContext; -import org.perlonjava.runtime.runtimetypes.GlobalVariable; import org.perlonjava.runtime.runtimetypes.RuntimeContextType; public class EmitForeach { @@ -134,18 +132,10 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { if (variableNode instanceof OperatorNode opNode && (opNode.operator.equals("my") || opNode.operator.equals("our"))) { isDeclaredInFor = true; - // Shadow warning is emitted during parsing in OperatorParser.addVariableToScope() - // For loop variables, we need to temporarily disable it to avoid spurious warnings - // Check both lexical 'shadow' category and $^W - boolean isWarningEnabled = Warnings.warningManager.isWarningEnabled("shadow"); - boolean isWarnVarEnabled = GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("W")).getBoolean(); + boolean isWarningEnabled = Warnings.warningManager.isWarningEnabled("redefine"); if (isWarningEnabled) { - // turn off lexical "shadow" warning for loop variables - Warnings.warningManager.setWarningState("shadow", false); - } - if (isWarnVarEnabled) { - // temporarily turn off $^W for loop variables - GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("W")).set(0); + // turn off "masks earlier declaration" warning + Warnings.warningManager.setWarningState("redefine", false); } // emit the variable declarations variableNode.accept(emitterVisitor.with(RuntimeContextType.VOID)); @@ -170,12 +160,8 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { } if (isWarningEnabled) { - // restore lexical warnings - Warnings.warningManager.setWarningState("shadow", true); - } - if (isWarnVarEnabled) { - // restore $^W - GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("W")).set(1); + // restore warnings + Warnings.warningManager.setWarningState("redefine", true); } // Reset global variable check after rewriting diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index 0cde29b47..08867fc82 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -262,26 +262,8 @@ static void handleIndexBuiltin(EmitterVisitor emitterVisitor, OperatorNode node) static void handleAtan2(EmitterVisitor emitterVisitor, OperatorNode node) { EmitterVisitor scalarVisitor = emitterVisitor.with(RuntimeContextType.SCALAR); if (node.operand instanceof ListNode operand) { - // Spill the first operand before evaluating the second so non-local control flow - // propagation can't jump to returnLabel with an extra value on the JVM operand stack. - MethodVisitor mv = emitterVisitor.ctx.mv; operand.elements.get(0).accept(scalarVisitor); - - int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); - boolean pooled = leftSlot >= 0; - if (!pooled) { - leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); - } - mv.visitVarInsn(Opcodes.ASTORE, leftSlot); - operand.elements.get(1).accept(scalarVisitor); - - mv.visitVarInsn(Opcodes.ALOAD, leftSlot); - mv.visitInsn(Opcodes.SWAP); - - if (pooled) { - emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); - } emitOperator(node, emitterVisitor); } } @@ -559,31 +541,9 @@ static void handleGlobBuiltin(EmitterVisitor emitterVisitor, OperatorNode node) // Handles the 'range' operator, which creates a range of values. static void handleRangeOperator(EmitterVisitor emitterVisitor, BinaryOperatorNode node) { - // Spill the left operand before evaluating the right side so non-local control flow - // propagation can't jump to returnLabel with an extra value on the JVM operand stack. - if (ENABLE_SPILL_BINARY_LHS) { - MethodVisitor mv = emitterVisitor.ctx.mv; - node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); - - int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); - boolean pooled = leftSlot >= 0; - if (!pooled) { - leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); - } - mv.visitVarInsn(Opcodes.ASTORE, leftSlot); - - node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); - - mv.visitVarInsn(Opcodes.ALOAD, leftSlot); - mv.visitInsn(Opcodes.SWAP); - - if (pooled) { - emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); - } - } else { - node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); - node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); - } + // Accept both left and right operands in SCALAR context. + node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + node.right.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); emitOperator(node, emitterVisitor); } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorDeleteExists.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorDeleteExists.java index 15a0c3b68..a4781e17a 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorDeleteExists.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorDeleteExists.java @@ -103,18 +103,9 @@ private static void handleDeleteExistsInner(OperatorNode node, EmitterVisitor em // Check if this is a compound expression like $hash->{key}[index] if (binop.left instanceof BinaryOperatorNode leftBinop && leftBinop.operator.equals("->")) { // Handle compound hash->array dereference for exists/delete - // Spill the left operand before evaluating the index so non-local control flow - // propagation can't jump to returnLabel with an extra value on the JVM operand stack. - MethodVisitor mv = emitterVisitor.ctx.mv; + // First evaluate the hash dereference to get the array leftBinop.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); - int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot(); - boolean pooled = leftSlot >= 0; - if (!pooled) { - leftSlot = emitterVisitor.ctx.symbolTable.allocateLocalVariable(); - } - mv.visitVarInsn(Opcodes.ASTORE, leftSlot); - // Now emit the index if (binop.right instanceof ArrayLiteralNode arrayLiteral && arrayLiteral.elements.size() == 1) { @@ -125,13 +116,6 @@ private static void handleDeleteExistsInner(OperatorNode node, EmitterVisitor em emitterVisitor.ctx.errorUtil); } - mv.visitVarInsn(Opcodes.ALOAD, leftSlot); - mv.visitInsn(Opcodes.SWAP); - - if (pooled) { - emitterVisitor.ctx.javaClassInfo.releaseSpillSlot(); - } - // Call the appropriate method if (operator.equals("exists")) { emitterVisitor.ctx.mv.visitMethodInsn( diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java index 279fddad9..e58d6b937 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java @@ -1092,8 +1092,15 @@ static void handleMyOperator(EmitterVisitor emitterVisitor, OperatorNode node) { String name = ((IdentifierNode) identifierNode).name; String var = sigil + name; emitterVisitor.ctx.logDebug("MY " + operator + " " + sigil + name); - // Note: shadow warning is emitted during parsing in OperatorParser.addVariableToScope() - // We don't emit it again here to avoid duplicates + if (emitterVisitor.ctx.symbolTable.getVariableIndexInCurrentScope(var) != -1) { + if (Warnings.warningManager.isWarningEnabled("redefine")) { + System.err.println( + emitterVisitor.ctx.errorUtil.errorMessage(node.getIndex(), + "Warning: \"" + operator + "\" variable " + + var + + " masks earlier declaration in same ctx.symbolTable")); + } + } int varIndex = emitterVisitor.ctx.symbolTable.addVariable(var, operator, sigilNode); // TODO optimization - SETVAR+MY can be combined diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index a85b142f0..3bf28c7a6 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -6,7 +6,6 @@ import org.perlonjava.frontend.lexer.LexerToken; import org.perlonjava.runtime.operators.WarnDie; import org.perlonjava.runtime.perlmodule.Strict; -import org.perlonjava.runtime.runtimetypes.GlobalContext; import org.perlonjava.runtime.runtimetypes.GlobalVariable; import org.perlonjava.runtime.runtimetypes.NameNormalizer; import org.perlonjava.runtime.runtimetypes.PerlCompilerException; @@ -233,16 +232,11 @@ private static void addVariableToScope(EmitterContext ctx, String operator, Oper String name = ((IdentifierNode) identifierNode).name; String var = sigil + name; if (ctx.symbolTable.getVariableIndexInCurrentScope(var) != -1) { - // Check if shadow warnings are enabled via 'use warnings "shadow"' or via $^W (-w flag) - boolean shadowEnabled = ctx.symbolTable.isWarningCategoryEnabled("shadow") - || GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("W")).getBoolean(); - if (shadowEnabled) { - // "our" uses "redeclared", "my"/"state" use "masks earlier declaration in same scope" - String message = operator.equals("our") - ? "\"" + operator + "\" variable " + var + " redeclared" - : "\"" + operator + "\" variable " + var + " masks earlier declaration in same scope"; - System.err.print(ctx.errorUtil.warningMessage(node.getIndex(), message)); - } + System.err.println( + ctx.errorUtil.errorMessage(node.getIndex(), + "Warning: \"" + operator + "\" variable " + + var + + " masks earlier declaration in same ctx.symbolTable")); } int varIndex = ctx.symbolTable.addVariable(var, operator, node); // Note: the isDeclaredReference flag is stored in node.annotations diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java index 692797217..dda9d18e1 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java @@ -18,6 +18,7 @@ import static org.perlonjava.frontend.parser.NumberParser.parseNumber; import static org.perlonjava.frontend.parser.ParserNodeUtils.atUnderscoreArgs; import static org.perlonjava.frontend.parser.ParserNodeUtils.scalarUnderscore; +import static org.perlonjava.frontend.parser.SpecialBlockParser.getCurrentScope; import static org.perlonjava.frontend.parser.SpecialBlockParser.runSpecialBlock; import static org.perlonjava.frontend.parser.SpecialBlockParser.setCurrentScope; import static org.perlonjava.frontend.parser.StringParser.parseVstring; @@ -533,6 +534,12 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) { useWarnings(new RuntimeArray( new RuntimeScalar("warnings"), new RuntimeScalar("all")), RuntimeContextType.VOID); + // Copy warning flags to ALL levels of the parser's symbol table + // This matches what's done after import() for 'use warnings' + java.util.BitSet currentWarnings = getCurrentScope().warningFlagsStack.peek(); + for (int i = 0; i < parser.ctx.symbolTable.warningFlagsStack.size(); i++) { + parser.ctx.symbolTable.warningFlagsStack.set(i, (java.util.BitSet) currentWarnings.clone()); + } } } } diff --git a/src/main/java/org/perlonjava/runtime/operators/ListOperators.java b/src/main/java/org/perlonjava/runtime/operators/ListOperators.java index 880a0a1b5..96993004b 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ListOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ListOperators.java @@ -108,6 +108,9 @@ public static RuntimeList sort(RuntimeList runtimeList, RuntimeScalar perlCompar // Retrieve the comparison result and return it as an integer return result.getFirst().getInt(); + } catch (PerlExitException e) { + // exit() should propagate immediately - don't wrap it + throw e; } catch (Exception e) { // Wrap any exceptions thrown by the comparator in a RuntimeException throw new RuntimeException(e); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java index 4d792d376..77efd771d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrorMessageUtil.java @@ -209,18 +209,6 @@ public String errorMessage(int index, String message) { return message + " at " + loc.fileName() + " line " + loc.lineNumber() + ", near " + errorMessageQuote(nearString) + "\n"; } - /** - * Constructs a warning message without "near" context, matching Perl's warning format. - * - * @param index the index of the token where the warning occurred - * @param message the warning message - * @return the formatted warning message - */ - public String warningMessage(int index, String message) { - SourceLocation loc = getSourceLocationAccurate(index); - return message + " at " + loc.fileName() + " line " + loc.lineNumber() + ".\n"; - } - private String buildNearString(int index) { int end = Math.min(tokens.size() - 1, index + 5); StringBuilder sb = new StringBuilder(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java index 646b2e12a..fa695fcd2 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java @@ -60,11 +60,6 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { GlobalVariable.getGlobalVariable("main::" + Character.toString('X' - 'A' + 1)).set("jperl"); } - // Initialize $^W based on -w flag - if (compilerOptions.warnFlag) { - GlobalVariable.getGlobalVariable(encodeSpecialVar("W")).set(1); - } - GlobalVariable.getGlobalVariable("main::]").set(Configuration.getPerlVersionOld()); // initialize $] to Perl version GlobalVariable.getGlobalVariable("main::@").set(""); // initialize $@ to "" GlobalVariable.getGlobalVariable("main::_"); // initialize $_ to "undef" diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 87c9d4ed4..64d14d1a0 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -30,6 +30,7 @@ import java.util.function.Supplier; import static org.perlonjava.frontend.parser.ParserTables.CORE_PROTOTYPES; +import static org.perlonjava.frontend.parser.SpecialBlockParser.getCurrentScope; import static org.perlonjava.frontend.parser.SpecialBlockParser.setCurrentScope; import static org.perlonjava.runtime.runtimetypes.GlobalVariable.getGlobalVariable; import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarUndef; @@ -323,6 +324,11 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // Retrieve the eval context that was saved at program compile-time EmitterContext ctx = RuntimeCode.evalContext.get(evalTag); + // Save the current scope so we can restore it after eval compilation. + // This is critical because eval may be called from code compiled with different + // warning/feature flags than the caller, and we must not leak the eval's scope. + ScopedSymbolTable savedCurrentScope = getCurrentScope(); + // Store runtime values in ThreadLocal so SpecialBlockParser can access them during parsing. // This enables BEGIN blocks to see outer lexical variables' runtime values. // @@ -583,7 +589,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje capturedHintHash.elements.clear(); capturedHintHash.elements.putAll(savedHintHash); - setCurrentScope(capturedSymbolTable); + // Note: Scope restoration moved to outer finally block to handle cache hits // Clean up BEGIN aliases for captured variables after compilation. // These aliases were only needed during parsing (for BEGIN blocks to access @@ -615,6 +621,11 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje return generatedClass; } finally { + // Restore the original current scope, not the captured symbol table. + // This prevents eval from leaking its compile-time scope to the caller. + // This MUST be in the outer finally to handle both cache hits and compilation paths. + setCurrentScope(savedCurrentScope); + // Clean up ThreadLocal to prevent memory leaks // IMPORTANT: Always clean up ThreadLocal in finally block to ensure it's removed // even if compilation fails. Failure to do so could cause memory leaks in @@ -761,6 +772,11 @@ public static RuntimeList evalStringWithInterpreter( // Retrieve the eval context that was saved at program compile-time EmitterContext ctx = RuntimeCode.evalContext.get(evalTag); + // Save the current scope so we can restore it after eval execution. + // This is critical because eval may be called from code compiled with different + // warning/feature flags than the caller, and we must not leak the eval's scope. + ScopedSymbolTable savedCurrentScope = getCurrentScope(); + // Store runtime values in ThreadLocal for BEGIN block support EvalRuntimeContext runtimeCtx = new EvalRuntimeContext( runtimeValues, @@ -1081,6 +1097,10 @@ public static RuntimeList evalStringWithInterpreter( // Restore dynamic variables (local) to their state before eval DynamicVariableManager.popToLocalLevel(dynamicVarLevel); + // Restore the original current scope, not the captured symbol table. + // This prevents eval from leaking its compile-time scope to the caller. + setCurrentScope(savedCurrentScope); + // 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 diff --git a/src/main/perl/lib/POSIX.pm b/src/main/perl/lib/POSIX.pm index bb0a6f8ce..1a13dde1c 100644 --- a/src/main/perl/lib/POSIX.pm +++ b/src/main/perl/lib/POSIX.pm @@ -289,24 +289,6 @@ BEGIN { } } -# Time functions -sub strftime { - my ($fmt, $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = @_; - # wday, yday, isdst are ignored per POSIX spec - $wday //= -1; - $yday //= -1; - $isdst //= -1; - return POSIX::_strftime($fmt, $sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst); -} - -sub mktime { - my ($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst) = @_; - $wday //= -1; - $yday //= -1; - $isdst //= -1; - return POSIX::_mktime($sec, $min, $hour, $mday, $mon, $year, $wday, $yday, $isdst); -} - # Exit status macros sub WIFEXITED { POSIX::_WIFEXITED(@_) } sub WEXITSTATUS { POSIX::_WEXITSTATUS(@_) }