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
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,8 @@ static boolean isImmutableProxy(RuntimeBase val) {
private static boolean lexicalAssignmentMustPreserveSlot(RuntimeBase val) {
if (!(val instanceof RuntimeScalar scalar)) return false;
return scalar instanceof ReadOnlyAlias
|| scalar.captureCount > 0
|| scalar.captureRefCountOwned > 0
|| scalar.type == RuntimeScalarType.TIED_SCALAR
|| scalar.type == RuntimeScalarType.READONLY_SCALAR;
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import org.perlonjava.frontend.analysis.ConstantFoldingVisitor;
import org.perlonjava.frontend.analysis.LValueVisitor;
import org.perlonjava.frontend.astnode.*;
import org.perlonjava.frontend.semantic.SymbolTable;
import org.perlonjava.runtime.runtimetypes.NameNormalizer;
import org.perlonjava.runtime.runtimetypes.RuntimeCode;
import org.perlonjava.runtime.runtimetypes.RuntimeContextType;
Expand Down Expand Up @@ -822,7 +823,25 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler,
if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode) {
String varName = "$" + ((IdentifierNode) leftOp.operand).name;

if (bytecodeCompiler.hasVariable(varName)) {
if (bytecodeCompiler.hasVariable(varName) && bytecodeCompiler.isOurVariable(varName)) {
SymbolTable.SymbolEntry ourEntry = bytecodeCompiler.symbolTable.getSymbolEntry(varName);
String ourPkg = (ourEntry != null && ourEntry.perlPackage() != null)
? ourEntry.perlPackage()
: bytecodeCompiler.getCurrentPackage();
String bareVarName = varName.substring(1);
String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, ourPkg);
int nameIdx = bytecodeCompiler.addToStringPool(normalizedName);

bytecodeCompiler.emit(Opcodes.STORE_GLOBAL_SCALAR);
bytecodeCompiler.emit(nameIdx);
bytecodeCompiler.emitReg(valueReg);

int lvalReg = bytecodeCompiler.allocateRegister();
bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_SCALAR);
bytecodeCompiler.emitReg(lvalReg);
bytecodeCompiler.emit(nameIdx);
bytecodeCompiler.lastResultReg = lvalReg;
} else if (bytecodeCompiler.hasVariable(varName)) {
// Lexical variable - check if it's captured
int targetReg = bytecodeCompiler.getVariableRegister(varName);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -946,14 +946,23 @@ public static int executeCreateClosure(int[] bytecode, int pc, RuntimeBase[] reg
// Without this, scopeExitCleanup() doesn't know the variable is still alive
// via this closure, and may prematurely clear weak references to its value.
java.util.List<RuntimeScalar> capturedScalars = new java.util.ArrayList<>();
java.util.List<RuntimeBase> capturedAggregates = new java.util.ArrayList<>();
for (RuntimeBase captured : capturedVars) {
if (captured instanceof RuntimeScalar s) {
capturedScalars.add(s);
s.captureCount++;
s.retainClosureCapture();
} else if (captured instanceof RuntimeArray || captured instanceof RuntimeHash) {
capturedAggregates.add(captured);
captured.retainClosureCapture();
}
}
if (!capturedScalars.isEmpty()) {
closureCode.capturedScalars = capturedScalars.toArray(new RuntimeScalar[0]);
}
if (!capturedAggregates.isEmpty()) {
closureCode.capturedAggregates = capturedAggregates.toArray(new RuntimeBase[0]);
}
if (!capturedScalars.isEmpty() || !capturedAggregates.isEmpty()) {
closureCode.refCount = 0;
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@
import org.perlonjava.frontend.analysis.Visitor;
import org.perlonjava.frontend.astnode.*;

import java.util.ArrayDeque;
import java.util.HashSet;
import java.util.Deque;
import java.util.Set;

/**
Expand All @@ -18,6 +21,7 @@
public class VariableCollectorVisitor implements Visitor {
private final Set<String> variables;
private boolean hasEvalString = false;
private final Deque<Set<String>> localScopes = new ArrayDeque<>();

/**
* Create a new VariableCollectorVisitor.
Expand All @@ -26,6 +30,7 @@ public class VariableCollectorVisitor implements Visitor {
*/
public VariableCollectorVisitor(Set<String> variables) {
this.variables = variables;
this.localScopes.push(new HashSet<>());
}

/**
Expand All @@ -37,6 +42,71 @@ public boolean hasEvalString() {
return hasEvalString;
}

private boolean isDeclarationOperator(String op) {
return op.equals("my") || op.equals("state") || op.equals("our");
}

private boolean isVariableOperator(String op) {
return op.equals("$") || op.equals("@") || op.equals("%") || op.equals("&");
}

private boolean isDeclaredLocal(String varName) {
for (Set<String> scope : localScopes) {
if (scope.contains(varName)) return true;
}
return false;
}

private void declare(String varName) {
localScopes.peek().add(varName);
}

private void declareFrom(Node node) {
if (node == null) return;
if (node instanceof OperatorNode opNode) {
if (isDeclarationOperator(opNode.operator)) {
declareFrom(opNode.operand);
} else if (isVariableOperator(opNode.operator)
&& opNode.operand instanceof IdentifierNode idNode) {
declare(opNode.operator + idNode.name);
} else {
declareFrom(opNode.operand);
}
} else if (node instanceof ListNode listNode && listNode.elements != null) {
for (Node element : listNode.elements) {
declareFrom(element);
}
}
}

private boolean containsDeclaration(Node node) {
if (node == null) return false;
if (node instanceof OperatorNode opNode) {
return isDeclarationOperator(opNode.operator) || containsDeclaration(opNode.operand);
}
if (node instanceof ListNode listNode && listNode.elements != null) {
for (Node element : listNode.elements) {
if (containsDeclaration(element)) return true;
}
}
return false;
}

private void visitAssignmentTarget(Node node) {
if (node == null) return;
if (node instanceof OperatorNode opNode && isDeclarationOperator(opNode.operator)) {
declareFrom(opNode.operand);
return;
}
if (node instanceof ListNode listNode && listNode.elements != null) {
for (Node element : listNode.elements) {
visitAssignmentTarget(element);
}
return;
}
node.accept(this);
}

@Override
public void visit(IdentifierNode node) {
// Leaf node - nothing to traverse
Expand All @@ -54,16 +124,25 @@ public void visit(OperatorNode node) {
}

// Check if this is a variable reference (sigil + identifier)
if ((op.equals("$") || op.equals("@") || op.equals("%") || op.equals("&"))
&& node.operand instanceof IdentifierNode idNode) {
if (isDeclarationOperator(op)) {
declareFrom(node.operand);
return;
}

if (isVariableOperator(op) && node.operand instanceof IdentifierNode idNode) {
// This is a variable reference
String varName = op + idNode.name;
variables.add(varName);
if (!isDeclaredLocal(varName)) {
variables.add(varName);
}
}

// $#arr references @arr (array last index)
if (op.equals("$#") && node.operand instanceof IdentifierNode idNode) {
variables.add("@" + idNode.name);
String varName = "@" + idNode.name;
if (!isDeclaredLocal(varName)) {
variables.add(varName);
}
}

// Visit operand if it exists
Expand All @@ -74,17 +153,31 @@ public void visit(OperatorNode node) {

@Override
public void visit(BinaryOperatorNode node) {
if ("=".equals(node.operator) && containsDeclaration(node.left)) {
if (node.right != null) {
node.right.accept(this);
}
visitAssignmentTarget(node.left);
return;
}

// $a{key}, @a{keys}, %a{keys} all access hash %a
if ("{".equals(node.operator) && node.left instanceof OperatorNode leftOp
&& ("$".equals(leftOp.operator) || "@".equals(leftOp.operator) || "%".equals(leftOp.operator))
&& leftOp.operand instanceof IdentifierNode idNode) {
variables.add("%" + idNode.name);
String varName = "%" + idNode.name;
if (!isDeclaredLocal(varName)) {
variables.add(varName);
}
}
// $a[idx], @a[indices], %a[indices] all access array @a
if ("[".equals(node.operator) && node.left instanceof OperatorNode leftOp
&& ("$".equals(leftOp.operator) || "@".equals(leftOp.operator) || "%".equals(leftOp.operator))
&& leftOp.operand instanceof IdentifierNode idNode) {
variables.add("@" + idNode.name);
String varName = "@" + idNode.name;
if (!isDeclaredLocal(varName)) {
variables.add(varName);
}
}
if (node.left != null) {
node.left.accept(this);
Expand All @@ -96,13 +189,15 @@ public void visit(BinaryOperatorNode node) {

@Override
public void visit(BlockNode node) {
localScopes.push(new HashSet<>());
if (node.elements != null) {
for (Node element : node.elements) {
if (element != null) {
element.accept(this);
}
}
}
localScopes.pop();
}

@Override
Expand Down Expand Up @@ -153,22 +248,25 @@ public void visit(StringNode node) {

@Override
public void visit(For1Node node) {
if (node.variable != null) {
node.variable.accept(this);
}
localScopes.push(new HashSet<>());
if (node.list != null) {
node.list.accept(this);
}
if (node.variable != null) {
visitAssignmentTarget(node.variable);
}
if (node.body != null) {
node.body.accept(this);
}
if (node.continueBlock != null) {
node.continueBlock.accept(this);
}
localScopes.pop();
}

@Override
public void visit(For3Node node) {
localScopes.push(new HashSet<>());
if (node.initialization != null) {
node.initialization.accept(this);
}
Expand All @@ -189,6 +287,7 @@ public void visit(For3Node node) {
if (node.continueBlock != null) {
node.continueBlock.accept(this);
}
localScopes.pop();
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,8 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean
// or can use a minimal null-store fast path. See CleanupNeededVisitor
// and JavaClassInfo.cleanupNeeded. JPERL_FORCE_CLEANUP=1 bypasses the
// analysis (forces cleanupNeeded=true) as an escape hatch.
ctx.javaClassInfo.isLvalueSubroutine =
Boolean.TRUE.equals(ast.getAnnotation("subroutineIsLvalue"));
if (FORCE_CLEANUP) {
ctx.javaClassInfo.cleanupNeeded = true;
} else {
Expand Down Expand Up @@ -781,10 +783,11 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean
// Materialize it into a local slot immediately so all subsequent control-flow
// checks operate from locals and join points don't depend on operand stack shape.
mv.visitVarInsn(Opcodes.ILOAD, 2);
mv.visitInsn(ctx.javaClassInfo.isLvalueSubroutine ? Opcodes.ICONST_0 : Opcodes.ICONST_1);
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/runtimetypes/RuntimeCode",
"returnList",
"(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;I)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;",
"(Lorg/perlonjava/runtime/runtimetypes/RuntimeBase;IZ)Lorg/perlonjava/runtime/runtimetypes/RuntimeList;",
false);
mv.visitVarInsn(Opcodes.ASTORE, returnListSlot);

Expand Down
7 changes: 7 additions & 0 deletions src/main/java/org/perlonjava/backend/jvm/JavaClassInfo.java
Original file line number Diff line number Diff line change
Expand Up @@ -90,6 +90,13 @@ public class JavaClassInfo {
*/
public boolean isMapGrepBlock;

/**
* True when this generated method belongs to a subroutine with the
* {@code :lvalue} attribute. Non-lvalue return copying must be disabled
* for these subs so callers can assign through returned aliases.
*/
public boolean isLvalueSubroutine;

/**
* Counter tracking nesting depth inside finally blocks.
* Control flow statements (last, next, redo, return, goto) are prohibited in finally blocks.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ public SubroutineNode(String name, String prototype, List<String> attributes, No

if (block instanceof AbstractNode abstractNode) {
abstractNode.setAnnotation("blockIsSubroutine", true);
if (attributes != null && attributes.contains("lvalue")) {
abstractNode.setAnnotation("subroutineIsLvalue", true);
}
}
}

Expand All @@ -60,4 +63,3 @@ public void accept(Visitor visitor) {
visitor.visit(this);
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -1469,6 +1469,9 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S

Supplier<Void> subroutineCreationTaskSupplier = () -> {
// Try unified API (returns RuntimeCode - either CompiledCode or InterpretedCode)
if (placeholder.attributes != null && placeholder.attributes.contains("lvalue")) {
block.setAnnotation("subroutineIsLvalue", true);
}
RuntimeCode runtimeCode =
EmitterMethodCreator.createRuntimeCode(newCtx, block, false);

Expand Down Expand Up @@ -1599,14 +1602,24 @@ private static void installClosureCaptureMetadata(RuntimeCode code, List<Object>
}

ArrayList<RuntimeScalar> capturedScalars = new ArrayList<>();
ArrayList<RuntimeBase> capturedAggregates = new ArrayList<>();
for (Object value : capturedValues) {
if (value instanceof RuntimeScalar scalar) {
capturedScalars.add(scalar);
scalar.captureCount++;
scalar.retainClosureCapture();
} else if (value instanceof RuntimeArray || value instanceof RuntimeHash) {
RuntimeBase aggregate = (RuntimeBase) value;
capturedAggregates.add(aggregate);
aggregate.retainClosureCapture();
}
}
if (!capturedScalars.isEmpty()) {
code.capturedScalars = capturedScalars.toArray(new RuntimeScalar[0]);
}
if (!capturedAggregates.isEmpty()) {
code.capturedAggregates = capturedAggregates.toArray(new RuntimeBase[0]);
}
if (!capturedScalars.isEmpty() || !capturedAggregates.isEmpty()) {
code.refCount = 0;
}
}
Expand Down
Loading
Loading