diff --git a/dev/modules/cpan_compiler_tooling_suite_2.md b/dev/modules/cpan_compiler_tooling_suite_2.md new file mode 100644 index 0000000000..e88da2011c --- /dev/null +++ b/dev/modules/cpan_compiler_tooling_suite_2.md @@ -0,0 +1,90 @@ +# CPAN compiler and tooling compatibility suite II + +## Goal + +Make `jcpan -t` work for Data::Collector, Pod::Query, Char::Windows1258, +Map::Metro::Plugin::Map::Oslo, DBIx::Dictionary, +Date::Holidays::Abstract, Music::Note::Role::Operators, and their +dependencies. Fix reusable compiler and CPAN tooling defects first. A target +that fails under the local system Perl may be left unsupported with the +failure recorded. + +## Baseline (2026-08-14) + +| Target | First actionable result | +|---|---| +| Data::Collector | Passes: 3 files / 12 tests. | +| Pod::Query | Empty `qr//` values interpolated from hashes incorrectly reuse the previous successful match. | +| Char::Windows1258 | All 210 files abort because upstream deliberately rejects an executable whose `$^X` contains `jperl`; the same suite passes 5,703 tests on system Perl. | +| Map::Metro::Plugin::Map::Oslo | Dependency failures exposed unsupported `PadWalker::var_name`, incomplete low-level `Unicode::Normalize`, missing reverse charnames lookup, and weak-reference loss for `map` temporaries. | +| DBIx::Dictionary | DBI `execute` returned `-1` for a successful `SELECT`, which failed DBI's documented truth test. | +| Date::Holidays::Abstract | Bare `SUPER::can` resolved relative to `UNIVERSAL` instead of the caller package. | +| Music::Note::Role::Operators | Required native `Math::Factor::XS`; after replacing it in Java, a dependency exposed missing `POSIX::log2`. | + +Full command output is captured under `/tmp/jcpan-*.log`; every `jcpan`, +`jperl`, and `prove` run is wrapped in `timeout`. + +## Progress Tracking + +### Current Status: Implementation and requested-target validation complete + +### Completed Phases + +- [x] Repository pre-flight and feature branch (2026-08-14) + - Confirmed the tree was clean. + - Created `fix/jcpan-compiler-tooling-batch`. +- [x] Initial system-Perl classification (2026-08-14) + - Char::Windows1258 passes 210 files / 5,703 tests. + - No requested target was excluded as a system-Perl failure. +- [x] Compiler and runtime compatibility fixes (2026-08-14) + - Preserved the construction origin of empty `qr//` values through both + bytecode backends so they no longer acquire the previous match pattern. + - Kept `map`/`grep` aliases alive while their temporary values are active, + allowing weak references to those aliases to behave like Perl. + - Resolved bare `SUPER::can` relative to the current caller package. + - Implemented caller-aware `PadWalker::var_name` for live and captured + lexicals, including aliases inside `map` and `grep`. + - Added low-level Unicode normalization decomposition, canonical reordering, + and composition using the ICU library already shipped by PerlOnJava. + - Added ICU-backed reverse Unicode character-name lookup. + - Made successful DBI `SELECT` execution return the true-but-zero `0E0` + value required by DBI semantics. + - Added the standard `POSIX::log2` helper and exports. + - Files: bytecode compiler/interpreter and regex emitter/runtime, list and + scalar runtimes, `RuntimeCode`, `Universal`, `Internals`, `PadWalker`, + `UnicodeNormalize`, `_charnames`, `Charnames`, `DBI`, and `POSIX`. +- [x] Native dependency and CPAN tooling fixes (2026-08-14) + - Replaced `Math::Factor::XS` with a Java module; its upstream suite passes + 4 files / 69 tests without loading native code. + - Added and bootstrapped a reusable Char::Windows1258 patch that delegates + its source-generation step to system Perl, removes its obsolete `jperl` + rejection, and avoids regex constructs that the generated compatibility + layer cannot safely transform itself. + - Added and bootstrapped a MooseX BetterAnonClassNames patch that removes the + obsolete `autobox::Core` dependency from both source and build metadata. +- [x] Regression coverage and requested-target verification (2026-08-14) + - New regression tests were first validated with system Perl and then with + both PerlOnJava backends. + - Full `make` passes after the implementation changes. + - Passing `jcpan -t` results: Data::Collector (3 files / 12 tests), Pod::Query + (10 / 246), DBIx::Dictionary (7 / 30), Date::Holidays::Abstract (9 / 3), + Map::Metro::Plugin::Map::Oslo (3 / 4), and + Music::Note::Role::Operators (2 / 6). + - Char::Windows1258 passes all 210 files / 5,703 tests, matching its system + Perl baseline. `HARNESS_OPTIONS=j4` was used to reduce the cost of its + many independent test files while retaining the exact `jcpan -t` path. + +### Next Steps + +1. Review and commit the final diff. +2. Open the pull request and monitor CI to completion. + +### Open Questions + +- None. + +## Related References + +- `dev/modules/cpan_compiler_tooling_suite.md` +- `dev/design/patch-and-cpan-prefs-layout.md` +- `.agents/skills/debug-perlonjava/SKILL.md` diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index a908e9e8f5..abadb91f1d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -3278,12 +3278,16 @@ private static int executeTypeOps(int opcode, int[] bytecode, int pc, int flagsReg = bytecode[pc++]; int implicitU = bytecode[pc++]; int warningState = bytecode[pc++]; + int quoteConstruction = bytecode[pc++]; RuntimeScalar flags = registers[flagsReg].scalar(); if (implicitU != 0) { flags = RuntimeRegex.applyUnicodeStringsFeatureToModifiers(flags); } RegexQuoteMeta.setCallSiteWarningState(warningState); registers[rd] = RuntimeRegex.getQuotedRegex(registers[patternReg].scalar(), flags); + if (quoteConstruction != 0) { + registers[rd] = RuntimeRegex.markQuoteConstruction(registers[rd].scalar()); + } return pc; } case Opcodes.QUOTE_REGEX_O -> { @@ -3293,12 +3297,16 @@ private static int executeTypeOps(int opcode, int[] bytecode, int pc, int callsiteId = bytecode[pc++]; int implicitU = bytecode[pc++]; int warningState = bytecode[pc++]; + int quoteConstruction = bytecode[pc++]; RuntimeScalar flags = registers[flagsReg].scalar(); if (implicitU != 0) { flags = RuntimeRegex.applyUnicodeStringsFeatureToModifiers(flags); } RegexQuoteMeta.setCallSiteWarningState(warningState); registers[rd] = RuntimeRegex.getQuotedRegex(registers[patternReg].scalar(), flags, callsiteId); + if (quoteConstruction != 0) { + registers[rd] = RuntimeRegex.markQuoteConstruction(registers[rd].scalar()); + } return pc; } default -> throw new RuntimeException("Unknown type opcode: " + opcode); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 61ef822870..2250a7ca42 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -310,6 +310,7 @@ private static void visitMatchRegex(BytecodeCompiler bc, OperatorNode node) { bc.emitReg(callsiteId); bc.emit(unicodeStringsImplicitUFlag(bc)); bc.emit(regexWarningState(node)); + bc.emit(0); } else { bc.emit(Opcodes.QUOTE_REGEX); bc.emitReg(regexReg); @@ -317,6 +318,7 @@ private static void visitMatchRegex(BytecodeCompiler bc, OperatorNode node) { bc.emitReg(flagsReg); bc.emit(unicodeStringsImplicitUFlag(bc)); bc.emit(regexWarningState(node)); + bc.emit(0); } int stringReg; if (args.elements.size() > 2) { @@ -1094,6 +1096,7 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.emitReg(callsiteId); bytecodeCompiler.emit(unicodeStringsImplicitUFlag(bytecodeCompiler)); bytecodeCompiler.emit(regexWarningState(node)); + bytecodeCompiler.emit(1); } else { bytecodeCompiler.emit(Opcodes.QUOTE_REGEX); bytecodeCompiler.emitReg(rd); @@ -1101,6 +1104,7 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.emitReg(flagsReg); bytecodeCompiler.emit(unicodeStringsImplicitUFlag(bytecodeCompiler)); bytecodeCompiler.emit(regexWarningState(node)); + bytecodeCompiler.emit(1); } bytecodeCompiler.lastResultReg = rd; } diff --git a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java index c1b0e1dbfb..16e58cdf6d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java @@ -1279,9 +1279,11 @@ public static String disassemble(InterpretedCode interpretedCode) { int flagsReg = interpretedCode.bytecode[pc++]; int implicitU = interpretedCode.bytecode[pc++]; int warningState = interpretedCode.bytecode[pc++]; + int quoteConstruction = interpretedCode.bytecode[pc++]; sb.append("QUOTE_REGEX r").append(rd).append(" = qr{r").append(patternReg) .append("}r").append(flagsReg).append(" implicitU=").append(implicitU) - .append(" warningState=").append(warningState).append("\n"); + .append(" warningState=").append(warningState) + .append(" quoteConstruction=").append(quoteConstruction).append("\n"); break; case Opcodes.QUOTE_REGEX_O: rd = interpretedCode.bytecode[pc++]; @@ -1290,10 +1292,12 @@ public static String disassemble(InterpretedCode interpretedCode) { int callsiteId = interpretedCode.bytecode[pc++]; implicitU = interpretedCode.bytecode[pc++]; warningState = interpretedCode.bytecode[pc++]; + quoteConstruction = interpretedCode.bytecode[pc++]; sb.append("QUOTE_REGEX_O r").append(rd).append(" = qr{r").append(patternReg) .append("}r").append(flagsReg).append(" callsite=").append(callsiteId) .append(" implicitU=").append(implicitU) - .append(" warningState=").append(warningState).append("\n"); + .append(" warningState=").append(warningState) + .append(" quoteConstruction=").append(quoteConstruction).append("\n"); break; case Opcodes.ITERATOR_CREATE: rd = interpretedCode.bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index efb487ddb0..21a7a53f11 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -935,7 +935,7 @@ public class Opcodes { /** * Quote regex operator: rd = RuntimeRegex.getQuotedRegex(pattern_reg, flags_reg) - * Format: QUOTE_REGEX rd pattern_reg flags_reg implicit_unicode_strings_u warning_state + * Format: QUOTE_REGEX rd pattern_reg flags_reg implicit_unicode_strings_u warning_state quote_construction */ public static final short QUOTE_REGEX = 159; @@ -1834,7 +1834,7 @@ public class Opcodes { /** * Quote regex with /o modifier support: rd = RuntimeRegex.getQuotedRegex(pattern_reg, flags_reg, callsite_id) - * Format: QUOTE_REGEX_O rd pattern_reg flags_reg callsite_id implicit_unicode_strings_u warning_state + * Format: QUOTE_REGEX_O rd pattern_reg flags_reg callsite_id implicit_unicode_strings_u warning_state quote_construction */ public static final short QUOTE_REGEX_O = 374; diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java b/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java index 599a1e0084..fe422d1268 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java @@ -288,6 +288,9 @@ static void handleQuoteRegex(EmitterVisitor emitterVisitor, OperatorNode node) { emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/regex/RuntimeRegex", "getQuotedRegex", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); + emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/regex/RuntimeRegex", "markQuoteConstruction", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); if (emitterVisitor.ctx.contextType == RuntimeContextType.VOID) { emitterVisitor.ctx.mv.visitInsn(Opcodes.POP); diff --git a/src/main/java/org/perlonjava/runtime/operators/ListOperators.java b/src/main/java/org/perlonjava/runtime/operators/ListOperators.java index 1ab98aa5af..176f78b6f5 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ListOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ListOperators.java @@ -45,6 +45,7 @@ public static RuntimeList map(RuntimeList runtimeList, RuntimeScalar perlMapClos List transformedElements = new ArrayList<>(); RuntimeScalar saveValue = getGlobalVariable("main::_"); + boolean savedTemporaryAlias = GlobalVariable.isTemporaryGlobalAlias("main::_"); // Map results are captured by the caller after the operator returns; // flushing between iterations can destroy blessed return values early. boolean wasFlushing = MortalList.suppressFlush(true); @@ -57,7 +58,7 @@ public static RuntimeList map(RuntimeList runtimeList, RuntimeScalar perlMapClos // Iterate over each element in the current RuntimeArray for (RuntimeScalar element : runtimeList) { // Create $_ argument for the map subroutine - GlobalVariable.aliasGlobalVariable("main::_", element); + GlobalVariable.aliasTemporaryGlobalVariable("main::_", element); // Apply the Perl map subroutine with the outer @_ as arguments RuntimeList result = RuntimeCode.apply(perlMapClosure, mapArgs, RuntimeContextType.LIST); @@ -91,7 +92,7 @@ public static RuntimeList map(RuntimeList runtimeList, RuntimeScalar perlMapClos } } finally { MortalList.suppressFlush(wasFlushing); - GlobalVariable.aliasGlobalVariable("main::_", saveValue); + GlobalVariable.restoreTemporaryGlobalVariable("main::_", saveValue, savedTemporaryAlias); releaseEphemeralCaptures(perlMapClosure); } } @@ -237,6 +238,7 @@ public static RuntimeList grep(RuntimeList runtimeList, RuntimeScalar perlFilter List filteredElements = new ArrayList<>(); RuntimeScalar saveValue = getGlobalVariable("main::_"); + boolean savedTemporaryAlias = GlobalVariable.isTemporaryGlobalAlias("main::_"); try { // Use the outer @_ instead of an empty array @@ -246,7 +248,7 @@ public static RuntimeList grep(RuntimeList runtimeList, RuntimeScalar perlFilter for (RuntimeScalar element : runtimeList) { try { // Create $_ argument for the filter subroutine - GlobalVariable.aliasGlobalVariable("main::_", element); + GlobalVariable.aliasTemporaryGlobalVariable("main::_", element); // Apply the Perl filter subroutine with the outer @_ as arguments RuntimeList result = RuntimeCode.apply(perlFilterClosure, filterArgs, RuntimeContextType.SCALAR); @@ -287,7 +289,7 @@ public static RuntimeList grep(RuntimeList runtimeList, RuntimeScalar perlFilter return filteredList; } } finally { - GlobalVariable.aliasGlobalVariable("main::_", saveValue); + GlobalVariable.restoreTemporaryGlobalVariable("main::_", saveValue, savedTemporaryAlias); releaseEphemeralCaptures(perlFilterClosure); } } @@ -313,6 +315,7 @@ public static RuntimeList grep(RuntimeList runtimeList, RuntimeScalar perlFilter public static RuntimeList all(RuntimeList runtimeList, RuntimeScalar perlFilterClosure, RuntimeArray outerArgs, int ctx) { RuntimeScalar saveValue = getGlobalVariable("main::_"); + boolean savedTemporaryAlias = GlobalVariable.isTemporaryGlobalAlias("main::_"); try { RuntimeArray filterArgs = outerArgs != null ? outerArgs : new RuntimeArray(); @@ -321,7 +324,7 @@ public static RuntimeList all(RuntimeList runtimeList, RuntimeScalar perlFilterC for (RuntimeScalar element : runtimeList) { try { // Create $_ argument for the filter subroutine - GlobalVariable.aliasGlobalVariable("main::_", element); + GlobalVariable.aliasTemporaryGlobalVariable("main::_", element); // Apply the Perl filter subroutine with the argument RuntimeList result = RuntimeCode.apply(perlFilterClosure, filterArgs, RuntimeContextType.SCALAR); @@ -346,7 +349,7 @@ public static RuntimeList all(RuntimeList runtimeList, RuntimeScalar perlFilterC return scalarTrue.getList(); } finally { - GlobalVariable.aliasGlobalVariable("main::_", saveValue); + GlobalVariable.restoreTemporaryGlobalVariable("main::_", saveValue, savedTemporaryAlias); releaseEphemeralCaptures(perlFilterClosure); } } @@ -372,6 +375,7 @@ public static RuntimeList all(RuntimeList runtimeList, RuntimeScalar perlFilterC public static RuntimeList any(RuntimeList runtimeList, RuntimeScalar perlFilterClosure, RuntimeArray outerArgs, int ctx) { RuntimeScalar saveValue = getGlobalVariable("main::_"); + boolean savedTemporaryAlias = GlobalVariable.isTemporaryGlobalAlias("main::_"); try { RuntimeArray filterArgs = outerArgs != null ? outerArgs : new RuntimeArray(); @@ -380,7 +384,7 @@ public static RuntimeList any(RuntimeList runtimeList, RuntimeScalar perlFilterC for (RuntimeScalar element : runtimeList) { try { // Create $_ argument for the filter subroutine - GlobalVariable.aliasGlobalVariable("main::_", element); + GlobalVariable.aliasTemporaryGlobalVariable("main::_", element); // Apply the Perl filter subroutine with the argument RuntimeList result = RuntimeCode.apply(perlFilterClosure, filterArgs, RuntimeContextType.SCALAR); @@ -405,7 +409,7 @@ public static RuntimeList any(RuntimeList runtimeList, RuntimeScalar perlFilterC return scalarFalse.getList(); } finally { - GlobalVariable.aliasGlobalVariable("main::_", saveValue); + GlobalVariable.restoreTemporaryGlobalVariable("main::_", saveValue, savedTemporaryAlias); releaseEphemeralCaptures(perlFilterClosure); } } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Charnames.java b/src/main/java/org/perlonjava/runtime/perlmodule/Charnames.java index 048459e02a..96037102df 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Charnames.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Charnames.java @@ -99,6 +99,7 @@ public static void initialize() { Charnames charnames = new Charnames(); try { charnames.registerMethod("_java_viacode", "javaViacode", "$"); + charnames.registerMethod("_java_vianame", "javaVianame", "$"); } catch (NoSuchMethodException e) { System.err.println("Warning: Missing _charnames method: " + e.getMessage()); } @@ -129,4 +130,11 @@ public static RuntimeList javaViacode(RuntimeArray args, int ctx) { } return new RuntimeList(new RuntimeScalar(name)); } + + /** Return the code point for an official Unicode character name. */ + public static RuntimeList javaVianame(RuntimeArray args, int ctx) { + int codePoint = UCharacter.getCharFromName(args.getFirst().toString()); + if (codePoint < 0) return new RuntimeList(scalarUndef); + return new RuntimeScalar(codePoint).getList(); + } } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/DBI.java b/src/main/java/org/perlonjava/runtime/perlmodule/DBI.java index 37582499ab..2112cd8caf 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/DBI.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/DBI.java @@ -520,9 +520,9 @@ public static RuntimeList execute(RuntimeArray args, int ctx) { // Return value per DBI spec: // - For DML (INSERT/UPDATE/DELETE): number of rows affected, or "0E0" for 0 rows - // - For SELECT: -1 (unknown number of rows) + // - For SELECT: "0E0" (true zero; row count is not known until fetching) if (hasResultSet) { - return new RuntimeScalar(-1).getList(); + return new RuntimeScalar("0E0").getList(); } else { int updateCount = stmt.getUpdateCount(); if (updateCount == 0) { diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java b/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java index 2ce79d4fea..8fbb1eda80 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java @@ -22,6 +22,10 @@ public Internals() { * Static initializer to set up the module. */ public static void initialize() { + // PadWalker and Devel::LexAlias need live pad cells for active frames. + // Enable registration before user code begins so numeric caller-level + // lookups can see lexicals that were instantiated before the query. + RuntimeCode.enableLexicalAliasSupport(); Internals internals = new Internals(); try { internals.registerMethod("SvREADONLY", "svReadonly", "\\[$@%];$"); @@ -73,6 +77,7 @@ public static void initialize() { internals.registerMethod("jperl_set_closed_over", "jperlSetClosedOver", null); internals.registerMethod("jperl_closed_over", "jperlClosedOver", null); internals.registerMethod("jperl_peek_sub", "jperlPeekSub", null); + internals.registerMethod("jperl_var_name", "jperlVarName", null); internals.registerMethod("jperl_caller_cv", "jperlCallerCv", null); } catch (NoSuchMethodException e) { System.err.println("Warning: Missing Internals method: " + e.getMessage()); @@ -204,6 +209,41 @@ public static RuntimeList jperlPeekSub(RuntimeArray args, int ctx) { return result.createReference().getList(); } + /** Return the pad name whose live cell is the supplied reference. */ + public static RuntimeList jperlVarName(RuntimeArray args, int ctx) { + if (args.size() != 2 || !RuntimeScalarType.isReference(args.get(1)) + || !(args.get(1).value instanceof RuntimeBase target)) { + return new RuntimeList(); + } + + RuntimeScalar scope = args.get(0); + RuntimeCode code; + Map active; + if (scope.type == RuntimeScalarType.CODE) { + code = (RuntimeCode) scope.value; + active = RuntimeCode.snapshotActiveLexicals(code); + } else { + int level = scope.getInt(); + code = RuntimeCode.getActiveCodeAtPadWalkerFrame(level); + active = RuntimeCode.snapshotActiveLexicals(code); + } + + String name = findLexicalName(active, target); + if (name == null && code != null) { + name = findLexicalName(code.closedOverVariables, target); + } + return name == null ? new RuntimeList() : new RuntimeScalar(name).getList(); + } + + private static String findLexicalName( + Map lexicals, RuntimeBase target) { + if (lexicals == null) return null; + for (Map.Entry entry : lexicals.entrySet()) { + if (entry.getValue() == target) return entry.getKey(); + } + return null; + } + /** Return the exact CV for a caller frame, for Devel::Caller::caller_cv. */ public static RuntimeList jperlCallerCv(RuntimeArray args, int ctx) { int level = args.isEmpty() ? 0 : args.get(0).getInt(); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/MathFactorXS.java b/src/main/java/org/perlonjava/runtime/perlmodule/MathFactorXS.java new file mode 100644 index 0000000000..77deb5f95b --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/perlmodule/MathFactorXS.java @@ -0,0 +1,129 @@ +package org.perlonjava.runtime.perlmodule; + +import org.perlonjava.runtime.runtimetypes.PerlCompilerException; +import org.perlonjava.runtime.runtimetypes.RuntimeArray; +import org.perlonjava.runtime.runtimetypes.RuntimeHash; +import org.perlonjava.runtime.runtimetypes.RuntimeList; +import org.perlonjava.runtime.runtimetypes.RuntimeScalar; +import org.perlonjava.runtime.runtimetypes.RuntimeScalarType; + +import java.util.ArrayList; +import java.util.List; + +/** Pure-Java implementation of the Math::Factor::XS native API. */ +public class MathFactorXS extends PerlModuleBase { + private static final String CLASS_NAME = "Math::Factor::XS"; + + public MathFactorXS() { + super(CLASS_NAME, false); + } + + public static void initialize() { + MathFactorXS module = new MathFactorXS(); + try { + module.registerMethod("factors", "$"); + module.registerMethod("xs_matches", "$\\@"); + module.registerMethod("prime_factors", null); + module.registerMethod("count_prime_factors", null); + } catch (NoSuchMethodException e) { + throw new IllegalStateException("Unable to initialize " + CLASS_NAME, e); + } + } + + public static RuntimeList factors(RuntimeArray args, int ctx) { + long number = unsignedLongArgument(args.get(0), "factors"); + RuntimeList result = new RuntimeList(); + List upper = new ArrayList<>(); + for (long divisor = 2; divisor <= number / divisor; divisor++) { + if (number % divisor == 0) { + result.add(new RuntimeScalar(divisor)); + long quotient = number / divisor; + if (quotient > divisor) upper.add(quotient); + } + } + for (int i = upper.size() - 1; i >= 0; i--) { + result.add(new RuntimeScalar(upper.get(i))); + } + return result; + } + + public static RuntimeList xs_matches(RuntimeArray args, int ctx) { + long number = unsignedLongArgument(args.get(0), "matches"); + RuntimeArray factors = args.get(1).arrayDeref(); + boolean skipMultiples = false; + if (args.size() > 2 && args.get(2).type == RuntimeScalarType.HASHREFERENCE) { + RuntimeHash options = args.get(2).hashDeref(); + skipMultiples = options.containsKey("skip_multiples") + && options.get("skip_multiples").getBoolean(); + } + + RuntimeList result = new RuntimeList(); + List previousBases = new ArrayList<>(); + for (RuntimeScalar baseScalar : factors.elements) { + long base = baseScalar.getLong(); + for (RuntimeScalar comparisonScalar : factors.elements) { + long comparison = comparisonScalar.getLong(); + if (comparison < base || base == 0 + || number % base != 0 || number / base != comparison) { + continue; + } + boolean skip = false; + if (skipMultiples) { + for (long previousBase : previousBases) { + if (previousBase != 0 && base % previousBase == 0) { + skip = true; + break; + } + } + } + if (!skip) { + RuntimeArray pair = new RuntimeArray(); + pair.push(new RuntimeScalar(base)); + pair.push(new RuntimeScalar(comparison)); + result.add(pair.createAnonymousReference()); + if (skipMultiples) previousBases.add(base); + } + } + } + return result; + } + + public static RuntimeList prime_factors(RuntimeArray args, int ctx) { + long number = unsignedLongArgument(args.get(0), "prime_factors"); + return primeFactors(number); + } + + public static RuntimeList count_prime_factors(RuntimeArray args, int ctx) { + long number = unsignedLongArgument(args.get(0), "prime_factors"); + return new RuntimeScalar(primeFactors(number).size()).getList(); + } + + private static RuntimeList primeFactors(long number) { + RuntimeList result = new RuntimeList(); + while (number > 0 && (number & 1) == 0) { + result.add(new RuntimeScalar(2)); + number >>= 1; + } + while (number > 0 && number % 3 == 0) { + result.add(new RuntimeScalar(3)); + number /= 3; + } + long increment = 2; + for (long divisor = 5; divisor <= number / divisor; divisor += increment, increment = 6 - increment) { + while (number % divisor == 0) { + result.add(new RuntimeScalar(divisor)); + number /= divisor; + } + } + if (number > 1) result.add(new RuntimeScalar(number)); + return result; + } + + private static long unsignedLongArgument(RuntimeScalar argument, String function) { + double numeric = argument.getDouble(); + if (!Double.isFinite(numeric) || numeric < 0 || numeric > Long.MAX_VALUE) { + throw new PerlCompilerException("Cannot " + function + "() on " + numeric); + } + return argument.getLong(); + } +} diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/UnicodeNormalize.java b/src/main/java/org/perlonjava/runtime/perlmodule/UnicodeNormalize.java index 9ba0f1a5d5..b62f33c123 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/UnicodeNormalize.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/UnicodeNormalize.java @@ -7,6 +7,8 @@ import java.text.Normalizer; import java.text.Normalizer.Form; +import java.util.ArrayList; +import java.util.List; /** * Utility class for Unicode::Normalize operations in Perl. @@ -86,6 +88,9 @@ public static void initialize() { unicodeNormalize.registerMethod("NFC", "$"); unicodeNormalize.registerMethod("NFKD", "$"); unicodeNormalize.registerMethod("NFKC", "$"); + unicodeNormalize.registerMethod("decompose", "$;$"); + unicodeNormalize.registerMethod("reorder", "$"); + unicodeNormalize.registerMethod("compose", "$"); unicodeNormalize.registerMethod("getCombinClass", "$"); } catch (NoSuchMethodException e) { System.err.println("Warning: Missing Unicode::Normalize method: " + e.getMessage()); @@ -140,6 +145,51 @@ public static RuntimeList NFKC(RuntimeArray args, int ctx) { )); } + /** Canonically decompose, optionally applying compatibility mappings. */ + public static RuntimeList decompose(RuntimeArray args, int ctx) { + String input = args.get(0).toString(); + boolean compatibility = args.size() > 1 && args.get(1).getBoolean(); + return new RuntimeScalar(Normalizer.normalize( + input, compatibility ? Form.NFKD : Form.NFD)).getList(); + } + + /** Reorder combining marks by canonical combining class without decomposing. */ + public static RuntimeList reorder(RuntimeArray args, int ctx) { + String input = args.get(0).toString(); + StringBuilder output = new StringBuilder(input.length()); + List marks = new ArrayList<>(); + List classes = new ArrayList<>(); + + input.codePoints().forEach(codePoint -> { + int combiningClass = UCharacter.getCombiningClass(codePoint); + if (combiningClass == 0) { + appendCodePoints(output, marks); + marks.clear(); + classes.clear(); + output.appendCodePoint(codePoint); + return; + } + + int insertion = classes.size(); + while (insertion > 0 && classes.get(insertion - 1) > combiningClass) { + insertion--; + } + marks.add(insertion, codePoint); + classes.add(insertion, combiningClass); + }); + appendCodePoints(output, marks); + return new RuntimeScalar(output.toString()).getList(); + } + + /** Canonically compose an already-decomposed string. */ + public static RuntimeList compose(RuntimeArray args, int ctx) { + return new RuntimeScalar(Normalizer.normalize(args.get(0).toString(), Form.NFC)).getList(); + } + + private static void appendCodePoints(StringBuilder output, List codePoints) { + for (int codePoint : codePoints) output.appendCodePoint(codePoint); + } + // Normalize based on the form specified public static RuntimeList normalize(RuntimeArray args, int ctx) { RuntimeScalar formArg = args.get(0); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Universal.java b/src/main/java/org/perlonjava/runtime/perlmodule/Universal.java index f753c32f8e..2634a49b32 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Universal.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Universal.java @@ -155,13 +155,22 @@ public static RuntimeList can(RuntimeArray args, int ctx) { } } - // Handle SUPER::method - search parent classes only (skip index 0) - // This is used by Mojo::DynamicMethods: $caller->can('SUPER::can') + // A bare SUPER:: name is lexical: Perl resolves it relative to the + // package containing the call to can(), not relative to the invocant. + // This matters when a subclass inherits a method that asks + // $class->can('SUPER::method'). if (methodName.startsWith("SUPER::")) { String actualMethod = methodName.substring(7); + String callerPackage = RuntimeCode.getCurrentPackage(); + while (callerPackage.endsWith("::")) { + callerPackage = callerPackage.substring(0, callerPackage.length() - 2); + } + if (callerPackage.isEmpty()) { + callerPackage = perlClassName; + } RuntimeScalar method = InheritanceResolver.findMethodInHierarchy( - actualMethod, perlClassName, perlClassName + "::" + methodName, 1); - if (method != null && !isAutoloadDispatch(method, actualMethod, perlClassName)) { + actualMethod, callerPackage, callerPackage + "::" + methodName, 1); + if (method != null && !isAutoloadDispatch(method, actualMethod, callerPackage)) { return method.getList(); } return scalarUndef.getList(); diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index 3cacd37cd4..742dc53e78 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -166,6 +166,9 @@ private static RuntimeRegexState state() { private boolean deferredUserDefinedUnicodeProperties = false; private boolean hasBranchReset = false; // True if pattern uses (?|...) branch reset private boolean hasBackslashK = false; // True if pattern uses \K (keep assertion) + // An empty qr// object keeps its own empty pattern when interpolated; + // only empty match/substitution string syntax reuses the previous match. + private boolean quoteConstruction = false; private List warningsOnUse = new ArrayList<>(); // 0 = off, 1 = debug, 2 = debugcolor. Captured at the regex call site. private int lexicalDebugMode; @@ -205,6 +208,7 @@ public RuntimeRegex cloneTracked() { copy.deferredUserDefinedUnicodeProperties = this.deferredUserDefinedUnicodeProperties; copy.hasBranchReset = this.hasBranchReset; copy.hasBackslashK = this.hasBackslashK; + copy.quoteConstruction = this.quoteConstruction; copy.warningsOnUse = new ArrayList<>(this.warningsOnUse); copy.lexicalDebugMode = this.lexicalDebugMode; // replacement and callerArgs are not copied — they are set per-substitution @@ -1138,6 +1142,7 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS regex.patternUnicodeNoInternalMarkers = originalRegex.patternUnicodeNoInternalMarkers; regex.patternString = originalRegex.patternString; regex.hasPreservesMatch = originalRegex.hasPreservesMatch; + regex.quoteConstruction = originalRegex.quoteConstruction; regex.warningsOnUse = new ArrayList<>(originalRegex.warningsOnUse); regex.lexicalDebugMode = callSiteDebugMode != 0 ? callSiteDebugMode : originalRegex.lexicalDebugMode; @@ -1175,6 +1180,7 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS regex.patternUnicodeNoInternalMarkers = originalRegex.patternUnicodeNoInternalMarkers; regex.patternString = originalRegex.patternString; regex.hasPreservesMatch = originalRegex.hasPreservesMatch; + regex.quoteConstruction = originalRegex.quoteConstruction; regex.warningsOnUse = new ArrayList<>(originalRegex.warningsOnUse); regex.lexicalDebugMode = callSiteDebugMode != 0 ? callSiteDebugMode : originalRegex.lexicalDebugMode; @@ -1202,6 +1208,12 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS .propagateTaint(patternString); } + /** Mark a compiled value as originating from Perl's qr// constructor. */ + public static RuntimeScalar markQuoteConstruction(RuntimeScalar quotedRegex) { + resolveRegex(quotedRegex).quoteConstruction = true; + return quotedRegex; + } + private static void validateTaintedPatternSecurity(RuntimeScalar patternString) { if (!GlobalContext.isTaintModeActive() || patternString == null || !patternString.isTainted() || patternString.type == RuntimeScalarType.REGEX) { @@ -1292,6 +1304,7 @@ public static RuntimeScalar getReplacementRegex(RuntimeScalar patternString, Run regex.patternString = resolvedRegex.patternString; regex.regexFlags = resolvedRegex.regexFlags; regex.hasPreservesMatch = resolvedRegex.hasPreservesMatch; + regex.quoteConstruction = resolvedRegex.quoteConstruction; regex.useGAssertion = resolvedRegex.useGAssertion; regex.patternFlags = resolvedRegex.patternFlags; regex.hasBranchReset = resolvedRegex.hasBranchReset; @@ -1517,7 +1530,8 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc RegexFlags originalFlags = regex.regexFlags; // Handle empty pattern - reuse last successful pattern or use empty pattern - if (regex.patternString == null || regex.patternString.isEmpty()) { + if (!regex.quoteConstruction + && (regex.patternString == null || regex.patternString.isEmpty())) { if (regexState.lastSuccessfulPattern != null) { // Use the pattern from last successful match // But keep the current flags (especially /g and /i) @@ -2367,7 +2381,8 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar regex.callerArgs = null; // Handle empty pattern - reuse last successful pattern or use empty pattern - if (regex.patternString == null || regex.patternString.isEmpty()) { + if (!regex.quoteConstruction + && (regex.patternString == null || regex.patternString.isEmpty())) { if (state().lastSuccessfulPattern != null) { // Use the pattern from last successful match // But keep the current replacement and flags (especially /g and /i) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeState.java index c3de64ba10..bbd0a83294 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeState.java @@ -25,6 +25,7 @@ public final class GlobalRuntimeState { private final Map arrayValues = new HashMap<>(); private final Map hashValues = new HashMap<>(); private final Map foreachScalarAliases = new HashMap<>(); + private final Map temporaryScalarAliases = new HashMap<>(); private final Map importedSubs = new HashMap<>(); private final Map operatorOverrideGlobs = new HashMap<>(); private final Map codeRefs = new HashMap<>(); @@ -77,6 +78,10 @@ Map foreachScalarAliases() { return foreachScalarAliases; } + Map temporaryScalarAliases() { + return temporaryScalarAliases; + } + public Map codeRefs() { return codeRefs; } @@ -227,6 +232,7 @@ void clearCoreValues() { arrayValues.clear(); hashValues.clear(); foreachScalarAliases.clear(); + temporaryScalarAliases.clear(); coreGlobalsInitialized = false; invalidateStashEnumeration(); } @@ -279,6 +285,7 @@ synchronized void snapshotInto(GlobalRuntimeState target, RuntimeGraphCloner clo cloneMap(arrayValues, target.arrayValues, cloner, RuntimeArray.class); cloneMap(hashValues, target.hashValues, cloner, RuntimeHash.class); cloneMap(foreachScalarAliases, target.foreachScalarAliases, cloner, RuntimeScalar.class); + cloneMap(temporaryScalarAliases, target.temporaryScalarAliases, cloner, RuntimeScalar.class); cloneMap(codeRefs, target.codeRefs, cloner, RuntimeScalar.class); cloneMap(pseudoConstants, target.pseudoConstants, cloner, RuntimeScalar.class); cloneMap(pinnedCodeRefs, target.pinnedCodeRefs, cloner, RuntimeScalar.class); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index b512ecd4f9..f73cc97a31 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -97,6 +97,10 @@ private static Map foreachGlobalAliases() { return globalState().foreachScalarAliases(); } + private static Map temporaryGlobalAliases() { + return globalState().temporaryScalarAliases(); + } + private static Map globalPseudoConstants() { return globalState().pseudoConstants(); } @@ -1017,7 +1021,7 @@ public static RuntimeScalar getGlobalVariable(String key) { markPackageGlobalRoot(var); globalVariables.put(storageKey, var); invalidatePackageRootSnapshot(); - } else { + } else if (temporaryGlobalAliases().get(key) != var) { markPackageGlobalRoot(var); } return var; @@ -1039,6 +1043,43 @@ public static void aliasGlobalVariable(String key, RuntimeScalar var) { invalidatePackageRootSnapshot(); } + /** + * Temporarily aliases a package scalar without permanently labelling the + * aliased value as package-global. Operators such as map and grep localize + * {@code $_} to each input element; the global slot is a reachability root + * while the alias is installed, but the input SV must become ephemeral + * again when the operator restores the old slot. + */ + public static void aliasTemporaryGlobalVariable(String key, RuntimeScalar var) { + clearForeachGlobalAlias(key); + temporaryGlobalAliases().put(key, var); + globalVariables.put(key, var); + invalidatePackageRootSnapshot(); + } + + public static boolean isTemporaryGlobalAlias(String key) { + return temporaryGlobalAliases().containsKey(key); + } + + public static boolean isTemporaryGlobalAliasValue(RuntimeScalar value) { + for (RuntimeScalar alias : temporaryGlobalAliases().values()) { + if (alias == value) return true; + } + return false; + } + + public static void restoreTemporaryGlobalVariable( + String key, RuntimeScalar var, boolean wasTemporary) { + clearForeachGlobalAlias(key); + if (wasTemporary) { + temporaryGlobalAliases().put(key, var); + } else { + temporaryGlobalAliases().remove(key); + } + globalVariables.put(key, var); + invalidatePackageRootSnapshot(); + } + public static void aliasForeachGlobalVariable(String key, RuntimeScalar var) { clearForeachGlobalAlias(key); retainForeachAlias(var); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index a974c773e1..4564656b47 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -346,6 +346,21 @@ public static RuntimeBase findActiveLexical(RuntimeCode code, String variableNam return null; } + /** Return a stable snapshot of the live lexical cells for an active CV. */ + public static Map snapshotActiveLexicals(RuntimeCode code) { + if (code == null) return Collections.emptyMap(); + PerlRuntime runtime = PerlRuntime.current(); + if (!runtime.runtimeCodeState().lexicalAliasSupportEnabled) { + return Collections.emptyMap(); + } + for (ActiveLexicalFrame frame : activeLexicalFrames(runtime.executionState())) { + if (sameLogicalCode(frame.code(), code)) { + return new LinkedHashMap<>(frame.cells()); + } + } + return Collections.emptyMap(); + } + /** * Get the caller's @_ array (one level up from current). * Used by Java-implemented functions (like List::Util::any) that need to pass @@ -4118,6 +4133,31 @@ public static RuntimeCode getActiveCodeAtCallerFrame(int logicalFrame) { return activeCodeAtCallerFrame(logicalFrame); } + /** + * Resolve a PadWalker caller level from inside its Perl wrapper. + * Java builtins and map/grep block CVs are runtime implementation frames, + * not Perl subroutine levels, and therefore must not shift LEVEL. + */ + public static RuntimeCode getActiveCodeAtPadWalkerFrame(int level) { + if (level < 0) return null; + RuntimeCode previous = null; + int plumbingFrames = 2; // Internals::jperl_var_name and PadWalker::var_name + int logicalIndex = 0; + for (RuntimeCode active : activeCodeStack()) { + if (active == previous || isCompilerWrapperPair(active, previous)) { + continue; + } + previous = active; + if (active.isBuiltin || active.isMapGrepBlock) continue; + if (plumbingFrames > 0) { + plumbingFrames--; + continue; + } + if (logicalIndex++ == level) return active; + } + return null; + } + private static boolean isCompilerWrapperPair(RuntimeCode left, RuntimeCode right) { return left != null && right != null && Objects.equals(left.packageName, right.packageName) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 5e50c8c596..8c342588ed 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -3141,7 +3141,7 @@ public RuntimeScalar createReference() { } else if (this.refCount == -1 && !isRegisteredLexical && captureCount == 0 - && !isPackageGlobalRoot + && (!isPackageGlobalRoot || GlobalVariable.isTemporaryGlobalAliasValue(this)) && containerOwner == null && !RuntimeCode.isInstalledPadConstant(this)) { // An unbound scalar value returned from a subroutine is an diff --git a/src/main/perl/lib/CPAN/Config.pm b/src/main/perl/lib/CPAN/Config.pm index e5f117602a..2430f2cecf 100644 --- a/src/main/perl/lib/CPAN/Config.pm +++ b/src/main/perl/lib/CPAN/Config.pm @@ -81,6 +81,8 @@ sub _bootstrap_prefs { 'LRU-Cache.yml' => 'PerlOnJava/CpanDistroprefs/LRU-Cache.yml', 'Sort-External.yml' => 'PerlOnJava/CpanDistroprefs/Sort-External.yml', 'Char-Latin7.yml' => 'PerlOnJava/CpanDistroprefs/Char-Latin7.yml', + 'Char-Windows1258.yml' => 'PerlOnJava/CpanDistroprefs/Char-Windows1258.yml', + 'MooseX-BetterAnonNames.yml' => 'PerlOnJava/CpanDistroprefs/MooseX-BetterAnonNames.yml', 'Text-Markdown.yml' => 'PerlOnJava/CpanDistroprefs/Text-Markdown.yml', 'UUID-Tiny.yml' => 'PerlOnJava/CpanDistroprefs/UUID-Tiny.yml', 'MooX-ClassAttribute.yml' => 'PerlOnJava/CpanDistroprefs/MooX-ClassAttribute.yml', @@ -332,6 +334,10 @@ sub _bootstrap_patches { 'PerlOnJava/CpanPatches/Sort-External-0.18/PurePerl.patch' ], [ 'Char-Latin7/PerlOnJavaExecutable.patch', 'PerlOnJava/CpanPatches/Char-Latin7-1.15/PerlOnJavaExecutable.patch' ], + [ 'Char-Windows1258/PerlOnJavaExecutable.patch', + 'PerlOnJava/CpanPatches/Char-Windows1258-1.15/PerlOnJavaExecutable.patch' ], + [ 'MooseX-BetterAnonNames/NoAutobox.patch', + 'PerlOnJava/CpanPatches/MooseX-TraitFor-Meta-Class-BetterAnonClassNames-0.002003/NoAutobox.patch' ], [ 'Text-Markdown/BoundedBalancedPatterns.patch', 'PerlOnJava/CpanPatches/Text-Markdown-1.000031/BoundedBalancedPatterns.patch' ], ); diff --git a/src/main/perl/lib/POSIX.pm b/src/main/perl/lib/POSIX.pm index bc87eda0af..3533997c8b 100644 --- a/src/main/perl/lib/POSIX.pm +++ b/src/main/perl/lib/POSIX.pm @@ -92,7 +92,7 @@ our @EXPORT_OK = qw( # Math functions abs acos asin atan atan2 ceil cos cosh exp fabs floor fmod frexp - ldexp log log10 modf pow sin sinh sqrt tan tanh strtod + ldexp log log10 log2 modf pow sin sinh sqrt tan tanh strtod HUGE_VAL # String functions @@ -554,6 +554,7 @@ sub sinh { (CORE::exp($_[0]) - CORE::exp(-$_[0])) / 2 } sub cosh { (CORE::exp($_[0]) + CORE::exp(-$_[0])) / 2 } sub tanh { sinh($_[0]) / cosh($_[0]) } sub log10 { CORE::log($_[0]) / CORE::log(10) } +sub log2 { CORE::log($_[0]) / CORE::log(2) } sub ldexp { $_[0] * (2 ** $_[1]) } sub frexp { my $x = CORE::abs($_[0]); diff --git a/src/main/perl/lib/PadWalker.pm b/src/main/perl/lib/PadWalker.pm index 192dc40f32..2954f732e8 100644 --- a/src/main/perl/lib/PadWalker.pm +++ b/src/main/perl/lib/PadWalker.pm @@ -20,7 +20,7 @@ sub _unsupported { sub peek_my { _unsupported('peek_my') } sub peek_our { _unsupported('peek_our') } -sub var_name { _unsupported('var_name') } +sub var_name { Internals::jperl_var_name(@_) } sub set_closed_over { Internals::jperl_set_closed_over(@_) } 1; diff --git a/src/main/perl/lib/PerlOnJava/CpanDistroprefs/Char-Windows1258.yml b/src/main/perl/lib/PerlOnJava/CpanDistroprefs/Char-Windows1258.yml new file mode 100644 index 0000000000..87b1a3c3fc --- /dev/null +++ b/src/main/perl/lib/PerlOnJava/CpanDistroprefs/Char-Windows1258.yml @@ -0,0 +1,13 @@ +--- +comment: | + PerlOnJava compatibility patch for Char::Windows1258. The distribution + rejects executables named `jperl` because that name historically referred + to a different Japanese Perl implementation. Keep that guard while + recognizing PerlOnJava's launcher through PERLONJAVA_EXECUTABLE. Delegate + its regex-code-block source transformer to system Perl, use PerlOnJava's + native split implementation, and replace runtime regex code blocks with + bounded ordinary patterns that both compiler backends can execute. +match: + distribution: "^.*/Char-Windows1258-" +patches: + - "Char-Windows1258/PerlOnJavaExecutable.patch" diff --git a/src/main/perl/lib/PerlOnJava/CpanDistroprefs/MooseX-BetterAnonNames.yml b/src/main/perl/lib/PerlOnJava/CpanDistroprefs/MooseX-BetterAnonNames.yml new file mode 100644 index 0000000000..a82d881b36 --- /dev/null +++ b/src/main/perl/lib/PerlOnJava/CpanDistroprefs/MooseX-BetterAnonNames.yml @@ -0,0 +1,11 @@ +--- +comment: | + PerlOnJava compatibility patch: this Moose metaclass trait uses + autobox::Core for a single string split. + Replace that call with Perl's portable split operator and remove the matching + MakeMaker prerequisite so CPAN does not install autobox's deep compile-op XS + hooks merely to derive an anonymous package serial number. +match: + distribution: "^.*/MooseX-TraitFor-Meta-Class-BetterAnonClassNames-" +patches: + - "MooseX-BetterAnonNames/NoAutobox.patch" diff --git a/src/main/perl/lib/PerlOnJava/CpanPatches/Char-Windows1258-1.15/PerlOnJavaExecutable.patch b/src/main/perl/lib/PerlOnJava/CpanPatches/Char-Windows1258-1.15/PerlOnJavaExecutable.patch new file mode 100644 index 0000000000..b45410f978 --- /dev/null +++ b/src/main/perl/lib/PerlOnJava/CpanPatches/Char-Windows1258-1.15/PerlOnJavaExecutable.patch @@ -0,0 +1,176 @@ +--- lib/Windows1258.pm.orig ++++ lib/Windows1258.pm +@@ -29,7 +29,7 @@ + $VERSION = $VERSION; + + BEGIN { +- if ($^X =~ / jperl /oxmsi) { ++ if ($^X =~ / jperl /oxmsi and not $ENV{PERLONJAVA_EXECUTABLE}) { + die __FILE__, ": needs perl(not jperl) 5.00503 or later. (\$^X==$^X)\n"; + } + if (CORE::ord('A') == 193) { +@@ -226,8 +226,24 @@ + CORE::eval q{ truncate($fh, 0) }; + seek($fh, 0, 0) or die __FILE__, ": Can't seek file: $filename.e\n"; + +- my $e_script = Windows1258::escape_script($filename); +- print {$fh} $e_script; ++ if ($ENV{PERLONJAVA_EXECUTABLE}) { ++ # PerlOnJava can execute the escaped program, but Ewindows1258's source ++ # transformer relies on Perl regex code blocks. Run only that ++ # build-time transformation with the system Perl. ++ my $generator = gensym(); ++ my $system_perl = $ENV{PERLONJAVA_SYSTEM_PERL} || 'perl'; ++ open($generator, '-|', $system_perl, __FILE__, $filename) ++ or die __FILE__, ": Can't start system Perl source transformer: $!\n"; ++ while (defined(my $line = <$generator>)) { ++ print {$fh} $line; ++ } ++ close($generator) ++ or die __FILE__, ": system Perl source transformer failed: $!\n"; ++ } ++ else { ++ my $e_script = Windows1258::escape_script($filename); ++ print {$fh} $e_script; ++ } + + my $mode = (stat($filename))[2] & 0777; + chmod $mode, "$filename.e"; +--- lib/Ewindows1258.pm.orig ++++ lib/Ewindows1258.pm +@@ -29,7 +29,7 @@ + $VERSION = $VERSION; + + BEGIN { +- if ($^X =~ / jperl /oxmsi) { ++ if ($^X =~ / jperl /oxmsi and not $ENV{PERLONJAVA_EXECUTABLE}) { + die __FILE__, ": needs perl(not jperl) 5.00503 or later. (\$^X==$^X)\n"; + } + if (CORE::ord('A') == 193) { +@@ -541,6 +541,15 @@ + } + } + ++ # PerlOnJava's native split already operates on the byte-preserving regex ++ # and avoids this legacy implementation's zero-width substitution loop. ++ if ($ENV{PERLONJAVA_EXECUTABLE}) { ++ if (defined $limit) { ++ return CORE::split($pattern, $string, $limit); ++ } ++ return CORE::split($pattern, $string); ++ } ++ + my @split = (); + + # when string is empty +@@ -2880,46 +2889,24 @@ + use vars qw($nest); + + # regexp of nested parens in qqXX +- +-# P.340 Matching Nested Constructs with Embedded Code +-# in Chapter 7: Perl +-# of ISBN 0-596-00289-0 Mastering Regular Expressions, Second edition + +-my $qq_paren = qr{(?{local $nest=0}) (?>(?: +- [^\\()] | +- \( (?{$nest++}) | +- \) (?(?{$nest>0})(?{$nest--})|(?!)))*) (?(?{$nest!=0})(?!)) | +- \\ [^c] | +- \\c[\x40-\x5F] | +- [\x00-\xFF] +- }xms; +- +-my $qq_brace = qr{(?{local $nest=0}) (?>(?: +- [^\\{}] | +- \{ (?{$nest++}) | +- \} (?(?{$nest>0})(?{$nest--})|(?!)))*) (?(?{$nest!=0})(?!)) | +- \\ [^c] | +- \\c[\x40-\x5F] | +- [\x00-\xFF] +- }xms; ++# PerlOnJava: Java regexes do not execute Perl code blocks. Expand balanced ++# delimiter patterns to a fixed practical depth using ordinary regexes. ++my $qq_paren_nested = qr{[^\\()]*}; ++my $qq_brace_nested = qr{[^\\{}]*}; ++my $qq_bracket_nested = qr{[^\\\[\]]*}; ++my $qq_angle_nested = qr{[^\\<>]*}; ++for (1 .. 4) { ++ $qq_paren_nested = qr{(?: [^\\()] | \( $qq_paren_nested \) )*}xms; ++ $qq_brace_nested = qr{(?: [^\\{}] | \{ $qq_brace_nested \} )*}xms; ++ $qq_bracket_nested = qr{(?: [^\\\[\]] | \[ $qq_bracket_nested \] )*}xms; ++ $qq_angle_nested = qr{(?: [^\\<>] | \< $qq_angle_nested \> )*}xms; ++} + +-my $qq_bracket = qr{(?{local $nest=0}) (?>(?: +- [^\\\[\]] | +- \[ (?{$nest++}) | +- \] (?(?{$nest>0})(?{$nest--})|(?!)))*) (?(?{$nest!=0})(?!)) | +- \\ [^c] | +- \\c[\x40-\x5F] | +- [\x00-\xFF] +- }xms; +- +-my $qq_angle = qr{(?{local $nest=0}) (?>(?: +- [^\\<>] | +- \< (?{$nest++}) | +- \> (?(?{$nest>0})(?{$nest--})|(?!)))*) (?(?{$nest!=0})(?!)) | +- \\ [^c] | +- \\c[\x40-\x5F] | +- [\x00-\xFF] +- }xms; ++my $qq_paren = qr{(?>(?: $qq_paren_nested )) | \\ [^c] | \\c[\x40-\x5F] | [\x00-\xFF]}xms; ++my $qq_brace = qr{(?>(?: $qq_brace_nested )) | \\ [^c] | \\c[\x40-\x5F] | [\x00-\xFF]}xms; ++my $qq_bracket = qr{(?>(?: $qq_bracket_nested )) | \\ [^c] | \\c[\x40-\x5F] | [\x00-\xFF]}xms; ++my $qq_angle = qr{(?>(?: $qq_angle_nested )) | \\ [^c] | \\c[\x40-\x5F] | [\x00-\xFF]}xms; + + my $qq_scalar = qr{(?: \{ (?:$qq_brace)*? \} | + (?: ::)? (?: +@@ -2944,33 +2931,21 @@ + }xms; + + # regexp of nested parens in qXX +-my $q_paren = qr{(?{local $nest=0}) (?>(?: +- [^()] | +- \( (?{$nest++}) | +- \) (?(?{$nest>0})(?{$nest--})|(?!)))*) (?(?{$nest!=0})(?!)) | +- [\x00-\xFF] +- }xms; +- +-my $q_brace = qr{(?{local $nest=0}) (?>(?: +- [^\{\}] | +- \{ (?{$nest++}) | +- \} (?(?{$nest>0})(?{$nest--})|(?!)))*) (?(?{$nest!=0})(?!)) | +- [\x00-\xFF] +- }xms; +- +-my $q_bracket = qr{(?{local $nest=0}) (?>(?: +- [^\[\]] | +- \[ (?{$nest++}) | +- \] (?(?{$nest>0})(?{$nest--})|(?!)))*) (?(?{$nest!=0})(?!)) | +- [\x00-\xFF] +- }xms; ++my $q_paren_nested = qr{[^()]*}; ++my $q_brace_nested = qr{[^{}]*}; ++my $q_bracket_nested = qr{[^\[\]]*}; ++my $q_angle_nested = qr{[^<>]*}; ++for (1 .. 4) { ++ $q_paren_nested = qr{(?: [^()] | \( $q_paren_nested \) )*}xms; ++ $q_brace_nested = qr{(?: [^{}] | \{ $q_brace_nested \} )*}xms; ++ $q_bracket_nested = qr{(?: [^\[\]] | \[ $q_bracket_nested \] )*}xms; ++ $q_angle_nested = qr{(?: [^<>] | \< $q_angle_nested \> )*}xms; ++} + +-my $q_angle = qr{(?{local $nest=0}) (?>(?: +- [^<>] | +- \< (?{$nest++}) | +- \> (?(?{$nest>0})(?{$nest--})|(?!)))*) (?(?{$nest!=0})(?!)) | +- [\x00-\xFF] +- }xms; ++my $q_paren = qr{(?>(?: $q_paren_nested )) | [\x00-\xFF]}xms; ++my $q_brace = qr{(?>(?: $q_brace_nested )) | [\x00-\xFF]}xms; ++my $q_bracket = qr{(?>(?: $q_bracket_nested )) | [\x00-\xFF]}xms; ++my $q_angle = qr{(?>(?: $q_angle_nested )) | [\x00-\xFF]}xms; + + my $matched = ''; + my $s_matched = ''; diff --git a/src/main/perl/lib/PerlOnJava/CpanPatches/MooseX-TraitFor-Meta-Class-BetterAnonClassNames-0.002003/NoAutobox.patch b/src/main/perl/lib/PerlOnJava/CpanPatches/MooseX-TraitFor-Meta-Class-BetterAnonClassNames-0.002003/NoAutobox.patch new file mode 100644 index 0000000000..0cc388ccfd --- /dev/null +++ b/src/main/perl/lib/PerlOnJava/CpanPatches/MooseX-TraitFor-Meta-Class-BetterAnonClassNames-0.002003/NoAutobox.patch @@ -0,0 +1,38 @@ +--- lib/MooseX/TraitFor/Meta/Class/BetterAnonClassNames.pm.orig ++++ lib/MooseX/TraitFor/Meta/Class/BetterAnonClassNames.pm +@@ -14,7 +14,6 @@ + + use Moose::Role; + use namespace::autoclean; +-use autobox::Core; + + use Moose::Exporter; + +@@ -67,7 +66,8 @@ + unless $opts{is_anon} && $opts{anon_package_prefix}; + + ### old anon package name: $opts{package} +- my $serial = $opts{package}->split(qr/::/)->[-1]; #at(-1); #tail(1); ++ my @package_parts = split qr/::/, $opts{package}; ++ my $serial = $package_parts[-1]; + $opts{package} = $opts{anon_package_prefix} . $serial; + + ### new anon package name: $opts{package} +--- Makefile.PL.orig ++++ Makefile.PL +@@ -28,7 +28,6 @@ + "PREREQ_PM" => { + "Moose::Exporter" => 0, + "Moose::Role" => 0, +- "autobox::Core" => 0, + "namespace::autoclean" => 0 + }, + "TEST_REQUIRES" => { +@@ -64,7 +63,6 @@ + "Test::CheckDeps" => "0.010", + "Test::Moose::More" => 0, + "Test::More" => "0.94", +- "autobox::Core" => 0, + "blib" => "1.01", + "namespace::autoclean" => 0, + "strict" => 0, diff --git a/src/main/perl/lib/_charnames.pm b/src/main/perl/lib/_charnames.pm index 50b99667e9..d5bb3c4b8e 100644 --- a/src/main/perl/lib/_charnames.pm +++ b/src/main/perl/lib/_charnames.pm @@ -428,6 +428,15 @@ sub lookup_name ($name, $wants_ord, $runtime, $regex_loose //= 0) { { $result = chr $ord; } + # PerlOnJava bundles ICU4J, whose Unicode name database is complete and + # current. Use it for strict official-name lookup before falling back to + # the generated Perl table, just as viacode() does for reverse lookup. + elsif (! $loose && $^H{charnames_full} && defined &_java_vianame + && defined(my $java_ord = _java_vianame($lookup_name))) + { + $result = chr $java_ord; + $full_names_cache{$name} = $result; + } else { # Not algorithmically determinable; look up in the table. The name diff --git a/src/test/resources/unit/charnames_runtime_lookup.t b/src/test/resources/unit/charnames_runtime_lookup.t new file mode 100644 index 0000000000..d34b851558 --- /dev/null +++ b/src/test/resources/unit/charnames_runtime_lookup.t @@ -0,0 +1,14 @@ +use strict; +use warnings; +use Test::More tests => 6; +use charnames (); + +is(charnames::vianame('LATIN CAPITAL LETTER O'), 0x4f, 'looks up a BMP character name'); +is(charnames::string_vianame('LATIN CAPITAL LETTER O'), 'O', 'returns the BMP character string'); +is(charnames::viacode(0xd8), 'LATIN CAPITAL LETTER O WITH STROKE', 'reverse lookup remains available'); + +is(charnames::vianame('GOTHIC LETTER AHSA'), 0x10330, 'looks up a supplementary character name'); +is(charnames::string_vianame('GOTHIC LETTER AHSA'), "\x{10330}", + 'returns a supplementary character string'); +ok(!defined charnames::vianame('NOT A REAL UNICODE CHARACTER NAME'), + 'unknown names return undef'); diff --git a/src/test/resources/unit/dbi_execute_select_result.t b/src/test/resources/unit/dbi_execute_select_result.t new file mode 100644 index 0000000000..5d18fedc83 --- /dev/null +++ b/src/test/resources/unit/dbi_execute_select_result.t @@ -0,0 +1,21 @@ +use strict; +use warnings; +use Test::More tests => 4; +use DBI; + +my $dbh = DBI->connect('dbi:SQLite:dbname=:memory:', '', '', { + RaiseError => 1, + PrintError => 0, +}); + +$dbh->do('CREATE TABLE example (value INTEGER)'); + +my $select = $dbh->prepare('SELECT value FROM example'); +is($select->execute, '0E0', 'SELECT execute returns DBI true-zero with no rows'); + +$dbh->do('INSERT INTO example VALUES (42)'); +is($select->execute, '0E0', 'SELECT execute returns DBI true-zero before fetching rows'); +is_deeply($select->fetchrow_arrayref, [42], 'SELECT result remains available'); + +$select->finish; +ok($dbh->disconnect, 'disconnect succeeds'); diff --git a/src/test/resources/unit/map_temporary_weak_refs.t b/src/test/resources/unit/map_temporary_weak_refs.t new file mode 100644 index 0000000000..fe72882474 --- /dev/null +++ b/src/test/resources/unit/map_temporary_weak_refs.t @@ -0,0 +1,17 @@ +use strict; +use warnings; +use Scalar::Util qw(weaken); +use Test::More tests => 4; + +sub temporary_values { + my $base = 1; + $base + 1, $base + 4, $base + 6; +} + +my @references = map { \$_ } temporary_values(); +is(scalar @references, 3, 'map returns references to each temporary value'); + +for my $index (0 .. $#references) { + weaken($references[$index]); + ok(!defined $references[$index], 'temporary map input weakens away'); +} diff --git a/src/test/resources/unit/padwalker_var_name.t b/src/test/resources/unit/padwalker_var_name.t new file mode 100644 index 0000000000..4bb8efd7ba --- /dev/null +++ b/src/test/resources/unit/padwalker_var_name.t @@ -0,0 +1,44 @@ +use strict; +use warnings; +use Test::More tests => 8; +use PadWalker qw(var_name); + +sub check_current_pad { + my $scalar = 42; + my @array = qw(alpha beta); + my %hash = (answer => 42); + my $unrelated = 'other'; + + is(var_name(0, \$scalar), '$scalar', 'finds a scalar in the current pad'); + is(var_name(0, \@array), '@array', 'finds an array in the current pad'); + is(var_name(0, \%hash), '%hash', 'finds a hash in the current pad'); + is(var_name(0, \$unrelated), '$unrelated', 'finds another current-pad variable'); +} + +sub name_in_caller { + return var_name(1, shift); +} + +sub names_through_map { + return map { var_name(1, \$_) } @_; +} + +sub check_caller_pad { + my $caller_value = 42; + is(name_in_caller(\$caller_value), '$caller_value', 'finds a variable in a caller pad'); + + my $first = 'one'; + my $second = 'two'; + is_deeply([names_through_map($first, $second)], ['$first', '$second'], + 'map aliases do not add a PadWalker caller level'); +} + +check_current_pad(); +check_caller_pad(); + +my $captured = 'value'; +my $closure = sub { return $captured }; +is(var_name($closure, \$captured), '$captured', 'finds a captured variable in a coderef'); + +my $unrelated = 'other'; +ok(!defined var_name($closure, \$unrelated), 'returns undef for an unrelated variable'); diff --git a/src/test/resources/unit/posix_log2.t b/src/test/resources/unit/posix_log2.t new file mode 100644 index 0000000000..00249acc4e --- /dev/null +++ b/src/test/resources/unit/posix_log2.t @@ -0,0 +1,16 @@ +use strict; +use warnings; +use Test::More tests => 4; + +use POSIX qw(log2); + +ok(defined(&log2), 'POSIX exports log2 on request'); +{ + package DefaultImport; + POSIX->import(); + sub has_log2 { defined(&log2) } +} +ok(!DefaultImport::has_log2(), 'POSIX does not export log2 by default'); +is(log2(8), 3, 'log2 computes an integral power of two'); +cmp_ok(abs(log2(10) - (log(10) / log(2))), '<', 1e-12, + 'log2 agrees with the logarithm identity'); diff --git a/src/test/resources/unit/regex_empty_pattern_origin.t b/src/test/resources/unit/regex_empty_pattern_origin.t new file mode 100644 index 0000000000..1839948ddc --- /dev/null +++ b/src/test/resources/unit/regex_empty_pattern_origin.t @@ -0,0 +1,38 @@ +use strict; +use warnings; +use Test::More tests => 8; + +"a" =~ /a/; +ok !("b" =~ //), 'literal empty match reuses the last successful pattern'; + +my $empty_qr = qr//; +"a" =~ /a/; +ok "b" =~ /$empty_qr/, 'an interpolated empty qr does not reuse the last pattern'; + +my $patterns = { empty => qr// }; +"a" =~ /a/; +ok "b" =~ /$patterns->{empty}/, + 'an empty qr behind a hash dereference keeps its own origin'; + +my $empty_string = ''; +"a" =~ /a/; +ok !("b" =~ /$empty_string/), + 'a dynamically empty string still reuses the last pattern'; + +$_ = 'b'; +"a" =~ /a/; +is s//x/r, 'b', 'literal empty substitution reuses the last pattern'; + +$_ = 'b'; +"a" =~ /a/; +is s/$empty_qr/x/r, 'xb', 'an interpolated empty qr substitutes at the start'; + +$_ = 'b'; +"a" =~ /a/; +is s/$patterns->{empty}/x/r, + 'xb', 'an empty qr hash value substitutes at the start'; + +$_ = 'b'; +"a" =~ /a/; +is s/$empty_string/x/r, + 'b', 'a dynamically empty string substitution reuses the last pattern'; diff --git a/src/test/resources/unit/unicode_normalize_lowlevel.t b/src/test/resources/unit/unicode_normalize_lowlevel.t new file mode 100644 index 0000000000..9b7f074f2f --- /dev/null +++ b/src/test/resources/unit/unicode_normalize_lowlevel.t @@ -0,0 +1,13 @@ +use strict; +use warnings; +use Test::More tests => 6; +use Unicode::Normalize qw(decompose reorder compose); + +is(decompose("\x{e9}"), "e\x{301}", 'canonical decomposition'); +is(decompose("\x{fb01}"), "\x{fb01}", 'canonical decomposition keeps compatibility characters'); +is(decompose("\x{fb01}", 1), 'fi', 'compatibility decomposition expands ligatures'); + +is(reorder("a\x{315}\x{300}"), "a\x{300}\x{315}", 'reorders combining marks'); + +is(compose("e\x{301}"), "\x{e9}", 'canonical composition'); +is(compose('plain'), 'plain', 'composition preserves normalized text'); diff --git a/src/test/resources/unit/universal_can_super_caller.t b/src/test/resources/unit/universal_can_super_caller.t new file mode 100644 index 0000000000..bfb979ed1c --- /dev/null +++ b/src/test/resources/unit/universal_can_super_caller.t @@ -0,0 +1,25 @@ +use strict; +use warnings; +use Test::More tests => 3; + +{ + package CanSuper::Base; + sub import { 'base import' } + + package CanSuper::Middle; + our @ISA = ('CanSuper::Base'); + sub import { 'middle import' } + sub inherited_import { + my ($class) = @_; + return $class->can('SUPER::import'); + } + + package CanSuper::Child; + our @ISA = ('CanSuper::Middle'); +} + +my $super_import = CanSuper::Child->inherited_import; +ok $super_import, 'can finds a SUPER method relative to the calling package'; +is $super_import->(), 'base import', 'can(SUPER::method) skips the caller package'; +is(CanSuper::Child->can('CanSuper::Middle::SUPER::import')->(), + 'base import', 'explicit Package::SUPER::method has the same resolution');