From 9872bbcad591ea594548f0c93f09aac56c80c4b8 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Mon, 7 Sep 2026 16:50:04 +0200 Subject: [PATCH] fix: support reference-alias assignment targets in interpreter Lower parenthesized reference-alias assignments directly in both execution backends and replace package scalar slots when an alias target is global. Add regression coverage for persistent array-element aliases. Fixes #1247 Generated with [Codex](https://openai.com/codex/) Co-Authored-By: Codex --- .../backend/bytecode/BytecodeInterpreter.java | 7 ++ .../backend/bytecode/CompileAssignment.java | 79 ++++++++++++++++++- .../backend/bytecode/Disassemble.java | 6 ++ .../perlonjava/backend/bytecode/Opcodes.java | 2 + .../perlonjava/backend/jvm/EmitVariable.java | 71 +++++++++++++++++ .../interpreter_our_undef_list_placeholder.t | 11 +++ 6 files changed, 174 insertions(+), 2 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index d78efa2f3..e31c2ce78 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -803,6 +803,13 @@ private static RuntimeList execute(SuspendedInterpreterFrame frame) { .aliasLvalueReference(registers[reference].getFirst()); } + case Opcodes.ALIAS_GLOBAL_SCALAR -> { + int nameIdx = bytecode[pc++]; + int scalarReg = bytecode[pc++]; + GlobalVariable.aliasGlobalVariable(code.stringPool[nameIdx], + registers[scalarReg].getFirst()); + } + case Opcodes.LOAD_CONST -> { // Load from constant pool: rd = constants[index] int rd = bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 624cab6a6..d286e5ba5 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -126,6 +126,63 @@ private static int compileRhs(BytecodeCompiler bc, Node rhs, int context) { return bc.lastResultReg; } + /** Compile a parenthesized reference-alias assignment element by element. */ + private static boolean compileReferenceAliasListAssignment( + BytecodeCompiler bc, BinaryOperatorNode node) { + if (!(node.left instanceof OperatorNode referenceOp) + || !referenceOp.operator.equals("\\") + || !(referenceOp.operand instanceof ListNode targets) + || targets.elements.size() <= 1) { + return false; + } + if (!bc.symbolTable.isFeatureCategoryEnabled("refaliasing")) { + bc.throwCompilerException("Experimental aliasing via reference not enabled"); + return true; + } + + // A reference to a parenthesized list produces one reference per + // target. SET_FROM_LIST would copy values, rather than replace slots. + int rhsReg = compileRhs(bc, node.right, RuntimeContextType.LIST); + int rhsListReg = bc.allocateRegister(); + bc.emit(Opcodes.SCALAR_TO_LIST); + bc.emitReg(rhsListReg); + bc.emitReg(rhsReg); + + for (int i = 0; i < targets.elements.size(); i++) { + Node target = targets.elements.get(i); + if (!(target instanceof BinaryOperatorNode element) + || !(element.operator.equals("[") || element.operator.equals("{"))) { + bc.throwCompilerException("Assignment to unsupported ref aliasing target"); + return true; + } + if (element.operator.equals("[") + && element.left instanceof OperatorNode arrayOp + && arrayOp.operator.equals("$") + && arrayOp.operand instanceof IdentifierNode) { + bc.handleArrayElementLvalueAccess(element, arrayOp); + } else { + bc.compileNode(element, -1, RuntimeContextType.LVALUE); + } + int targetReg = bc.lastResultReg; + + int indexReg = bc.allocateRegister(); + bc.emit(Opcodes.LOAD_INT); + bc.emitReg(indexReg); + bc.emit(i); + int referenceReg = bc.allocateRegister(); + bc.emit(Opcodes.ARRAY_GET); + bc.emitReg(referenceReg); + bc.emitReg(rhsListReg); + bc.emitReg(indexReg); + + bc.emit(Opcodes.ALIAS_LVALUE_REFERENCE); + bc.emitReg(targetReg); + bc.emitReg(referenceReg); + } + bc.lastResultReg = rhsListReg; + return true; + } + private static boolean handleLocalAssignment(BytecodeCompiler bc, BinaryOperatorNode node, OperatorNode leftOp, int rhsContext) { if (!leftOp.operator.equals("local")) return false; Node localOperand = leftOp.operand; @@ -641,6 +698,10 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, return; } + if (compileReferenceAliasListAssignment(bytecodeCompiler, node)) { + return; + } + // Special case: my $x = value if (node.left instanceof OperatorNode leftOp) { if (leftOp.operator.equals("my") || leftOp.operator.equals("state")) { @@ -1625,7 +1686,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, return; } - if (bytecodeCompiler.hasVariable(varName)) { + if (bytecodeCompiler.hasVariable(varName) && !bytecodeCompiler.isOurVariable(varName)) { int targetReg = bytecodeCompiler.getVariableRegister(varName); int derefReg = bytecodeCompiler.allocateRegister(); switch (varNode.operator) { @@ -1642,7 +1703,21 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, bytecodeCompiler.emitReg(derefReg); bytecodeCompiler.lastResultReg = targetReg; } else { - bytecodeCompiler.throwCompilerException("Variable " + varName + " not found for ref aliasing"); + String globalName = NameNormalizer.normalizeVariableName( + varName.substring(1), bytecodeCompiler.getCurrentPackage()); + int derefReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_SCALAR_STRICT, node.getIndex()); + bytecodeCompiler.emitReg(derefReg); + bytecodeCompiler.emitReg(valueReg); + int nameIdx = bytecodeCompiler.addToStringPool(globalName); + bytecodeCompiler.emit(Opcodes.ALIAS_GLOBAL_SCALAR); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.emitReg(derefReg); + int targetReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_SCALAR); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.lastResultReg = targetReg; } } else { bytecodeCompiler.throwCompilerException("Assignment to unsupported ref aliasing target: " + leftOp.operator); diff --git a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java index f77b3c2f8..c4ab378cc 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java @@ -224,6 +224,12 @@ public static String disassemble(InterpretedCode interpretedCode) { sb.append("ALIAS_LVALUE_REFERENCE r").append(rd) .append(" <- r").append(src).append("\n"); break; + case Opcodes.ALIAS_GLOBAL_SCALAR: + int globalAliasNameIdx = interpretedCode.bytecode[pc++]; + src = interpretedCode.bytecode[pc++]; + sb.append("ALIAS_GLOBAL_SCALAR ").append(interpretedCode.stringPool[globalAliasNameIdx]) + .append(" <- r").append(src).append("\n"); + break; case Opcodes.ASSIGN_LEXICAL_SCALAR: rd = interpretedCode.bytecode[pc++]; src = 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 fdcf2ee80..f609b6586 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -2568,6 +2568,8 @@ public class Opcodes { /** Array-element lvalue fetch. Format: ARRAY_GET_LVALUE rd arrayReg indexReg. */ public static final short ARRAY_GET_LVALUE = 542; + /** Alias a package scalar slot to a scalar. Format: ALIAS_GLOBAL_SCALAR nameIdx scalarReg. */ + public static final short ALIAS_GLOBAL_SCALAR = 546; /** Preallocate hash buckets for {@code keys %hash = CAPACITY}. Format: hashReg capacityReg. */ public static final short HASH_PREALLOCATE = 543; diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java index b54813db4..766589588 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java @@ -820,6 +820,11 @@ static void handleAssignOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo } } + if (isReferenceAliasListAssignment(node.left)) { + emitReferenceAliasListAssignment(emitterVisitor, node); + break; + } + // The left value can be a variable, an operator or a subroutine call: // `pos`, `substr`, `vec`, `sub :lvalue` @@ -1002,6 +1007,23 @@ static void handleAssignOperator(EmitterVisitor emitterVisitor, BinaryOperatorNo ctx.javaClassInfo.releaseSpillSlot(); } break; + } else if ((symEntry == null || symEntry.decl().equals("our")) && varNode.operator.equals("$")) { + String globalName = NameNormalizer.normalizeVariableName( + varName.substring(1), ctx.symbolTable.getCurrentPackage()); + mv.visitLdcInsn(globalName); + mv.visitVarInsn(Opcodes.ALOAD, rhsSlot); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/runtimetypes/RuntimeScalar", "scalarDeref", + "()Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/GlobalVariable", "aliasGlobalVariable", + "(Ljava/lang/String;Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)V", false); + mv.visitLdcInsn(globalName); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/GlobalVariable", "getGlobalVariable", + "(Ljava/lang/String;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); + if (pooledRhs) ctx.javaClassInfo.releaseSpillSlot(); + break; } } // Fall through for unsupported ref aliasing targets (global vars, etc.) @@ -1140,6 +1162,55 @@ private static boolean isScalarLvalueTarget(Node node) { || (binop.right instanceof BinaryOperatorNode call && call.operator.equals("(")); } + private static boolean isReferenceAliasListAssignment(Node left) { + return left instanceof OperatorNode referenceOp + && referenceOp.operator.equals("\\") + && referenceOp.operand instanceof ListNode targets + && targets.elements.size() > 1; + } + + /** Emits \(TARGETS) = \(REFERENTS) without collapsing the RHS list to scalar context. */ + private static void emitReferenceAliasListAssignment(EmitterVisitor emitterVisitor, BinaryOperatorNode node) { + EmitterContext ctx = emitterVisitor.ctx; + MethodVisitor mv = ctx.mv; + ListNode targets = (ListNode) ((OperatorNode) node.left).operand; + if (!ctx.symbolTable.isFeatureCategoryEnabled("refaliasing")) { + throw new PerlCompilerException(node.tokenIndex, "Experimental aliasing via reference not enabled", ctx.errorUtil); + } + + node.right.accept(emitterVisitor.with(RuntimeContextType.LIST)); + int rhsListSlot = ctx.javaClassInfo.acquireSpillSlot(); + boolean pooledRhsList = rhsListSlot >= 0; + if (!pooledRhsList) rhsListSlot = ctx.symbolTable.allocateLocalVariable(); + mv.visitVarInsn(Opcodes.ASTORE, rhsListSlot); + + for (int i = 0; i < targets.elements.size(); i++) { + Node target = targets.elements.get(i); + if (!(target instanceof BinaryOperatorNode targetElement) + || !(targetElement.operator.equals("[") || targetElement.operator.equals("{"))) { + throw new PerlCompilerException(node.tokenIndex, + "Assignment to unsupported ref aliasing target", ctx.errorUtil); + } + if (targetElement.operator.equals("[")) { + Dereference.handleArrayElementOperator( + emitterVisitor.with(RuntimeContextType.LVALUE), targetElement, "getLvalue"); + } else { + targetElement.accept(emitterVisitor.with(RuntimeContextType.LVALUE)); + } + mv.visitVarInsn(Opcodes.ALOAD, rhsListSlot); + mv.visitFieldInsn(Opcodes.GETFIELD, + "org/perlonjava/runtime/runtimetypes/RuntimeList", "elements", "Ljava/util/List;"); + mv.visitLdcInsn(i); + mv.visitMethodInsn(Opcodes.INVOKEINTERFACE, "java/util/List", "get", "(I)Ljava/lang/Object;", true); + mv.visitTypeInsn(Opcodes.CHECKCAST, "org/perlonjava/runtime/runtimetypes/RuntimeScalar"); + mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, + "org/perlonjava/runtime/runtimetypes/RuntimeScalar", "aliasLvalueReference", + "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false); + if (i < targets.elements.size() - 1) mv.visitInsn(Opcodes.POP); + } + if (pooledRhsList) ctx.javaClassInfo.releaseSpillSlot(); + } + /** * Emits a scalar assignment where the LHS is a ternary operator with at least one * branch that is a LIST assignment expression. diff --git a/src/test/resources/unit/interpreter_our_undef_list_placeholder.t b/src/test/resources/unit/interpreter_our_undef_list_placeholder.t index 6722cb590..a87fe083b 100644 --- a/src/test/resources/unit/interpreter_our_undef_list_placeholder.t +++ b/src/test/resources/unit/interpreter_our_undef_list_placeholder.t @@ -94,6 +94,17 @@ is($multidimensional_hash{"left\034right"}, 'combined', 'declared-reference scalar alias writes through its source'); } +{ + use feature 'refaliasing'; + no warnings 'experimental::refaliasing'; + my @source = qw(left right); + my @target = qw(one two); + \($target[0], $target[1]) = \($source[1], $source[0]); + $target[0] = 'updated'; + is_deeply(\@source, ['left', 'updated'], + 'parenthesized array-element reference aliases replace their matching slots'); +} + { no warnings 'numeric'; is('a' x ('Inf' + 0), '', 'infinite positive repeat count is empty');