diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java index 9055e8ff1..a8587c458 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java @@ -526,6 +526,9 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) { if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("use: " + token.text); boolean isNoDeclaration = token.text.equals("no"); + // Capture token index for caller() before consuming any tokens + int useTokenIndex = parser.tokenIndex; + TokenUtils.consume(parser); // "use" token = TokenUtils.peek(parser); @@ -627,11 +630,12 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) { // execute the statement immediately, using: // `require "fullName.pm"` - // Setup the caller stack + // Setup the caller stack - use getSourceLocationAccurate to honor #line directives + ErrorMessageUtil.SourceLocation loc = ctx.errorUtil.getSourceLocationAccurate(useTokenIndex); CallerStack.push( ctx.symbolTable.getCurrentPackage(), - ctx.compilerOptions.fileName, - ctx.errorUtil.getLineNumber(parser.tokenIndex)); + loc.fileName(), + loc.lineNumber()); try { if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Use statement: " + fullName + " called from " + CallerStack.peek(0)); diff --git a/src/main/java/org/perlonjava/runtime/mro/DFS.java b/src/main/java/org/perlonjava/runtime/mro/DFS.java index 66b5c6d94..af085c602 100644 --- a/src/main/java/org/perlonjava/runtime/mro/DFS.java +++ b/src/main/java/org/perlonjava/runtime/mro/DFS.java @@ -1,6 +1,7 @@ package org.perlonjava.runtime.mro; import org.perlonjava.runtime.runtimetypes.GlobalVariable; +import org.perlonjava.runtime.runtimetypes.NameNormalizer; import org.perlonjava.runtime.runtimetypes.PerlCompilerException; import org.perlonjava.runtime.runtimetypes.RuntimeArray; import org.perlonjava.runtime.runtimetypes.RuntimeBase; @@ -91,6 +92,8 @@ private static void populateIsaMapWithCycleDetection(String className, String parentName = entity.toString(); // FIXED: Skip empty or null parent names if (parentName != null && !parentName.isEmpty()) { + // Normalize old-style ' separator to :: (e.g., Foo'Bar -> Foo::Bar) + parentName = NameNormalizer.normalizePackageName(parentName); parents.add(parentName); } } diff --git a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java index e34f40ef7..bfba25b4d 100644 --- a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java +++ b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java @@ -226,6 +226,8 @@ private static void populateIsaMapHelper(String className, } } if (!parentName.isEmpty()) { + // Normalize old-style ' separator to :: (e.g., Foo'Bar -> Foo::Bar) + parentName = NameNormalizer.normalizePackageName(parentName); parents.add(parentName); } } diff --git a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java index ab30dfb84..3222a1411 100644 --- a/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/FileTestOperator.java @@ -81,7 +81,7 @@ private static RuntimeScalar callerWhere() { } String fileName = caller.elements.get(1).toString(); int line = ((RuntimeScalar) caller.elements.get(2)).getInt(); - return new RuntimeScalar(" at " + fileName + " line " + line + "\n"); + return new RuntimeScalar(" at " + fileName + " line " + line + ".\n"); } private static String filehandleShortName(RuntimeScalar fileHandle) { diff --git a/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java b/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java index e81239161..0d567b1de 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ModuleOperators.java @@ -615,12 +615,22 @@ else if (code == null) { FeatureFlags outerFeature = featureManager; String savedPackage = InterpreterState.currentPackage.get().toString(); + // Save and clear %^H (hints hash) to prevent hint leakage into required modules. + // In Perl >= 5.11 (which we emulate), hints don't leak into require'd files. + // The hints hash affects compile-time behavior (strict, warnings, features), + // and a required module should start with clean compile-time state. + RuntimeHash hintHash = GlobalVariable.getGlobalHash(GlobalContext.encodeSpecialVar("H")); + java.util.Map savedHintHash = new java.util.HashMap<>(hintHash.elements); + // Notify B::Hooks::EndOfScope that we're starting to load a file // This enables on_scope_end callbacks to know which file they belong to BHooksEndOfScope.beginFileLoad(parsedArgs.fileName); try { featureManager = new FeatureFlags(); + + // Clear the hints hash for a fresh compilation context + hintHash.elements.clear(); result = PerlLanguageProvider.executePerlCode(parsedArgs, false, ctx); @@ -643,6 +653,10 @@ else if (code == null) { featureManager = outerFeature; InterpreterState.currentPackage.get().set(savedPackage); + + // Restore the caller's hints hash + hintHash.elements.clear(); + hintHash.elements.putAll(savedHintHash); } // Return result based on context @@ -732,7 +746,8 @@ public static RuntimeScalar require(RuntimeScalar runtimeScalar) { RuntimeScalar incEntry = incHash.elements.get(fileName); if (!incEntry.defined().getBoolean()) { // This was a compilation failure, throw the cached error - throw new PerlCompilerException("Compilation failed in require at " + fileName); + // Perl outputs: "Attempt to reload aborted.\nCompilation failed in require at ..." + throw new PerlCompilerException("Attempt to reload " + fileName + " aborted.\nCompilation failed in require at " + fileName); } // module was already loaded successfully - always return exactly 1 return getScalarInt(1); @@ -763,7 +778,22 @@ public static RuntimeScalar require(RuntimeScalar runtimeScalar) { message = fileName + " did not return a true value"; throw new PerlCompilerException(message); } else if (err.isEmpty()) { - message = "Can't locate " + fileName + " in @INC"; + // Derive module name from filename for helpful error message + String moduleName = fileName; + if (moduleName.endsWith(".pm")) { + moduleName = moduleName.substring(0, moduleName.length() - 3); + } + moduleName = moduleName.replace("/", "::"); + + // Build @INC list for error message + RuntimeArray incArray = GlobalVariable.getGlobalArray("main::INC"); + StringBuilder incList = new StringBuilder(); + for (int i = 0; i < incArray.size(); i++) { + if (i > 0) incList.append(" "); + incList.append(incArray.get(i).toString()); + } + + message = "Can't locate " + fileName + " in @INC (you may need to install the " + moduleName + " module) (@INC entries checked: " + incList + ")"; // Don't set %INC for file not found errors throw new PerlCompilerException(message); } else { diff --git a/src/main/java/org/perlonjava/runtime/operators/Operator.java b/src/main/java/org/perlonjava/runtime/operators/Operator.java index 5b2ee702b..fc1c91d7a 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Operator.java +++ b/src/main/java/org/perlonjava/runtime/operators/Operator.java @@ -276,17 +276,18 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas int length = hasExplicitLength ? ((RuntimeScalar) args[2]).getInt() : strLength - offset; String replacement = (size > 3) ? args[3].toString() : null; - // Store original offset and length for LValue creation - int originalOffset = offset; - int originalLength = length; - // Handle negative offsets (count from the end of the string) if (offset < 0) { offset = strLength + offset; - // When no explicit length is provided, Perl clips negative offsets to 0 (no warning) - // When explicit length IS provided, Perl warns and returns undef for too-negative offsets + // When computed offset goes negative (before string start): + // - Clip offset to 0 + // - Reduce length by the overshoot amount + // Example: substr("a", -2, 2) -> offset=-1, clip to 0, length=2+(-1)=1, returns "a" + // But: substr("hello", -10, 1) -> offset=-5, length=1+(-5)=-4 → warn and return undef if (offset < 0) { - if (hasExplicitLength) { + // Check if adjusted length would be non-positive (Perl warns in this case) + int adjustedLength = length + offset; + if (adjustedLength <= 0) { // Warn and return undef (same as positive offset out of bounds) if (warnEnabled) { WarnDie.warn(new RuntimeScalar("substr outside of string"), @@ -295,14 +296,14 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas if (replacement != null) { return new RuntimeScalar(); } - var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", originalOffset, originalLength); + var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", 0, 0); lvalue.type = RuntimeScalarType.UNDEF; lvalue.value = null; return lvalue; - } else { - // Clip to 0 without warning - offset = 0; } + // Reduce length by the overshoot (negative offset value) + length = adjustedLength; + offset = 0; } } @@ -315,7 +316,7 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas if (replacement != null) { return new RuntimeScalar(); } - var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", originalOffset, originalLength); + var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", offset, length); lvalue.type = RuntimeScalarType.UNDEF; lvalue.value = null; return lvalue; @@ -332,6 +333,17 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas // Ensure length is non-negative and within bounds length = Math.max(0, Math.min(length, strLength - offset)); + // If length is zero or negative after all adjustments, return empty string + if (length <= 0) { + if (replacement != null) { + // With replacement, still need to handle the replacement at position 0 + var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", offset, 0); + lvalue.set(replacement); + return new RuntimeScalar(""); + } + return new RuntimeSubstrLvalue((RuntimeScalar) args[0], "", offset, 0); + } + // Extract the substring (offset/length are in Unicode code points) int startIndex = str.offsetByCodePoints(0, offset); int endIndex = str.offsetByCodePoints(startIndex, length); @@ -339,7 +351,8 @@ private static RuntimeScalar substrImpl(int ctx, boolean warnEnabled, RuntimeBas // Return an LValue "RuntimeSubstrLvalue" that can be used to assign to the original string // This allows for in-place modification of the original string if needed - var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], result, originalOffset, originalLength); + // Pass the adjusted offset and length, not the originals + var lvalue = new RuntimeSubstrLvalue((RuntimeScalar) args[0], result, offset, length); if (replacement != null) { // When replacement is provided, save the extracted substring before modifying diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Base.java b/src/main/java/org/perlonjava/runtime/perlmodule/Base.java index 5ae2bea0e..00f9da3bd 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Base.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Base.java @@ -53,6 +53,9 @@ public static RuntimeList importBase(RuntimeArray args, int ctx) { RuntimeList callerList = RuntimeCode.caller(new RuntimeList(), RuntimeContextType.SCALAR); String inheritor = callerList.scalar().toString(); + // Keep track of bases we're adding in this import call + java.util.List basesToAdd = new java.util.ArrayList<>(); + // Process each base class specified in the arguments for (RuntimeScalar baseClass : args.elements) { String baseClassName = baseClass.toString(); @@ -62,6 +65,35 @@ public static RuntimeList importBase(RuntimeArray args, int ctx) { continue; } + // Check if inheritor or any base we're adding already isa this base class + // This matches Perl's base.pm line 92: next if grep $_->isa($base), ($inheritor, @bases); + boolean shouldSkip = false; + + // Check if inheritor already isa baseClassName + RuntimeArray isaArgs = new RuntimeArray(); + RuntimeArray.push(isaArgs, new RuntimeScalar(inheritor)); + RuntimeArray.push(isaArgs, new RuntimeScalar(baseClassName)); + if (Universal.isa(isaArgs, RuntimeContextType.SCALAR).getBoolean()) { + shouldSkip = true; + } + + // Check if any of the bases we're adding already isa baseClassName + if (!shouldSkip) { + for (String addedBase : basesToAdd) { + RuntimeArray isaArgs2 = new RuntimeArray(); + RuntimeArray.push(isaArgs2, new RuntimeScalar(addedBase)); + RuntimeArray.push(isaArgs2, new RuntimeScalar(baseClassName)); + if (Universal.isa(isaArgs2, RuntimeContextType.SCALAR).getBoolean()) { + shouldSkip = true; + break; + } + } + } + + if (shouldSkip) { + continue; + } + if (!GlobalVariable.isPackageLoaded(baseClassName)) { // Require the base class file String filename = baseClassName.replace("::", "/").replace("'", "/") + ".pm"; @@ -77,8 +109,13 @@ public static RuntimeList importBase(RuntimeArray args, int ctx) { } } - // Add the base class to the @ISA array of the inheritor - RuntimeArray isa = getGlobalArray(inheritor + "::ISA"); + // Add to our list of bases to add + basesToAdd.add(baseClassName); + } + + // Add all the bases to @ISA at the end (like Perl's base.pm line 138) + RuntimeArray isa = getGlobalArray(inheritor + "::ISA"); + for (String baseClassName : basesToAdd) { RuntimeArray.push(isa, new RuntimeScalar(baseClassName)); } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java index 94e83ce95..9d74294d3 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java @@ -6,6 +6,7 @@ import org.perlonjava.runtime.runtimetypes.SystemUtils; import java.io.File; +import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; @@ -425,7 +426,22 @@ public static RuntimeList abs2rel(RuntimeArray args, int ctx) { } String path = args.get(1).toString(); String base = args.size() == 3 ? args.get(2).toString() : System.getProperty("user.dir"); - String relPath = Paths.get(base).relativize(Paths.get(path)).toString(); + + // Ensure both paths are absolute before relativizing (like Perl does) + // Note: We use user.dir explicitly because Java's Path.toAbsolutePath() + // doesn't respect System.setProperty("user.dir", ...) set by chdir() + Path pathObj = Paths.get(path); + Path baseObj = Paths.get(base); + String userDir = System.getProperty("user.dir"); + + if (!pathObj.isAbsolute()) { + pathObj = Paths.get(userDir).resolve(pathObj).normalize(); + } + if (!baseObj.isAbsolute()) { + baseObj = Paths.get(userDir).resolve(baseObj).normalize(); + } + + String relPath = baseObj.relativize(pathObj).toString(); return new RuntimeScalar(relPath).getList(); } @@ -454,8 +470,14 @@ public static RuntimeList rel2abs(RuntimeArray args, int ctx) { return new RuntimeScalar(absPath).getList(); } + // If base is relative, resolve it against current working directory first + Path basePath = Paths.get(base); + if (!basePath.isAbsolute()) { + basePath = Paths.get(System.getProperty("user.dir")).resolve(basePath); + } + // For relative paths, resolve against the base directory - String absPath = Paths.get(base, path).toAbsolutePath().normalize().toString(); + String absPath = basePath.resolve(path).normalize().toString(); return new RuntimeScalar(absPath).getList(); } } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Universal.java b/src/main/java/org/perlonjava/runtime/perlmodule/Universal.java index c20aa0fa5..8c790e4a8 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Universal.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Universal.java @@ -232,9 +232,11 @@ public static RuntimeList isa(RuntimeArray args, int ctx) { // Get the linearized inheritance hierarchy using C3 List linearizedClasses = InheritanceResolver.linearizeHierarchy(perlClassName); - // Normalize the argument: main::Foo -> Foo, ::Foo -> Foo + // Normalize the argument: main::Foo -> Foo, ::Foo -> Foo, Foo'Bar -> Foo::Bar // This is needed because isa("main::Foo") should match a class blessed as "Foo" String normalizedArg = argString; + // First normalize old-style ' separator to :: + normalizedArg = NameNormalizer.normalizePackageName(normalizedArg); if (normalizedArg.startsWith("main::")) { normalizedArg = normalizedArg.substring(6); } else if (normalizedArg.startsWith("::")) { diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java b/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java index 5091c3c28..1c5eb5906 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java +++ b/src/main/java/org/perlonjava/runtime/regex/RegexFlags.java @@ -18,11 +18,12 @@ * @param isDotAll s flag - dot matches all characters including newline * @param isExtended x flag - ignore whitespace and # comments in pattern * @param preservesMatch p flag - preserve match after failed matches + * @param isUnicode u flag - Unicode semantics (\w, \d, \s match Unicode) */ public record RegexFlags(boolean isGlobalMatch, boolean keepCurrentPosition, boolean isNonDestructive, boolean isMatchExactlyOnce, boolean useGAssertion, boolean isExtendedWhitespace, boolean isNonCapturing, boolean isOptimized, boolean isCaseInsensitive, boolean isMultiLine, - boolean isDotAll, boolean isExtended, boolean preservesMatch) { + boolean isDotAll, boolean isExtended, boolean preservesMatch, boolean isUnicode) { public static RegexFlags fromModifiers(String modifiers, String patternString) { return new RegexFlags( @@ -38,7 +39,8 @@ public static RegexFlags fromModifiers(String modifiers, String patternString) { modifiers.contains("m"), modifiers.contains("s"), modifiers.contains("x"), - modifiers.contains("p") + modifiers.contains("p"), + modifiers.contains("u") ); } @@ -63,6 +65,12 @@ public static void validateModifiers(String modifiers) { public int toPatternFlags() { int flags = 0; + + // /u flag enables Unicode semantics for \w, \d, \s + if (isUnicode) { + flags |= UNICODE_CHARACTER_CLASS; + } + if (isCaseInsensitive) { // For proper Unicode case-insensitive matching, we need both flags: // - CASE_INSENSITIVE: enables case-insensitive matching @@ -89,6 +97,7 @@ public RegexFlags with(String positiveFlags, String negativeFlags) { boolean newIsDotAll = this.isDotAll; boolean newIsExtended = this.isExtended; boolean newPreservesMatch = this.preservesMatch; + boolean newIsUnicode = this.isUnicode; // Handle positive flags if (positiveFlags.indexOf('n') >= 0) newFlagN = true; @@ -97,6 +106,7 @@ public RegexFlags with(String positiveFlags, String negativeFlags) { if (positiveFlags.indexOf('s') >= 0) newIsDotAll = true; if (positiveFlags.indexOf('x') >= 0) newIsExtended = true; if (positiveFlags.indexOf('p') >= 0) newPreservesMatch = true; + if (positiveFlags.indexOf('u') >= 0) newIsUnicode = true; // Handle negative flags if (negativeFlags.indexOf('n') >= 0) newFlagN = false; @@ -104,6 +114,7 @@ public RegexFlags with(String positiveFlags, String negativeFlags) { if (negativeFlags.indexOf('m') >= 0) newIsMultiLine = false; if (negativeFlags.indexOf('s') >= 0) newIsDotAll = false; if (negativeFlags.indexOf('x') >= 0) newIsExtended = false; + if (negativeFlags.indexOf('u') >= 0) newIsUnicode = false; return new RegexFlags( this.isGlobalMatch, @@ -118,7 +129,8 @@ public RegexFlags with(String positiveFlags, String negativeFlags) { newIsMultiLine, newIsDotAll, newIsExtended, - newPreservesMatch + newPreservesMatch, + newIsUnicode ); } @@ -133,6 +145,7 @@ public String toFlagString() { if (isNonCapturing) flagString.append('n'); if (isExtended) flagString.append('x'); if (isNonDestructive) flagString.append('r'); + if (isUnicode) flagString.append('u'); return flagString.toString(); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java index 3a3f7ef80..d927a5e57 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java @@ -96,11 +96,14 @@ public Set> entrySet() { // Process each key to extract the namespace part Set uniqueKeys = new HashSet<>(); // Set to track unique keys + boolean isMainStash = "main::".equals(namespace); for (String key : allKeys) { + String entryKey = null; + String globName = null; + if (key.startsWith(namespace)) { String remainingKey = key.substring(namespace.length()); int nextSeparatorIndex = remainingKey.indexOf("::"); - String entryKey; if (nextSeparatorIndex == -1) { entryKey = remainingKey; } else { @@ -108,23 +111,38 @@ public Set> entrySet() { // (e.g. "Foo::" not "Foo") - this is how Perl indicates sub-packages entryKey = remainingKey.substring(0, nextSeparatorIndex + 2); } - - // Special sort variables should not show up in stash enumeration - if (entryKey.equals("a") || entryKey.equals("b")) { - continue; + // entryKey already includes "::" for nested packages + globName = namespace + entryKey; + } else if (isMainStash) { + // For %main::, also include top-level packages that aren't explicitly + // prefixed with "main::". In Perl, $Foo::x and $main::Foo::x are the same. + // Variables in top-level packages are stored as "Foo::x", not "main::Foo::x". + int separatorIndex = key.indexOf("::"); + if (separatorIndex > 0) { + // This is a top-level package (like "Foo::test") + // Extract "Foo::" as the entry key + entryKey = key.substring(0, separatorIndex + 2); + // The glob name is the original key prefix + globName = entryKey; } + } - if (entryKey.isEmpty()) { - continue; - } + if (entryKey == null) { + continue; + } - // entryKey already includes "::" for nested packages - String globName = namespace + entryKey; + // Special sort variables should not show up in stash enumeration + if (entryKey.equals("a") || entryKey.equals("b")) { + continue; + } - // Add the entry only if it's not already in the set of unique keys - if (uniqueKeys.add(entryKey)) { - entries.add(new SimpleEntry<>(entryKey, new RuntimeStashEntry(globName, true))); - } + if (entryKey.isEmpty()) { + continue; + } + + // Add the entry only if it's not already in the set of unique keys + if (uniqueKeys.add(entryKey)) { + entries.add(new SimpleEntry<>(entryKey, new RuntimeStashEntry(globName, true))); } } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/NameNormalizer.java b/src/main/java/org/perlonjava/runtime/runtimetypes/NameNormalizer.java index 7af572976..04cd69378 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/NameNormalizer.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/NameNormalizer.java @@ -183,6 +183,24 @@ public static String moduleToFilename(String moduleName) { return moduleName.replace("::", "/") + ".pm"; } + /** + * Normalizes a package name by converting old-style single-quote separator to '::'. + * In Perl, "Foo'Bar" is equivalent to "Foo::Bar". + * + * @param packageName The package name to normalize. + * @return The normalized package name with '::' separators. + */ + public static String normalizePackageName(String packageName) { + if (packageName == null || packageName.isEmpty()) { + return packageName; + } + // Replace old-style ' separator with :: + if (packageName.indexOf('\'') >= 0) { + return packageName.replace("'", "::"); + } + return packageName; + } + /** * Composite key for name cache to avoid string concatenation overhead. * Using a record provides efficient hashCode/equals with no allocation. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlCompilerException.java b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlCompilerException.java index 47a556cd3..135c5ff7e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlCompilerException.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlCompilerException.java @@ -67,7 +67,7 @@ private static String buildErrorMessage(String message) { } String fileName = caller.elements.get(1).toString(); int line = ((RuntimeScalar) caller.elements.get(2)).getInt(); - return message + " at " + fileName + " line " + line + "\n"; + return message + " at " + fileName + " line " + line + ".\n"; } catch (Throwable t) { // caller() failed (e.g. mid-exception in interpreter) — use bare message return message + "\n";