Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
71 changes: 67 additions & 4 deletions dev/design/moo_support.md
Original file line number Diff line number Diff line change
Expand Up @@ -547,13 +547,76 @@ Moo tests run via `jcpan -t Moo`. Recent fixes (Phases 12-13) should improve pas
- This ensures the actual hash/array container is passed to `undefine()`, not a scalar
- ExifTool PDF.t now passes all 26 tests (was 7/26)

### Next Steps
- [x] Phase 22: Fix stash keys for nested packages and overload &{} handling (2026-03-15)
- **Two fixes in this phase**:

- **Fix 1: Stash keys for nested packages must include trailing `::`**
- Root cause: `keys %Foo::Bar::` was returning `Baz` instead of `Baz::` for nested packages
- This broke `Role::Tiny::_load_module` which uses `grep !/::\z/, keys %{_getstash($module)}`
to detect if a module's stash has actual symbols vs just sub-package markers
- **HashSpecialVariable.java fix**:
- Changed `entryKey = remainingKey.substring(0, nextSeparatorIndex)`
to `entryKey = remainingKey.substring(0, nextSeparatorIndex + 2)`
- Now stash keys correctly include `::` suffix for sub-packages
- Fixes t/load_module_role_tiny.t (0/2 → 2/2)

- **Fix 2: \&{$blessed_obj} must throw "Not a subroutine reference" for objects without &{} overload**
- Root cause: `\&{$obj}` on a blessed object without `&{}` overload was creating a symbolic
reference instead of throwing an error
- In Perl: `\&{bless({}, "Foo")}` throws "Not a subroutine reference"
- In PerlOnJava: was creating CODE ref pointing to non-existent `&Foo=HASH(...)` sub
- **RuntimeCode.java fix (createCodeReference)**:
- Check `blessedId()`: negative = blessed with overload, positive = blessed without, 0 = not blessed
- For `blessId != 0` (blessed), try `&{}` overload if available
- If no `&{}` overload exists, throw "Not a subroutine reference"
- Also added check for unblessed REFERENCE types to throw same error
- Fixes t/method-generate-accessor.t (41/49 → 46/49), t/coerce-1.t (0/2 → 2/2)

- [x] Phase 23: Fix `local @_` in string eval (2026-03-15)
- Root cause: `local @_ = (...)` in string eval was localizing `@main::_` (global) instead of
register 1 which holds the actual `@_` for the subroutine
- In PerlOnJava, `@_` in a subroutine (register 1) and `@main::_` are different arrays
- When compiling `local @_ = (...)`, the compiler was emitting:
- `LOAD_GLOBAL_ARRAY @main::_` followed by `PUSH_LOCAL_VARIABLE` and assignment
- For reserved variables like `@_`, we need to use register-based localization:
- `PUSH_LOCAL_VARIABLE r1` to save register 1's state
- `ARRAY_SET_FROM_LIST r1` to assign new values
- **CompileAssignment.java fix**:
- In `handleLocalAssignment`, check `bc.isReservedVariable(varName)` for `@` case
- If reserved, use `PUSH_LOCAL_VARIABLE` on the register directly instead of loading global
- **BytecodeCompiler.java fix**:
- Similar fix for standalone `local @_` without assignment
- This fixes Sub::Quote's `local @_ = ($value)` inlinification pattern
- Fixes t/method-generate-accessor.t (46/49 → 49/49)

### Current Status

**Test Results (after Phase 23):**
- 62/71 test programs passing (87%)
- ~768/829 subtests passing (93%)

1. **Fix no-moo.t cleanup** - `no Moo` should remove `extends`, `has`, etc. from namespace
**Remaining Failures (categorized):**
1. **accessor-weaken tests** (20 failures) - Expected, weak references not supported in Java GC
2. **croak-locations.t** (29 failures) - Carp reports `(eval N)` instead of actual filename
3. **demolish tests** (6 failures) - Expected, DESTROY not supported
4. **moo-utils-_subname-Sub-Name.t** (1 failure) - Expected, we have Sub::Util (no fallback to Sub::Name)
5. **no-moo.t** (5 failures) - Namespace cleanup requires weak references
6. **overloaded-coderefs.t** - Expected, B::Deparse not available

