From a0001a705e8743a60fc4429e04802efc6e2f19e5 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 8 May 2026 18:38:09 +0200 Subject: [PATCH 1/2] fix: make Role::Tiny pass under jcpan Fix several Perl compatibility gaps exposed by Role::Tiny: - preserve declared-but-undefined CODE slot semantics for exists/defined - resolve dynamic subroutine lookups in the current package - apply overloads installed after objects were blessed - route imported forward declarations through source package AUTOLOAD - restore deleted keys from localized hash slices Add Role::Tiny-focused regression coverage for these cases. Generated with [OpenAI Codex](https://openai.com/codex) Co-Authored-By: OpenAI Codex --- .../backend/jvm/EmitOperatorDeleteExists.java | 3 +- .../backend/jvm/EmitOperatorLocal.java | 3 + .../runtime/mro/InheritanceResolver.java | 6 +- .../runtime/runtimetypes/GlobalVariable.java | 17 +-- .../runtime/runtimetypes/Overload.java | 10 +- .../runtime/runtimetypes/OverloadContext.java | 5 +- .../runtime/runtimetypes/RuntimeCode.java | 104 +++++++++++++----- .../runtime/runtimetypes/RuntimeHash.java | 23 ++++ .../runtime/runtimetypes/RuntimeScalar.java | 3 +- .../resources/unit/role_tiny_regressions.t | 94 ++++++++++++++++ 10 files changed, 217 insertions(+), 51 deletions(-) create mode 100644 src/test/resources/unit/role_tiny_regressions.t diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorDeleteExists.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorDeleteExists.java index 98cd08650..ac0d512fc 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorDeleteExists.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorDeleteExists.java @@ -261,11 +261,12 @@ private static void handleExistsSubroutine(EmitterVisitor emitterVisitor, String private static void handleExistsSubroutine(EmitterVisitor emitterVisitor, String operator, OperatorNode operatorNode) { // exists &{"sub"} operatorNode.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); + emitterVisitor.pushCurrentPackage(); emitterVisitor.ctx.mv.visitMethodInsn( Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/GlobalVariable", operator + "GlobalCodeRefAsScalar", - "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Ljava/lang/String;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); EmitOperator.handleVoidContext(emitterVisitor); } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorLocal.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorLocal.java index 0b0ef54f5..7ce71e9a2 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperatorLocal.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperatorLocal.java @@ -219,6 +219,9 @@ static void handleLocal(EmitterVisitor emitterVisitor, OperatorNode node) { && binNode.left instanceof OperatorNode sigNode && sigNode.operator.equals("$") && sigNode.operand instanceof IdentifierNode) { Dereference.handleHashElementOperator(emitterVisitor.with(lvalueContext), binNode, "getForLocal"); + } else if (varToLocal instanceof BinaryOperatorNode binNode && binNode.operator.equals("{") + && binNode.left instanceof OperatorNode sigNode && sigNode.operator.equals("@")) { + Dereference.handleHashElementOperator(emitterVisitor.with(lvalueContext), binNode, "getForLocal"); } else if (varToLocal instanceof BinaryOperatorNode binNode && binNode.operator.equals("->") && binNode.right instanceof HashLiteralNode) { // For arrow hash dereference (local $ref->{key}), use getForLocal via arrow deref path. diff --git a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java index 7c50855fd..25db177c3 100644 --- a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java +++ b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java @@ -378,7 +378,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl if (GlobalVariable.existsGlobalCodeRef(normalizedClassMethodName)) { RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(normalizedClassMethodName); - if (!codeRef.getDefinedBoolean()) { + if (!RuntimeCode.isCodeDefined(codeRef)) { continue; } cacheMethod(cacheKey, codeRef); @@ -400,7 +400,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl String autoloadName = (effectiveClassName.endsWith("::") ? effectiveClassName : effectiveClassName + "::") + "AUTOLOAD"; if (GlobalVariable.existsGlobalCodeRef(autoloadName)) { RuntimeScalar autoload = GlobalVariable.getGlobalCodeRef(autoloadName); - if (autoload.getDefinedBoolean()) { + if (RuntimeCode.isCodeDefined(autoload)) { // Use the AUTOLOAD sub's CvSTASH (packageName) for $AUTOLOAD, // not the glob's package. Perl sets $AUTOLOAD in the package // where the AUTOLOAD sub was compiled, which matters for closures @@ -429,4 +429,4 @@ public enum MROAlgorithm { C3, DFS } -} \ No newline at end of file +} diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index 329052202..576fcd028 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -755,14 +755,16 @@ public static boolean isGlobalCodeRefDefined(String key) { return false; } + private static boolean codeSlotExists(RuntimeScalar var) { + return var != null + && var.type == RuntimeScalarType.CODE + && var.value instanceof RuntimeCode runtimeCode + && (runtimeCode.defined() || runtimeCode.isDeclared); + } + public static RuntimeScalar existsGlobalCodeRefAsScalar(String key) { RuntimeScalar var = globalCodeRefs.get(key); - if (var != null && var.type == RuntimeScalarType.CODE && var.value instanceof RuntimeCode runtimeCode) { - // Use the RuntimeCode.defined() method to check if the subroutine actually exists - // This checks methodHandle, constantValue, and compilerSupplier - return runtimeCode.defined() ? scalarTrue : scalarFalse; - } - return scalarFalse; + return codeSlotExists(var) ? scalarTrue : scalarFalse; } public static RuntimeScalar existsGlobalCodeRefAsScalar(RuntimeScalar key) { @@ -772,8 +774,7 @@ public static RuntimeScalar existsGlobalCodeRefAsScalar(RuntimeScalar key) { } // Handle RuntimeCode objects by extracting the subroutine name if (key.type == RuntimeScalarType.CODE && key.value instanceof RuntimeCode runtimeCode) { - // Use the RuntimeCode.defined() method to check if the subroutine actually exists - return runtimeCode.defined() ? scalarTrue : scalarFalse; + return (runtimeCode.defined() || runtimeCode.isDeclared) ? scalarTrue : scalarFalse; } return existsGlobalCodeRefAsScalar(key.toString()); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/Overload.java b/src/main/java/org/perlonjava/runtime/runtimetypes/Overload.java index 8a106082e..12c02620a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/Overload.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/Overload.java @@ -73,7 +73,7 @@ public static RuntimeScalar stringify(RuntimeScalar runtimeScalar) { try { // Prepare overload context and check if object is eligible for overloading int blessId = RuntimeScalarType.blessedId(runtimeScalar); - if (blessId < 0) { + if (blessId != 0) { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try primary overload method @@ -116,13 +116,13 @@ public static RuntimeScalar numify(RuntimeScalar runtimeScalar) { System.err.println(" Input scalar: " + runtimeScalar); System.err.println(" Input type: " + runtimeScalar.type); System.err.println(" blessId: " + blessId); - if (blessId < 0) { + if (blessId != 0) { System.err.println(" Blessed as: " + NameNormalizer.getBlessStr(blessId)); } System.err.flush(); } - if (blessId < 0) { + if (blessId != 0) { OverloadContext ctx = OverloadContext.prepare(blessId); if (TRACE_OVERLOAD) { @@ -182,7 +182,7 @@ public static RuntimeScalar numify(RuntimeScalar runtimeScalar) { public static RuntimeScalar boolify(RuntimeScalar runtimeScalar) { // Prepare overload context and check if object is eligible for overloading int blessId = RuntimeScalarType.blessedId(runtimeScalar); - if (blessId < 0) { + if (blessId != 0) { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try primary overload method @@ -211,7 +211,7 @@ public static RuntimeScalar boolify(RuntimeScalar runtimeScalar) { public static RuntimeScalar bool_not(RuntimeScalar runtimeScalar) { // Prepare overload context and check if object is eligible for overloading int blessId = RuntimeScalarType.blessedId(runtimeScalar); - if (blessId < 0) { + if (blessId != 0) { OverloadContext ctx = OverloadContext.prepare(blessId); if (ctx != null) { // Try primary overload method diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java b/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java index d0b47296d..c55af876e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OverloadContext.java @@ -163,10 +163,7 @@ public boolean allowsFallbackAutogen() { * @return OverloadContext instance if overloading is enabled, null otherwise */ public static OverloadContext prepare(int blessId) { - // Fast path: positive blessIds are non-overloaded classes (set at bless time) - // Negative blessIds indicate classes with overloads - // This saves ~10-20ns HashMap lookup per hash access - if (blessId > 0) { + if (blessId == 0) { return null; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 63605a154..20e82bcb6 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -358,7 +358,10 @@ public static void clearInlineMethodCache() { public String subName; // Source package for imported forward declarations (used for AUTOLOAD resolution) public String sourcePackage = null; - // Flag to indicate this is a symbolic reference created by \&{string} that should always be "defined" + // Historical marker for symbolic references created by \&{string}. A CODE + // scalar is defined as a scalar value even when its underlying subroutine is + // only declared; RuntimeCode.defined() still reports whether the subroutine + // itself has an implementation. public boolean isSymbolicReference = false; // Flag to indicate this is a built-in operator public boolean isBuiltin = false; @@ -649,6 +652,44 @@ private static String autoloadVarFor(RuntimeScalar autoloadCoderef, String looku return lookupPackage + "::AUTOLOAD"; } + public static boolean isCodeDefined(RuntimeScalar codeRef) { + return codeRef != null + && codeRef.type == RuntimeScalarType.CODE + && codeRef.value instanceof RuntimeCode code + && code.defined(); + } + + private static RuntimeScalar findImportedStubAutoload(RuntimeCode code, String fullSubName) { + if (code.packageName == null || code.packageName.isEmpty() + || fullSubName == null || fullSubName.isEmpty()) { + return null; + } + int sep = fullSubName.lastIndexOf("::"); + if (sep < 0) { + return null; + } + String lookupPackage = fullSubName.substring(0, sep); + if (code.packageName.equals(lookupPackage)) { + return null; + } + + String shortName = fullSubName.substring(sep + 2); + String sourceSubName = (code.subName != null + && !code.subName.isEmpty() + && !"__ANON__".equals(code.subName)) + ? code.subName + : shortName; + String sourceAutoloadString = code.packageName + "::AUTOLOAD"; + RuntimeScalar sourceAutoload = GlobalVariable.getGlobalCodeRef(sourceAutoloadString); + if (!isCodeDefined(sourceAutoload)) { + return null; + } + + getGlobalVariable(autoloadVarFor(sourceAutoload, code.packageName)) + .set(code.packageName + "::" + sourceSubName); + return sourceAutoload; + } + /** * Check if AUTOLOAD exists for a given RuntimeCode's package. * Checks source package first (for imported subs), then current package. @@ -664,14 +705,14 @@ public static boolean hasAutoload(RuntimeCode code) { if (code.sourcePackage != null && !code.sourcePackage.equals(code.packageName)) { String sourceAutoloadString = code.sourcePackage + "::AUTOLOAD"; RuntimeScalar sourceAutoload = GlobalVariable.getGlobalCodeRef(sourceAutoloadString); - if (sourceAutoload.getDefinedBoolean()) { + if (isCodeDefined(sourceAutoload)) { return true; } } // Then check current package AUTOLOAD String autoloadString = code.packageName + "::AUTOLOAD"; RuntimeScalar autoload = GlobalVariable.getGlobalCodeRef(autoloadString); - return autoload.getDefinedBoolean(); + return isCodeDefined(autoload); } @@ -2205,7 +2246,7 @@ public static RuntimeList call(RuntimeScalar runtimeScalar, String shortMethod = methodName.substring(sep + 2); method = InheritanceResolver.findMethodInHierarchy( shortMethod, targetPackage, methodName, 0); - if (method == null || !method.getDefinedBoolean()) { + if (method == null || !isCodeDefined(method)) { throw new PerlCompilerException("Undefined subroutine &" + methodName + " called"); } } @@ -2680,7 +2721,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int if (code.sourcePackage != null && !code.sourcePackage.equals(code.packageName)) { String sourceAutoloadString = code.sourcePackage + "::AUTOLOAD"; RuntimeScalar sourceAutoload = GlobalVariable.getGlobalCodeRef(sourceAutoloadString); - if (sourceAutoload.getDefinedBoolean()) { + if (isCodeDefined(sourceAutoload)) { // Set $AUTOLOAD name to the original package function name String sourceSubroutineName = code.sourcePackage + "::" + code.subName; getGlobalVariable(sourceAutoloadString).set(sourceSubroutineName); @@ -2693,7 +2734,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int // Then check if AUTOLOAD exists in the current package String autoloadString = code.packageName + "::AUTOLOAD"; RuntimeScalar autoload = GlobalVariable.getGlobalCodeRef(autoloadString); - if (autoload.getDefinedBoolean()) { + if (isCodeDefined(autoload)) { // Set $AUTOLOAD — in the package where the AUTOLOAD sub // was compiled, not in the package we looked it up from // (see autoloadVarFor() for details). @@ -3067,12 +3108,17 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa } if (!fullSubName.isEmpty()) { + RuntimeScalar importedStubAutoload = findImportedStubAutoload(code, fullSubName); + if (importedStubAutoload != null) { + return apply(importedStubAutoload, a, callContext); + } + // If this is an imported forward declaration, check AUTOLOAD in the source package FIRST // This matches Perl semantics where imported subs resolve via the exporting package's AUTOLOAD if (code.sourcePackage != null && !code.sourcePackage.isEmpty()) { String sourceAutoloadString = code.sourcePackage + "::AUTOLOAD"; RuntimeScalar sourceAutoload = GlobalVariable.getGlobalCodeRef(sourceAutoloadString); - if (sourceAutoload.getDefinedBoolean()) { + if (isCodeDefined(sourceAutoload)) { // Set $AUTOLOAD name to the original package function name String sourceSubroutineName = code.sourcePackage + "::" + code.subName; getGlobalVariable(sourceAutoloadString).set(sourceSubroutineName); @@ -3084,7 +3130,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa // Then check if AUTOLOAD exists in the current package String autoloadString = fullSubName.substring(0, fullSubName.lastIndexOf("::") + 2) + "AUTOLOAD"; RuntimeScalar autoload = GlobalVariable.getGlobalCodeRef(autoloadString); - if (autoload.getDefinedBoolean()) { + if (isCodeDefined(autoload)) { // Set $AUTOLOAD in the AUTOLOAD sub's compile-time package // (see autoloadVarFor() for the reasoning). String lookupPkg = fullSubName.substring(0, fullSubName.lastIndexOf("::")); @@ -3293,11 +3339,16 @@ private static RuntimeList applyImpl(RuntimeScalar runtimeScalar, String subrout : subroutineName; if (!fullSubName.isEmpty() && fullSubName.contains("::")) { + RuntimeScalar importedStubAutoload = findImportedStubAutoload(code, fullSubName); + if (importedStubAutoload != null) { + return apply(importedStubAutoload, a, callContext); + } + // If this is an imported forward declaration, check AUTOLOAD in the source package FIRST if (code.sourcePackage != null && !code.sourcePackage.isEmpty()) { String sourceAutoloadString = code.sourcePackage + "::AUTOLOAD"; RuntimeScalar sourceAutoload = GlobalVariable.getGlobalCodeRef(sourceAutoloadString); - if (sourceAutoload.getDefinedBoolean()) { + if (isCodeDefined(sourceAutoload)) { String sourceSubroutineName = code.sourcePackage + "::" + code.subName; getGlobalVariable(sourceAutoloadString).set(sourceSubroutineName); return apply(sourceAutoload, a, callContext); @@ -3307,7 +3358,7 @@ private static RuntimeList applyImpl(RuntimeScalar runtimeScalar, String subrout // Check if AUTOLOAD exists in the current package String autoloadString = fullSubName.substring(0, fullSubName.lastIndexOf("::") + 2) + "AUTOLOAD"; RuntimeScalar autoload = GlobalVariable.getGlobalCodeRef(autoloadString); - if (autoload.getDefinedBoolean()) { + if (isCodeDefined(autoload)) { // Set $AUTOLOAD in the AUTOLOAD sub's compile-time package // (see autoloadVarFor() for the reasoning). String lookupPkg = fullSubName.substring(0, fullSubName.lastIndexOf("::")); @@ -3457,18 +3508,16 @@ public static RuntimeScalar createCodeReference(RuntimeScalar runtimeScalar, Str } } - // Check if this is a constant subroutine - if (codeRef.type == RuntimeScalarType.CODE && codeRef.value instanceof RuntimeCode runtimeCode) { - // Mark this as a symbolic reference created by \&{string} pattern - // This ensures defined(\&{nonexistent}) returns true to match standard Perl behavior - runtimeCode.isSymbolicReference = true; - - // Note: We used to return a reference to the constant value here, but that - // breaks Exporter which does `*{$pkg::$sym} = \&{$src::$sym}`. The glob - // assignment expects a CODE reference, not a scalar reference. - // The constant value optimization is handled separately when calling the sub. + if (codeRef.type == RuntimeScalarType.CODE && codeRef.value instanceof RuntimeCode runtimeCode + && !runtimeCode.defined()) { + runtimeCode.isDeclared = true; } + // Note: We used to return a reference to the constant value here, but that + // breaks Exporter which does `*{$pkg::$sym} = \&{$src::$sym}`. The glob + // assignment expects a CODE reference, not a scalar reference. + // The constant value optimization is handled separately when calling the sub. + // Return a snapshot of the current code reference, not the global entry itself. // This ensures that saved code references (\&sub) point to the current RuntimeCode // and won't be affected if the subroutine is later redefined. @@ -3600,17 +3649,14 @@ public static RuntimeScalar materializeBlockResult(RuntimeScalar result) { } public boolean defined() { - // Symbolic references created by \&{string} are always considered "defined" to match standard Perl - if (this.isSymbolicReference) { - return true; - } // Built-in operators are always considered "defined" if (this.isBuiltin) { return true; } // Note: isDeclared is NOT checked here. In Perl 5, defined(&foo) returns // false for forward declarations (sub foo;). The isDeclared flag is used - // only by RuntimeGlob.getGlobSlot("CODE") for *foo{CODE} visibility. + // by RuntimeGlob.getGlobSlot("CODE") and exists(&foo), both of which see + // a declared-but-undefined CODE slot. return this.constantValue != null || this.compilerSupplier != null || this.subroutine != null || this.methodHandle != null; } @@ -3656,7 +3702,7 @@ public RuntimeList apply(RuntimeArray a, int callContext) { if (this.sourcePackage != null && !this.sourcePackage.isEmpty()) { String sourceAutoloadString = this.sourcePackage + "::AUTOLOAD"; RuntimeScalar sourceAutoload = GlobalVariable.getGlobalCodeRef(sourceAutoloadString); - if (sourceAutoload.getDefinedBoolean()) { + if (isCodeDefined(sourceAutoload)) { String sourceSubroutineName = this.sourcePackage + "::" + this.subName; getGlobalVariable(sourceAutoloadString).set(sourceSubroutineName); return apply(sourceAutoload, a, callContext); @@ -3664,7 +3710,7 @@ public RuntimeList apply(RuntimeArray a, int callContext) { } String autoloadString = fullSubName.substring(0, fullSubName.lastIndexOf("::") + 2) + "AUTOLOAD"; RuntimeScalar autoload = GlobalVariable.getGlobalCodeRef(autoloadString); - if (autoload.getDefinedBoolean()) { + if (isCodeDefined(autoload)) { // Set $AUTOLOAD in the AUTOLOAD sub's compile-time package // (see autoloadVarFor() for the reasoning). String lookupPkg = fullSubName.substring(0, fullSubName.lastIndexOf("::")); @@ -3765,7 +3811,7 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) if (this.sourcePackage != null && !this.sourcePackage.isEmpty()) { String sourceAutoloadString = this.sourcePackage + "::AUTOLOAD"; RuntimeScalar sourceAutoload = GlobalVariable.getGlobalCodeRef(sourceAutoloadString); - if (sourceAutoload.getDefinedBoolean()) { + if (isCodeDefined(sourceAutoload)) { String sourceSubroutineName = this.sourcePackage + "::" + this.subName; getGlobalVariable(sourceAutoloadString).set(sourceSubroutineName); return apply(sourceAutoload, a, callContext); @@ -3773,7 +3819,7 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) } String autoloadString = fullSubName.substring(0, fullSubName.lastIndexOf("::") + 2) + "AUTOLOAD"; RuntimeScalar autoload = GlobalVariable.getGlobalCodeRef(autoloadString); - if (autoload.getDefinedBoolean()) { + if (isCodeDefined(autoload)) { // Set $AUTOLOAD in the AUTOLOAD sub's compile-time package // (see autoloadVarFor() for the reasoning). String lookupPkg = fullSubName.substring(0, fullSubName.lastIndexOf("::")); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java index 0ad41605d..88619d1b7 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java @@ -770,6 +770,29 @@ public RuntimeList getSlice(RuntimeList value) { return result; } + /** + * Slices the hash for {@code local @hash{...}}. + * + *

Unlike {@link #getSlice(RuntimeList)}, this returns key-aware proxy + * entries so dynamic restore can reinsert deleted keys into the parent hash. + * + * @param value The RuntimeList containing the keys to slice. + * @return A RuntimeList containing localized proxy entries for the keys. + */ + public RuntimeList getForLocalSlice(RuntimeList value) { + + if (this.type == AUTOVIVIFY_HASH) { + AutovivificationHash.vivify(this); + } + + RuntimeList result = new RuntimeList(); + List outElements = result.elements; + for (RuntimeScalar runtimeScalar : value) { + outElements.add(this.getForLocal(runtimeScalar)); + } + return result; + } + /** * Key-value slice of the hash: %x{"a", "b"} * diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 1ce3d09ca..6e662aa20 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -2629,8 +2629,9 @@ public boolean getDefinedBoolean() { case DUALVAR -> ((DualVar) this.value).stringValue().getDefinedBoolean(); // 10 case FORMAT -> ((RuntimeFormat) value).getDefinedBoolean(); // 11 case READONLY_SCALAR -> ((RuntimeScalar) this.value).getDefinedBoolean(); // 12 + case CODE -> true; // Reference types (with REFERENCE_BIT) fall through to default - default -> type != CODE || ((RuntimeCode) value).defined(); + default -> true; }; } diff --git a/src/test/resources/unit/role_tiny_regressions.t b/src/test/resources/unit/role_tiny_regressions.t new file mode 100644 index 000000000..31e937b91 --- /dev/null +++ b/src/test/resources/unit/role_tiny_regressions.t @@ -0,0 +1,94 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; + +{ + package RoleTinyForwardDeclaration; + sub required_method; + + ::ok(exists &required_method, 'forward-declared sub has a CODE slot'); + ::ok(!defined &required_method, 'forward-declared sub is not defined'); + ::ok(RoleTinyForwardDeclaration->can('required_method'), 'forward-declared sub is visible to can'); +} + +my $stub_ref = \&RoleTinyForwardDeclaration::required_method; +ok(defined $stub_ref, 'CODE reference to a forward declaration is a defined scalar'); +ok(!defined &RoleTinyForwardDeclaration::required_method, 'taking a CODE reference does not define the stub'); + +{ + package RoleTinyCopiedStub; +} + +*RoleTinyCopiedStub::required_method = $stub_ref; +ok(exists &RoleTinyCopiedStub::required_method, 'assigning a stub CODE reference creates a CODE slot'); +ok(!defined &RoleTinyCopiedStub::required_method, 'assigning a stub CODE reference preserves undefined stub state'); + +{ + package RoleTinyDynamicLookup; + sub imported { 1 } +} + +{ + package RoleTinyDynamicLookup; + no strict 'refs'; + + my @defined = grep defined &$_, keys %RoleTinyDynamicLookup::; + my @exists = grep exists &$_, keys %RoleTinyDynamicLookup::; + + ::ok(grep($_ eq 'imported', @defined), 'defined &$_ resolves in the current package'); + ::ok(grep($_ eq 'imported', @exists), 'exists &$_ resolves in the current package'); +} + +{ + package RoleTinyLateOverload; + + sub new { + return bless {}, shift; + } + + sub as_string { + return 'welp'; + } + + sub as_num { + return 219; + } +} + +my $late = RoleTinyLateOverload->new; + +{ + package RoleTinyLateOverload; + require overload; + + overload->import( + '""' => \&as_string, + '0+' => 'as_num', + bool => sub { 0 }, + fallback => 1, + ); +} + +is("$late", 'welp', 'late-installed stringify overload applies to existing objects'); +is(sprintf('%d', $late), '219', 'late-installed numify overload applies to existing objects'); +ok(!$late, 'late-installed bool overload applies to existing objects'); + +{ + my %methods = ( + method => 'from role', + keep => 'still here', + ); + + { + local @methods{qw(method)}; + delete @methods{qw(method)}; + ok(!exists $methods{method}, 'localized hash slice key can be deleted inside scope'); + } + + is($methods{method}, 'from role', 'localized hash slice restores a deleted key'); + is(join(',', sort keys %methods), 'keep,method', 'localized hash slice restore keeps hash keys intact'); +} + +done_testing; From ad76ee84bebf6893ba295af5f85ae13d22b5ab1f Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Fri, 8 May 2026 19:28:43 +0200 Subject: [PATCH 2/2] fix: address Role::Tiny PR regressions Restore behavior regressed by the Role::Tiny fixes: - avoid autovivifying empty CODE slots when probing user-defined Unicode properties - restore localized list elements in reverse save order - keep tied hash slices on the tied slice path for local() Add focused unit tests for Unicode property lookup and localized hash slices. Generated with [OpenAI Codex](https://openai.com/codex) Co-Authored-By: OpenAI Codex --- .../runtime/regex/UnicodeResolver.java | 8 ++-- .../runtime/runtimetypes/RuntimeHash.java | 4 ++ .../runtime/runtimetypes/RuntimeList.java | 8 ++-- .../unit/local_hash_slice_regressions.t | 44 +++++++++++++++++++ .../unicode_property_user_sub_regression.t | 14 ++++++ 5 files changed, 70 insertions(+), 8 deletions(-) create mode 100644 src/test/resources/unit/local_hash_slice_regressions.t create mode 100644 src/test/resources/unit/unicode_property_user_sub_regression.t diff --git a/src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java b/src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java index 164ac3473..87d6ac7b2 100644 --- a/src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java +++ b/src/main/java/org/perlonjava/runtime/regex/UnicodeResolver.java @@ -454,11 +454,11 @@ private static String tryUserDefinedProperty(String property, Set recurs return userPropertyCache.get(subName); } - // Look up the subroutine - RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(subName); - if (codeRef == null || !codeRef.getDefinedBoolean()) { + // Look up the subroutine without autovivifying an empty CODE slot. + if (!GlobalVariable.isGlobalCodeRefDefined(subName)) { return null; } + RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(subName); try { // Call the subroutine with an empty argument list @@ -835,4 +835,4 @@ private static boolean isBlockProperty(String property) { } return false; } -} \ No newline at end of file +} diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java index 88619d1b7..d32a92ad3 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java @@ -785,6 +785,10 @@ public RuntimeList getForLocalSlice(RuntimeList value) { AutovivificationHash.vivify(this); } + if (this.type == TIED_HASH) { + return getSlice(value); + } + RuntimeList result = new RuntimeList(); List outElements = result.elements; for (RuntimeScalar runtimeScalar : value) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java index 6b91b5ab9..b3b034027 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeList.java @@ -789,10 +789,10 @@ public void dynamicSaveState() { */ @Override public void dynamicRestoreState() { - // Note: this method is probably not needed, - // because the elements are handled by their respective classes. - for (RuntimeBase elem : elements) { - elem.dynamicRestoreState(); + // Save pushes element state left-to-right onto per-type stacks; restore + // must unwind right-to-left so same-type elements get their own state. + for (int i = elements.size() - 1; i >= 0; i--) { + elements.get(i).dynamicRestoreState(); } } diff --git a/src/test/resources/unit/local_hash_slice_regressions.t b/src/test/resources/unit/local_hash_slice_regressions.t new file mode 100644 index 000000000..0fd1b5a7f --- /dev/null +++ b/src/test/resources/unit/local_hash_slice_regressions.t @@ -0,0 +1,44 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; + +{ + my %methods = ( + method => 'from role', + keep => 'still here', + ); + + { + local @methods{qw(method)}; + delete @methods{qw(method)}; + ok(!exists $methods{method}, 'localized plain hash slice key can be deleted inside scope'); + } + + is($methods{method}, 'from role', 'localized plain hash slice restores a deleted key'); + is(join(',', sort keys %methods), 'keep,method', 'localized plain hash slice restore keeps hash keys intact'); +} + +{ + my $key_a = 'PERLONJAVA_LOCAL_HASH_SLICE_A'; + my $key_b = 'PERLONJAVA_LOCAL_HASH_SLICE_B'; + my $key_c = 'PERLONJAVA_LOCAL_HASH_SLICE_C'; + + local $ENV{$key_a} = 1; + local $ENV{$key_b} = 2; + delete $ENV{$key_c}; + + { + local @ENV{$key_a, $key_c}; + ok(exists $ENV{$key_a}, 'localized tied hash slice keeps existing key visible inside scope'); + ok(exists $ENV{$key_b}, 'localized tied hash slice leaves unrelated key visible inside scope'); + ok(exists $ENV{$key_c}, 'localized tied hash slice creates missing key inside scope'); + } + + ok(exists $ENV{$key_a}, 'localized tied hash slice restores existing key after scope'); + ok(exists $ENV{$key_b}, 'localized tied hash slice keeps unrelated key after scope'); + ok(!exists $ENV{$key_c}, 'localized tied hash slice removes originally missing key after scope'); +} + +done_testing; diff --git a/src/test/resources/unit/unicode_property_user_sub_regression.t b/src/test/resources/unit/unicode_property_user_sub_regression.t new file mode 100644 index 000000000..a126718ea --- /dev/null +++ b/src/test/resources/unit/unicode_property_user_sub_regression.t @@ -0,0 +1,14 @@ +#!/usr/bin/env perl + +use strict; +use warnings; +use Test::More; + +my $input = "\x{3c3}foo.bar"; +my $output; + +($output = $input) =~ s/(\p{IsWord}+)/uc($1)/ge; +is($output, "\x{3a3}FOO.BAR", 'built-in IsWord property is not treated as an undefined user sub'); +ok(!exists &main::IsWord, 'probing built-in IsWord does not create a user property CODE slot'); + +done_testing;