**Expected failures** (not fixable without fundamental changes):
- Weak references: accessor-weaken tests (20), no-moo.t cleanup (5)
- DESTROY/GC: demolish tests (6)
- Missing B::Deparse: overloaded-coderefs.t
- Sub::Name fallback: moo-utils-_subname-Sub-Name.t (1)

**Potentially fixable**:
- croak-locations.t (29) - Carp filename in string eval

### Next Steps

2. **Prototype checking** - `$$` prototype should accept `@array` argument (workaround: removed prototype)
1. **Investigate croak-locations.t** - Carp reports `(eval N)` instead of actual filename

3. **DEMOLISH support** - Expected to remain unsupported (requires DESTROY/GC hooks)
2. **DEMOLISH support** - Expected to remain unsupported (requires DESTROY/GC hooks)

### PR Information
- **Branch**: `feature/moo-support` (PR #319 - merged)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3068,6 +3068,33 @@ void compileVariableDeclaration(OperatorNode node, String op) {
boolean isDeclaredReference = node.annotations != null &&
Boolean.TRUE.equals(node.annotations.get("isDeclaredReference"));

// For reserved variables like @_, use register-based localization
// because register 1 holds @_ which is different from @main::_
if (isReservedVariable(varName)) {
int regIdx = getVariableRegister(varName);

// Emit PUSH_LOCAL_VARIABLE to save register state
emit(Opcodes.PUSH_LOCAL_VARIABLE);
emitReg(regIdx);

// The result is the register itself (for assignment)
if (isDeclaredReference && currentCallContext != RuntimeContextType.VOID) {
int refReg1 = allocateRegister();
emit(Opcodes.CREATE_REF);
emitReg(refReg1);
emitReg(regIdx);

int refReg2 = allocateRegister();
emit(Opcodes.CREATE_REF);
emitReg(refReg2);
emitReg(refReg1);
lastResultReg = refReg2;
} else {
lastResultReg = regIdx;
}
return;
}

String globalVarName = NameNormalizer.normalizeVariableName(idNode.name, getCurrentPackage());
int nameIdx = addToStringPool(globalVarName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,17 @@ private static boolean handleLocalAssignment(BytecodeCompiler bc, BinaryOperator
bc.emitReg(valueReg);
}
case "@" -> {
// For reserved variables like @_, use register-based localization
if (bc.isReservedVariable(varName)) {
int regIdx = bc.getVariableRegister(varName);
bc.emit(Opcodes.PUSH_LOCAL_VARIABLE);
bc.emitReg(regIdx);
bc.emit(Opcodes.ARRAY_SET_FROM_LIST);
bc.emitReg(regIdx);
bc.emitReg(valueReg);
bc.lastResultReg = regIdx;
return true;
}
bc.emit(Opcodes.LOAD_GLOBAL_ARRAY);
bc.emitReg(localReg);
bc.emit(nameIdx);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ public final class Configuration {
* Automatically populated by Gradle/Maven during build.
* DO NOT EDIT MANUALLY - this value is replaced at build time.
*/
public static final String gitCommitId = "e78cba6d4";
public static final String gitCommitId = "a89a23a58";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ public class GlobalVariable {
// to Dst:: symbols can still point to their original objects.
static final Map<String, String> stashAliases = new HashMap<>();

// Glob aliasing: `*a = *b` makes a and b share the same glob.
// Maps glob names to their canonical (target) name.
// When looking up or assigning to glob slots, we resolve through this map.
static final Map<String, String> globAliases = new HashMap<>();

// Flags used by operator override
// globalGlobs: Tracks typeglob assignments (e.g., *CORE::GLOBAL::hex = sub {...})
// Used to detect when built-in operators have been globally overridden
Expand All @@ -66,6 +71,7 @@ public static void resetAllGlobals() {
globalGlobs.clear();
isSubs.clear();
stashAliases.clear();
globAliases.clear();
clearPackageCache();

RuntimeCode.clearCaches();
Expand Down Expand Up @@ -105,6 +111,49 @@ public static String resolveStashAlias(String namespace) {
return aliased;
}

/**
* Sets a glob alias. After `*a = *b`, calling setGlobAlias("a", "b") makes
* all slot assignments to "a" also affect "b" and vice versa.
*/
public static void setGlobAlias(String fromGlob, String toGlob) {
// Find the canonical name for toGlob (in case it's already an alias)
String canonical = resolveGlobAlias(toGlob);
globAliases.put(fromGlob, canonical);
// Also ensure toGlob points to the canonical name
if (!toGlob.equals(canonical)) {
globAliases.put(toGlob, canonical);
}
}

/**
* Resolves a glob name to its canonical name.
* If the glob is aliased, returns the target name; otherwise returns the input.
*/
public static String resolveGlobAlias(String globName) {
String aliased = globAliases.get(globName);
if (aliased != null) {
// Follow the chain in case of multiple aliases
return resolveGlobAlias(aliased);
}
return globName;
}

/**
* Gets all glob names that are aliased to the same canonical name.
* This is used when assigning to a glob slot - we need to update all aliases.
*/
public static java.util.List<String> getGlobAliasGroup(String globName) {
String canonical = resolveGlobAlias(globName);
java.util.List<String> group = new java.util.ArrayList<>();
group.add(canonical);
for (Map.Entry<String, String> entry : globAliases.entrySet()) {
if (resolveGlobAlias(entry.getKey()).equals(canonical) && !group.contains(entry.getKey())) {
group.add(entry.getKey());
}
}
return group;
}

/**
* Retrieves a global variable by its key, initializing it if necessary.
* If the key matches a regex capture variable pattern, it initializes a special variable.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -104,9 +104,9 @@ public Set<Entry<String, RuntimeScalar>> entrySet() {
if (nextSeparatorIndex == -1) {
entryKey = remainingKey;
} else {
// Stash keys for nested packages are reported without the trailing "::"
// (e.g. "Foo" instead of "Foo::")
entryKey = remainingKey.substring(0, nextSeparatorIndex);
// Stash keys for nested packages include the trailing "::"
// (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
Expand All @@ -118,9 +118,8 @@ public Set<Entry<String, RuntimeScalar>> entrySet() {
continue;
}

String globName = (nextSeparatorIndex == -1)
? (namespace + entryKey)
: (namespace + entryKey + "::");
// entryKey already includes "::" for nested packages
String globName = namespace + entryKey;

// Add the entry only if it's not already in the set of unique keys
if (uniqueKeys.add(entryKey)) {
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java
Original file line number Diff line number Diff line change
Expand Up @@ -1922,6 +1922,42 @@ public static RuntimeScalar createCodeReference(RuntimeScalar runtimeScalar, Str
return runtimeScalar;
}

// Check if object is eligible for &{} overloading (e.g., blessed object with &{} operator)
// This handles cases like \&{$constraint_obj} where $constraint_obj overloads &{}
// blessId: negative = blessed with overload, positive = blessed without overload, 0 = not blessed
int blessId = blessedId(runtimeScalar);
// System.err.println("DEBUG createCodeReference: type=" + runtimeScalar.type + " blessId=" + blessId + " value=" + runtimeScalar.value);
if (blessId != 0) {
// Object is blessed
if (blessId < 0) {
// Has overloading - try to get &{} overload
OverloadContext ctx = OverloadContext.prepare(blessId);
if (ctx != null) {
RuntimeScalar result = ctx.tryOverload("(&{}", new RuntimeArray(runtimeScalar));
if (result != null && result.value.hashCode() != runtimeScalar.value.hashCode()) {
// Successfully got a CODE reference via overload, return it
if (result.type == RuntimeScalarType.CODE) {
return result;
}
// Recursively handle if not CODE yet
return createCodeReference(result, packageName);
}
}
}
// Blessed reference without &{} overload - this is an error in Perl
// "Not a subroutine reference"
throw new PerlCompilerException("Not a subroutine reference");
}

// Check if this is a reference type that isn't CODE - error "Not a subroutine reference"
// This catches cases like \&{$hashref} where $hashref is an unblessed reference
if (runtimeScalar.type == RuntimeScalarType.REFERENCE) {
RuntimeScalar deref = (RuntimeScalar) runtimeScalar.value;
if (deref.type != RuntimeScalarType.CODE) {
throw new PerlCompilerException("Not a subroutine reference");
}
}

String name = NameNormalizer.normalizeVariableName(runtimeScalar.toString(), packageName);
// System.out.println("Creating code reference: " + name + " got: " + GlobalContext.getGlobalCodeRef(name));
RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRef(name);
Expand Down
23 changes: 19 additions & 4 deletions src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java
Original file line number Diff line number Diff line change
Expand Up @@ -106,14 +106,20 @@ public RuntimeScalar set(RuntimeScalar value) {
case ARRAYREFERENCE:
// Handle the case where a typeglob is assigned a reference to an array
// `*foo = \@bar` creates an alias - both names refer to the same array
// Also update all glob aliases
if (value.value instanceof RuntimeArray arr) {
GlobalVariable.globalArrays.put(this.globName, arr);
for (String aliasedName : GlobalVariable.getGlobAliasGroup(this.globName)) {
GlobalVariable.globalArrays.put(aliasedName, arr);
}
}
return value;
case HASHREFERENCE:
// `*foo = \%bar` creates an alias - both names refer to the same hash
// Also update all glob aliases
if (value.value instanceof RuntimeHash hash) {
GlobalVariable.globalHashes.put(this.globName, hash);
for (String aliasedName : GlobalVariable.getGlobAliasGroup(this.globName)) {
GlobalVariable.globalHashes.put(aliasedName, hash);
}
}
return value;
case REFERENCE:
Expand All @@ -127,10 +133,16 @@ public RuntimeScalar set(RuntimeScalar value) {
InheritanceResolver.invalidateCache();
} else if (deref.type == RuntimeScalarType.ARRAYREFERENCE && deref.value instanceof RuntimeArray arr) {
// `*foo = \@bar` assigns to the ARRAY slot.
GlobalVariable.globalArrays.put(this.globName, arr);
// Also update all glob aliases
for (String aliasedName : GlobalVariable.getGlobAliasGroup(this.globName)) {
GlobalVariable.globalArrays.put(aliasedName, arr);
}
} else if (deref.type == RuntimeScalarType.HASHREFERENCE && deref.value instanceof RuntimeHash hash) {
// `*foo = \%bar` assigns to the HASH slot.
GlobalVariable.globalHashes.put(this.globName, hash);
// Also update all glob aliases
for (String aliasedName : GlobalVariable.getGlobAliasGroup(this.globName)) {
GlobalVariable.globalHashes.put(aliasedName, hash);
}
} else if (value.type == RuntimeScalarType.REFERENCE && deref.type == RuntimeScalarType.ARRAYREFERENCE) {
// `*foo = \$array_ref` creates a constant subroutine returning the array reference
RuntimeCode constSub = new RuntimeCode("", null);
Expand Down Expand Up @@ -205,6 +217,9 @@ public RuntimeScalar set(RuntimeGlob value) {
return value.scalar();
}

// Register glob alias so future slot assignments affect both globs
GlobalVariable.setGlobAlias(this.globName, value.globName);

// Retrieve the RuntimeScalar value associated with the provided RuntimeGlob.
RuntimeScalar result = value.scalar();

Expand Down
Loading