diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index edcf203c0..1fb01f516 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -150,6 +150,10 @@ public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil erro // Add parent scope variables (for eval STRING variable capture) globalScope.putAll(parentRegistry); + // Preserve parent scope variables in the compiled code's variableRegistry so that + // nested eval STRING can capture lexicals from this eval scope. + allDeclaredVariables.putAll(parentRegistry); + // Mark parent scope variables as captured so assignments use SET_SCALAR capturedVarIndices = new HashMap<>(); for (Map.Entry entry : parentRegistry.entrySet()) { @@ -2018,6 +2022,28 @@ void compileVariableDeclaration(OperatorNode node, String op) { continue; } + // local @x / local %x in list form + if ((sigil.equals("@") || sigil.equals("%")) && sigilOp.operand instanceof IdentifierNode idNode) { + String varName = sigil + idNode.name; + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + } + + String globalVarName = NameNormalizer.normalizeVariableName(idNode.name, getCurrentPackage()); + int nameIdx = addToStringPool(globalVarName); + + int rd = allocateRegister(); + if (sigil.equals("@")) { + emitWithToken(Opcodes.LOCAL_ARRAY, node.getIndex()); + } else { + emitWithToken(Opcodes.LOCAL_HASH, node.getIndex()); + } + emitReg(rd); + emit(nameIdx); + varRegs.add(rd); + continue; + } + if (sigilOp.operand instanceof IdentifierNode) { String varName = sigil + ((IdentifierNode) sigilOp.operand).name; @@ -2211,6 +2237,72 @@ void compileVariableDeclaration(OperatorNode node, String op) { if (sigilOp.operand instanceof IdentifierNode) { String varName = sigil + ((IdentifierNode) sigilOp.operand).name; + // Parser may rewrite element-level declared refs like \$h/\@h/\%h into a scalar $h + // with annotations (isDeclaredReference + declaredReferenceOriginalSigil). + // Handle that here for single-variable our declarations. + if ("$".equals(sigil) && sigilOp.getBooleanAnnotation("isDeclaredReference") && sigilOp.annotations != null + && sigilOp.annotations.get("declaredReferenceOriginalSigil") instanceof String originalSigil + && ("$".equals(originalSigil) || "@".equals(originalSigil) || "%".equals(originalSigil))) { + + String baseName = ((IdentifierNode) sigilOp.operand).name; + + // Declare/load the package scalar $baseName + String scalarVarName = "$" + baseName; + int scalarReg = hasVariable(scalarVarName) ? getVariableRegister(scalarVarName) : addVariable(scalarVarName, "our"); + String globalScalarName = NameNormalizer.normalizeVariableName(baseName, getCurrentPackage()); + int scalarNameIdx = addToStringPool(globalScalarName); + emit(Opcodes.LOAD_GLOBAL_SCALAR); + emitReg(scalarReg); + emit(scalarNameIdx); + + int declaredReg; + if ("$".equals(originalSigil)) { + // Hidden scalar storage for declared-ref scalars + declaredReg = allocateRegister(); + emit(Opcodes.LOAD_UNDEF); + emitReg(declaredReg); + } else { + // Underlying package @/%, declared reference points at the container + String declaredVarName = originalSigil + baseName; + declaredReg = hasVariable(declaredVarName) ? getVariableRegister(declaredVarName) : addVariable(declaredVarName, "our"); + String globalName = NameNormalizer.normalizeVariableName(baseName, getCurrentPackage()); + int nameIdx = addToStringPool(globalName); + if ("@".equals(originalSigil)) { + emit(Opcodes.LOAD_GLOBAL_ARRAY); + } else { + emit(Opcodes.LOAD_GLOBAL_HASH); + } + emitReg(declaredReg); + emit(nameIdx); + } + + // Set $baseName to reference to underlying storage + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(declaredReg); + emit(Opcodes.SET_SCALAR); + emitReg(scalarReg); + emitReg(refReg); + + if (currentCallContext != RuntimeContextType.VOID) { + if ("$".equals(originalSigil)) { + // Return \$h (a REF) for scalar declared-ref elements + int scalarRefReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(scalarRefReg); + emitReg(scalarReg); + lastResultReg = scalarRefReg; + } else { + // Return the referent arrayref/hashref value + lastResultReg = refReg; + } + } else { + lastResultReg = -1; + } + return; + } + // Check if this is a declared reference (our \$x) boolean isDeclaredReference = node.annotations != null && Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); @@ -2306,6 +2398,64 @@ void compileVariableDeclaration(OperatorNode node, String op) { OperatorNode sigilOp = (OperatorNode) element; String sigil = sigilOp.operator; + // Parser may rewrite element-level declared refs like \$h/\@h/\%h into a scalar $h + // with annotations (isDeclaredReference + declaredReferenceOriginalSigil). + if ("$".equals(sigil) && sigilOp.operand instanceof IdentifierNode idNode + && sigilOp.getBooleanAnnotation("isDeclaredReference") + && sigilOp.annotations != null + && sigilOp.annotations.get("declaredReferenceOriginalSigil") instanceof String originalSigil + && ("$".equals(originalSigil) || "@".equals(originalSigil) || "%".equals(originalSigil))) { + + String baseName = idNode.name; + + // Declare/load the package scalar $baseName + String scalarVarName = "$" + baseName; + int scalarReg = hasVariable(scalarVarName) ? getVariableRegister(scalarVarName) : addVariable(scalarVarName, "our"); + String globalScalarName = NameNormalizer.normalizeVariableName(baseName, getCurrentPackage()); + int scalarNameIdx = addToStringPool(globalScalarName); + emit(Opcodes.LOAD_GLOBAL_SCALAR); + emitReg(scalarReg); + emit(scalarNameIdx); + + int declaredReg; + if ("$".equals(originalSigil)) { + declaredReg = allocateRegister(); + emit(Opcodes.LOAD_UNDEF); + emitReg(declaredReg); + } else { + String declaredVarName = originalSigil + baseName; + declaredReg = hasVariable(declaredVarName) ? getVariableRegister(declaredVarName) : addVariable(declaredVarName, "our"); + String globalName = NameNormalizer.normalizeVariableName(baseName, getCurrentPackage()); + int nameIdx = addToStringPool(globalName); + if ("@".equals(originalSigil)) { + emit(Opcodes.LOAD_GLOBAL_ARRAY); + } else { + emit(Opcodes.LOAD_GLOBAL_HASH); + } + emitReg(declaredReg); + emit(nameIdx); + } + + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(declaredReg); + emit(Opcodes.SET_SCALAR); + emitReg(scalarReg); + emitReg(refReg); + + if ("$".equals(originalSigil)) { + int scalarRefReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(scalarRefReg); + emitReg(scalarReg); + varRegs.add(scalarRefReg); + } else { + varRegs.add(refReg); + } + continue; + } + // Handle backslash operator (reference constructor): our (\$x) or our (\($x, $y)) if (sigil.equals("\\")) { // Check if it's a double backslash first: our (\\$x) @@ -2556,6 +2706,47 @@ void compileVariableDeclaration(OperatorNode node, String op) { return; } + // local @x / local %x - localize global array/hash + if ((sigil.equals("@") || sigil.equals("%")) && sigilOp.operand instanceof IdentifierNode idNode) { + String varName = sigil + idNode.name; + + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + return; + } + + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); + + String globalVarName = NameNormalizer.normalizeVariableName(idNode.name, getCurrentPackage()); + int nameIdx = addToStringPool(globalVarName); + + int rd = allocateRegister(); + if (sigil.equals("@")) { + emitWithToken(Opcodes.LOCAL_ARRAY, node.getIndex()); + } else { + emitWithToken(Opcodes.LOCAL_HASH, node.getIndex()); + } + emitReg(rd); + emit(nameIdx); + + if (isDeclaredReference && currentCallContext != RuntimeContextType.VOID) { + int refReg1 = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg1); + emitReg(rd); + + int refReg2 = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg2); + emitReg(refReg1); + lastResultReg = refReg2; + } else { + lastResultReg = rd; + } + return; + } + // Handle local \$x or local \\$x - backslash operator if (sigil.equals("\\")) { boolean isDeclaredReference = node.annotations != null && @@ -2567,8 +2758,8 @@ void compileVariableDeclaration(OperatorNode node, String op) { "$@%".contains(innerOp.operator) && innerOp.operand instanceof IdentifierNode idNode) { - // For declared refs, always use scalar sigil - String varName = "$" + idNode.name; + String originalSigil = innerOp.operator; + String varName = originalSigil + idNode.name; // Check if it's a lexical variable if (hasVariable(varName)) { @@ -2581,9 +2772,19 @@ void compileVariableDeclaration(OperatorNode node, String op) { int nameIdx = addToStringPool(globalVarName); int rd = allocateRegister(); - emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); - emitReg(rd); - emit(nameIdx); + if (originalSigil.equals("$")) { + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); + } else if (originalSigil.equals("@")) { + emitWithToken(Opcodes.LOCAL_ARRAY, node.getIndex()); + emitReg(rd); + emit(nameIdx); + } else { + emitWithToken(Opcodes.LOCAL_HASH, node.getIndex()); + emitReg(rd); + emit(nameIdx); + } // Create first reference int refReg1 = allocateRegister(); @@ -2641,7 +2842,7 @@ void compileVariableDeclaration(OperatorNode node, String op) { "$@%".contains(varNode.operator) && varNode.operand instanceof IdentifierNode idNode) { - String varName = "$" + idNode.name; + String varName = varNode.operator + idNode.name; // Check if it's a lexical variable if (hasVariable(varName)) { @@ -2653,9 +2854,19 @@ void compileVariableDeclaration(OperatorNode node, String op) { int nameIdx = addToStringPool(globalVarName); int rd = allocateRegister(); - emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); - emitReg(rd); - emit(nameIdx); + if (varNode.operator.equals("$")) { + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); + } else if (varNode.operator.equals("@")) { + emitWithToken(Opcodes.LOCAL_ARRAY, node.getIndex()); + emitReg(rd); + emit(nameIdx); + } else { + emitWithToken(Opcodes.LOCAL_HASH, node.getIndex()); + emitReg(rd); + emit(nameIdx); + } // Create first reference int refReg1 = allocateRegister(); @@ -2683,8 +2894,7 @@ void compileVariableDeclaration(OperatorNode node, String op) { if (nestedElement instanceof OperatorNode nestedVarNode && "$@%".contains(nestedVarNode.operator) && nestedVarNode.operand instanceof IdentifierNode idNode) { - // For declared refs, always use scalar sigil - String varName = "$" + idNode.name; + String varName = nestedVarNode.operator + idNode.name; // Check if it's a lexical variable if (hasVariable(varName)) { @@ -2696,9 +2906,19 @@ void compileVariableDeclaration(OperatorNode node, String op) { int nameIdx = addToStringPool(globalVarName); int rd = allocateRegister(); - emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); - emitReg(rd); - emit(nameIdx); + if (nestedVarNode.operator.equals("$")) { + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); + } else if (nestedVarNode.operator.equals("@")) { + emitWithToken(Opcodes.LOCAL_ARRAY, node.getIndex()); + emitReg(rd); + emit(nameIdx); + } else { + emitWithToken(Opcodes.LOCAL_HASH, node.getIndex()); + emitReg(rd); + emit(nameIdx); + } varRegs.add(rd); } @@ -2707,8 +2927,8 @@ void compileVariableDeclaration(OperatorNode node, String op) { "$@%".contains(varNode.operator) && varNode.operand instanceof IdentifierNode idNode) { // Single variable: local (\$x), local (\@x), local (\%x) - // For declared refs, always use scalar sigil - String varName = "$" + idNode.name; + String originalSigil = varNode.operator; + String varName = originalSigil + idNode.name; // Check if it's a lexical variable if (hasVariable(varName)) { @@ -2720,9 +2940,19 @@ void compileVariableDeclaration(OperatorNode node, String op) { int nameIdx = addToStringPool(globalVarName); int rd = allocateRegister(); - emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); - emitReg(rd); - emit(nameIdx); + if (originalSigil.equals("$")) { + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); + } else if (originalSigil.equals("@")) { + emitWithToken(Opcodes.LOCAL_ARRAY, node.getIndex()); + emitReg(rd); + emit(nameIdx); + } else { + emitWithToken(Opcodes.LOCAL_HASH, node.getIndex()); + emitReg(rd); + emit(nameIdx); + } varRegs.add(rd); } @@ -3494,23 +3724,37 @@ public void visit(SubroutineNode node) { * interoperate seamlessly with interpreted code via the shared RuntimeCode.apply() API. */ private void visitNamedSubroutine(SubroutineNode node) { - // Step 1: Collect outer variables used by this subroutine - Set usedVars = new HashSet<>(); - VariableCollectorVisitor collector = new VariableCollectorVisitor(usedVars); - node.block.accept(collector); - - // Step 2: Filter to only include lexical variables that exist in current scope - List closureVarNames = new ArrayList<>(); - List closureVarIndices = new ArrayList<>(); - - for (String varName : usedVars) { - int varIndex = getVariableRegister(varName); - if (varIndex != -1 && varIndex >= 3) { - closureVarNames.add(varName); - closureVarIndices.add(varIndex); + // Step 1: Collect closure variables. + // + // IMPORTANT: For named subs defined in eval STRING, the body may contain nested + // eval STRING that references outer lexicals that are NOT referenced directly + // in the AST (because they're inside a string). Perl still expects those outer + // lexicals to be visible from inside the sub. + // + // Therefore capture all visible Perl variables (scalars/arrays/hashes) from the + // current scope, not just variables referenced directly in the sub AST. + TreeMap closureVarsByReg = new TreeMap<>(); + for (Map scope : variableScopes) { + for (Map.Entry e : scope.entrySet()) { + String varName = e.getKey(); + Integer reg = e.getValue(); + if (reg == null || reg < 3) { + continue; + } + if (varName == null || varName.isEmpty()) { + continue; + } + char sigil = varName.charAt(0); + if (sigil != '$' && sigil != '@' && sigil != '%') { + continue; + } + closureVarsByReg.put(reg, varName); } } + List closureVarNames = new ArrayList<>(closureVarsByReg.values()); + List closureVarIndices = new ArrayList<>(closureVarsByReg.keySet()); + // If there are closure variables, we need to store them in PersistentVariable globals // so the named sub can retrieve them using RETRIEVE_BEGIN opcodes int beginId = 0; @@ -3550,11 +3794,22 @@ private void visitNamedSubroutine(SubroutineNode node) { } } - // Step 3: Create a new BytecodeCompiler for the subroutine body + // Step 3: Compile the subroutine body with a packed registry for captured variables. + // The closure created at runtime packs capturedVars sequentially into registers 3.., + // so the compiled sub body must use the same packed register layout. + Map packedRegistry = new HashMap<>(); + packedRegistry.put("this", 0); + packedRegistry.put("@_", 1); + packedRegistry.put("wantarray", 2); + for (int i = 0; i < closureVarNames.size(); i++) { + packedRegistry.put(closureVarNames.get(i), 3 + i); + } + BytecodeCompiler subCompiler = new BytecodeCompiler( this.sourceName, node.getIndex(), - this.errorUtil + this.errorUtil, + packedRegistry ); // Set the BEGIN ID in the sub-compiler so it knows to use RETRIEVE_BEGIN opcodes @@ -3575,12 +3830,15 @@ private void visitNamedSubroutine(SubroutineNode node) { emitReg(codeReg); emit(constIdx); } else { - // Store the InterpretedCode directly (closures are handled via PersistentVariable) - RuntimeScalar codeScalar = new RuntimeScalar((RuntimeCode) subCode); - int constIdx = addToConstantPool(codeScalar); - emit(Opcodes.LOAD_CONST); + // Create a closure capturing the current register values. + int templateIdx = addToConstantPool(subCode); + emit(Opcodes.CREATE_CLOSURE); emitReg(codeReg); - emit(constIdx); + emit(templateIdx); + emit(closureVarIndices.size()); + for (int regIdx : closureVarIndices) { + emit(regIdx); + } } // Step 6: Store in global namespace @@ -3612,23 +3870,34 @@ private void visitNamedSubroutine(SubroutineNode node) { * Anonymous subs capture lexical variables from the enclosing scope. */ private void visitAnonymousSubroutine(SubroutineNode node) { - // Step 1: Collect outer variables used by this subroutine - Set usedVars = new HashSet<>(); - VariableCollectorVisitor collector = new VariableCollectorVisitor(usedVars); - node.block.accept(collector); - - // Step 2: Filter to only include lexical variables that exist in current scope - List closureVarNames = new ArrayList<>(); - List closureVarIndices = new ArrayList<>(); - - for (String varName : usedVars) { - int varIndex = getVariableRegister(varName); - if (varIndex != -1 && varIndex >= 3) { - closureVarNames.add(varName); - closureVarIndices.add(varIndex); + // Step 1: Collect closure variables. + // + // IMPORTANT: Anonymous subs can contain nested eval STRING that references outer + // lexicals only inside strings (so they won't appear as IdentifierNodes in the AST). + // Perl still expects those lexicals to be visible to eval STRING at runtime. + // Capture all visible Perl variables (scalars/arrays/hashes) from the current scope. + TreeMap closureVarsByReg = new TreeMap<>(); + for (Map scope : variableScopes) { + for (Map.Entry e : scope.entrySet()) { + String varName = e.getKey(); + Integer reg = e.getValue(); + if (reg == null || reg < 3) { + continue; + } + if (varName == null || varName.isEmpty()) { + continue; + } + char sigil = varName.charAt(0); + if (sigil != '$' && sigil != '@' && sigil != '%') { + continue; + } + closureVarsByReg.put(reg, varName); } } + List closureVarNames = new ArrayList<>(closureVarsByReg.values()); + List closureVarIndices = new ArrayList<>(closureVarsByReg.keySet()); + // Step 3: Create a new BytecodeCompiler for the subroutine body // Build a variable registry from current scope to pass to sub-compiler // This allows nested closures to see grandparent scope variables @@ -4071,7 +4340,12 @@ public void visit(IfNode node) { // Mark position for forward jump to else/end int ifFalsePos = bytecode.size(); - emit(Opcodes.GOTO_IF_FALSE); + // Invert condition for 'unless' + if ("unless".equals(node.operator)) { + emit(Opcodes.GOTO_IF_TRUE); + } else { + emit(Opcodes.GOTO_IF_FALSE); + } emitReg(condReg); emitInt(0); // Placeholder for else/end target diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 6a4f4b4f7..ef589e88b 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -1779,6 +1779,8 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.RETRIEVE_BEGIN_ARRAY: case Opcodes.RETRIEVE_BEGIN_HASH: case Opcodes.LOCAL_SCALAR: + case Opcodes.LOCAL_ARRAY: + case Opcodes.LOCAL_HASH: pc = executeScopeOps(opcode, bytecode, pc, registers, code); break; @@ -2213,381 +2215,6 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } /** - * Handle type and reference operations (opcodes 62-70, 102-105). - * Separated to keep main execute() under JIT compilation limit. - * - * @return Updated program counter - */ - private static int executeTypeOps(int opcode, int[] bytecode, int pc, - RuntimeBase[] registers, InterpretedCode code) { - switch (opcode) { - case Opcodes.CREATE_LAST: { - int rd = bytecode[pc++]; - int labelIdx = bytecode[pc++]; - String label = labelIdx == 255 ? null : code.stringPool[labelIdx]; - registers[rd] = new RuntimeControlFlowList( - ControlFlowType.LAST, label, - code.sourceName, code.sourceLine - ); - return pc; - } - - case Opcodes.CREATE_NEXT: { - int rd = bytecode[pc++]; - int labelIdx = bytecode[pc++]; - String label = labelIdx == 255 ? null : code.stringPool[labelIdx]; - registers[rd] = new RuntimeControlFlowList( - ControlFlowType.NEXT, label, - code.sourceName, code.sourceLine - ); - return pc; - } - - case Opcodes.CREATE_REDO: { - int rd = bytecode[pc++]; - int labelIdx = bytecode[pc++]; - String label = labelIdx == 255 ? null : code.stringPool[labelIdx]; - registers[rd] = new RuntimeControlFlowList( - ControlFlowType.REDO, label, - code.sourceName, code.sourceLine - ); - return pc; - } - - case Opcodes.CREATE_GOTO: { - int rd = bytecode[pc++]; - int labelIdx = bytecode[pc++]; - String label = labelIdx == 255 ? null : code.stringPool[labelIdx]; - registers[rd] = new RuntimeControlFlowList( - ControlFlowType.GOTO, label, - code.sourceName, code.sourceLine - ); - return pc; - } - - case Opcodes.IS_CONTROL_FLOW: { - int rd = bytecode[pc++]; - int rs = bytecode[pc++]; - boolean isControlFlow = registers[rs] instanceof RuntimeControlFlowList; - registers[rd] = isControlFlow ? - RuntimeScalarCache.scalarTrue : RuntimeScalarCache.scalarFalse; - return pc; - } - - case Opcodes.GET_CONTROL_FLOW_TYPE: { - int rd = bytecode[pc++]; - int rs = bytecode[pc++]; - RuntimeControlFlowList cf = (RuntimeControlFlowList) registers[rs]; - registers[rd] = new RuntimeScalar(cf.marker.type.ordinal()); - return pc; - } - - case Opcodes.CREATE_REF: { - int rd = bytecode[pc++]; - int rs = bytecode[pc++]; - RuntimeBase value = registers[rs]; - if (value instanceof RuntimeList list) { - if (list.size() == 1) { - registers[rd] = list.getFirst().createReference(); - } else { - RuntimeList refs = new RuntimeList(); - for (RuntimeScalar element : list) { - refs.add(element.createReference()); - } - registers[rd] = refs; - } - } else { - registers[rd] = value.createReference(); - } - return pc; - } - - case Opcodes.DEREF: { - int rd = bytecode[pc++]; - int rs = bytecode[pc++]; - RuntimeBase value = registers[rs]; - - // Only dereference if it's a RuntimeScalar with REFERENCE type - if (value instanceof RuntimeScalar) { - RuntimeScalar scalar = (RuntimeScalar) value; - if (scalar.type == RuntimeScalarType.REFERENCE) { - registers[rd] = scalar.scalarDeref(); - } else { - // Non-reference scalar, just copy - registers[rd] = value; - } - } else { - // RuntimeList or other types, pass through - registers[rd] = value; - } - return pc; - } - - case Opcodes.GET_TYPE: { - int rd = bytecode[pc++]; - int rs = bytecode[pc++]; - RuntimeScalar value = (RuntimeScalar) registers[rs]; - registers[rd] = new RuntimeScalar(value.type); - return pc; - } - - case Opcodes.DEFINED: { - int rd = bytecode[pc++]; - int rs = bytecode[pc++]; - RuntimeBase val = registers[rs]; - boolean isDefined = val != null && val.getDefinedBoolean(); - registers[rd] = isDefined ? - RuntimeScalarCache.scalarTrue : RuntimeScalarCache.scalarFalse; - return pc; - } - - case Opcodes.REF: { - int rd = bytecode[pc++]; - int rs = bytecode[pc++]; - RuntimeBase val = registers[rs]; - RuntimeScalar result; - if (val instanceof RuntimeScalar) { - result = ReferenceOperators.ref((RuntimeScalar) val); - } else { - result = ReferenceOperators.ref(val.scalar()); - } - registers[rd] = result; - return pc; - } - - case Opcodes.BLESS: { - int rd = bytecode[pc++]; - int refReg = bytecode[pc++]; - int packageReg = bytecode[pc++]; - RuntimeScalar ref = (RuntimeScalar) registers[refReg]; - RuntimeScalar packageName = (RuntimeScalar) registers[packageReg]; - registers[rd] = ReferenceOperators.bless(ref, packageName); - return pc; - } - - case Opcodes.ISA: { - int rd = bytecode[pc++]; - int objReg = bytecode[pc++]; - int packageReg = bytecode[pc++]; - RuntimeScalar obj = (RuntimeScalar) registers[objReg]; - RuntimeScalar packageName = (RuntimeScalar) registers[packageReg]; - RuntimeArray isaArgs = new RuntimeArray(); - isaArgs.push(obj); - isaArgs.push(packageName); - RuntimeList result = Universal.isa(isaArgs, RuntimeContextType.SCALAR); - registers[rd] = result.scalar(); - return pc; - } - - case Opcodes.PROTOTYPE: { - int rd = bytecode[pc++]; - int rs = bytecode[pc++]; - int packageIdx = readInt(bytecode, pc); - pc += 1; // readInt reads 2 shorts - RuntimeScalar codeRef = (RuntimeScalar) registers[rs]; - String packageName = code.stringPool[packageIdx]; - registers[rd] = RuntimeCode.prototype(codeRef, packageName); - return pc; - } - - case Opcodes.QUOTE_REGEX: { - int rd = bytecode[pc++]; - int patternReg = bytecode[pc++]; - int flagsReg = bytecode[pc++]; - RuntimeScalar pattern = (RuntimeScalar) registers[patternReg]; - RuntimeScalar flags = (RuntimeScalar) registers[flagsReg]; - - // Debug logging - if (DEBUG_REGEX) { - System.err.println("BytecodeInterpreter.QUOTE_REGEX: pattern=" + pattern.toString() + - " flags=" + flags.toString()); - } - - registers[rd] = RuntimeRegex.getQuotedRegex(pattern, flags); - return pc; - } - - default: - throw new RuntimeException("Unknown type opcode: " + opcode); - } - } - - /** - * Handle array and hash operations (opcodes 43-49, 51-56, 93-96). - * Separated to keep main execute() under JIT compilation limit. - * - * @return Updated program counter - */ - private static int executeCollections(int opcode, int[] bytecode, int pc, - RuntimeBase[] registers, InterpretedCode code) { - switch (opcode) { - case Opcodes.ARRAY_SET: { - int arrayReg = bytecode[pc++]; - int indexReg = bytecode[pc++]; - int valueReg = bytecode[pc++]; - RuntimeArray arr = (RuntimeArray) registers[arrayReg]; - RuntimeScalar idx = (RuntimeScalar) registers[indexReg]; - RuntimeScalar val = (RuntimeScalar) registers[valueReg]; - arr.get(idx.getInt()).set(val); - return pc; - } - - case Opcodes.ARRAY_PUSH: { - int arrayReg = bytecode[pc++]; - int valueReg = bytecode[pc++]; - RuntimeArray arr = (RuntimeArray) registers[arrayReg]; - RuntimeBase val = registers[valueReg]; - arr.push(val); - return pc; - } - - case Opcodes.ARRAY_POP: { - int rd = bytecode[pc++]; - int arrayReg = bytecode[pc++]; - RuntimeArray arr = (RuntimeArray) registers[arrayReg]; - registers[rd] = RuntimeArray.pop(arr); - return pc; - } - - case Opcodes.ARRAY_SHIFT: { - int rd = bytecode[pc++]; - int arrayReg = bytecode[pc++]; - RuntimeArray arr = (RuntimeArray) registers[arrayReg]; - registers[rd] = RuntimeArray.shift(arr); - return pc; - } - - case Opcodes.ARRAY_UNSHIFT: { - int arrayReg = bytecode[pc++]; - int valueReg = bytecode[pc++]; - RuntimeArray arr = (RuntimeArray) registers[arrayReg]; - RuntimeBase val = registers[valueReg]; - RuntimeArray.unshift(arr, val); - return pc; - } - - case Opcodes.ARRAY_SIZE: { - int rd = bytecode[pc++]; - int operandReg = bytecode[pc++]; - RuntimeBase operand = registers[operandReg]; - if (operand instanceof RuntimeList) { - registers[rd] = new RuntimeScalar(((RuntimeList) operand).size()); - } else { - registers[rd] = operand.scalar(); - } - return pc; - } - - case Opcodes.CREATE_ARRAY: { - int rd = bytecode[pc++]; - int listReg = bytecode[pc++]; - RuntimeBase source = registers[listReg]; - RuntimeArray array; - if (source instanceof RuntimeArray) { - array = (RuntimeArray) source; - } else { - RuntimeList list = source.getList(); - array = new RuntimeArray(list); - } - registers[rd] = array.createReference(); - return pc; - } - - case Opcodes.HASH_SET: { - int hashReg = bytecode[pc++]; - int keyReg = bytecode[pc++]; - int valueReg = bytecode[pc++]; - RuntimeHash hash = (RuntimeHash) registers[hashReg]; - RuntimeScalar key = (RuntimeScalar) registers[keyReg]; - RuntimeScalar val = (RuntimeScalar) registers[valueReg]; - hash.put(key.toString(), val); - return pc; - } - - case Opcodes.HASH_EXISTS: { - int rd = bytecode[pc++]; - int hashReg = bytecode[pc++]; - int keyReg = bytecode[pc++]; - RuntimeHash hash = (RuntimeHash) registers[hashReg]; - RuntimeScalar key = (RuntimeScalar) registers[keyReg]; - registers[rd] = hash.exists(key); - return pc; - } - - case Opcodes.HASH_DELETE: { - int rd = bytecode[pc++]; - int hashReg = bytecode[pc++]; - int keyReg = bytecode[pc++]; - RuntimeHash hash = (RuntimeHash) registers[hashReg]; - RuntimeScalar key = (RuntimeScalar) registers[keyReg]; - registers[rd] = hash.delete(key); - return pc; - } - - case Opcodes.HASH_KEYS: { - int rd = bytecode[pc++]; - int hashReg = bytecode[pc++]; - RuntimeHash hash = (RuntimeHash) registers[hashReg]; - registers[rd] = hash.keys(); - return pc; - } - - case Opcodes.HASH_VALUES: { - int rd = bytecode[pc++]; - int hashReg = bytecode[pc++]; - RuntimeHash hash = (RuntimeHash) registers[hashReg]; - registers[rd] = hash.values(); - return pc; - } - - case Opcodes.CREATE_HASH: { - int rd = bytecode[pc++]; - int listReg = bytecode[pc++]; - RuntimeBase list = registers[listReg]; - RuntimeHash hash = RuntimeHash.createHash(list); - registers[rd] = hash.createReference(); - return pc; - } - - case Opcodes.NEW_ARRAY: { - int rd = bytecode[pc++]; - registers[rd] = new RuntimeArray(); - return pc; - } - - case Opcodes.NEW_HASH: { - int rd = bytecode[pc++]; - registers[rd] = new RuntimeHash(); - return pc; - } - - case Opcodes.ARRAY_SET_FROM_LIST: { - int arrayReg = bytecode[pc++]; - int listReg = bytecode[pc++]; - RuntimeArray array = (RuntimeArray) registers[arrayReg]; - RuntimeBase listBase = registers[listReg]; - RuntimeList list = listBase.getList(); - array.setFromList(list); - return pc; - } - - case Opcodes.HASH_SET_FROM_LIST: { - int hashReg = bytecode[pc++]; - int listReg = bytecode[pc++]; - RuntimeHash hash = (RuntimeHash) registers[hashReg]; - RuntimeBase listBase = registers[listReg]; - RuntimeList list = listBase.getList(); - hash.setFromList(list); - return pc; - } - - default: - throw new RuntimeException("Unknown collection opcode: " + opcode); - } - } - - /** - * Handle arithmetic and string operations (opcodes 19-30, 110-113). * Separated to keep main execute() under JIT compilation limit. * * @return Updated program counter @@ -3047,6 +2674,68 @@ private static int executeComparisons(int opcode, int[] bytecode, int pc, } } + /** + * Execute type and reference operations. + * Handles: DEFINED, REF, BLESS, ISA, PROTOTYPE, QUOTE_REGEX + */ + private static int executeTypeOps(int opcode, int[] bytecode, int pc, + RuntimeBase[] registers, InterpretedCode code) { + switch (opcode) { + case Opcodes.DEFINED: { + int rd = bytecode[pc++]; + int rs = bytecode[pc++]; + RuntimeBase v = registers[rs]; + boolean defined = v != null && v.scalar().getDefinedBoolean(); + registers[rd] = defined ? RuntimeScalarCache.scalarTrue : RuntimeScalarCache.scalarFalse; + return pc; + } + case Opcodes.REF: { + int rd = bytecode[pc++]; + int rs = bytecode[pc++]; + registers[rd] = ReferenceOperators.ref(registers[rs].scalar()); + return pc; + } + case Opcodes.BLESS: { + int rd = bytecode[pc++]; + int refReg = bytecode[pc++]; + int pkgReg = bytecode[pc++]; + RuntimeScalar ref = registers[refReg].scalar(); + RuntimeScalar pkg = registers[pkgReg].scalar(); + registers[rd] = ReferenceOperators.bless(ref, pkg); + return pc; + } + case Opcodes.ISA: { + int rd = bytecode[pc++]; + int objReg = bytecode[pc++]; + int pkgReg = bytecode[pc++]; + RuntimeScalar obj = registers[objReg].scalar(); + RuntimeScalar pkg = registers[pkgReg].scalar(); + registers[rd] = ReferenceOperators.isa(obj, pkg); + return pc; + } + case Opcodes.PROTOTYPE: { + int rd = bytecode[pc++]; + int rs = bytecode[pc++]; + int packageIdx = readInt(bytecode, pc); + pc += 1; + String packageName = (code.stringPool != null && packageIdx >= 0 && packageIdx < code.stringPool.length) + ? code.stringPool[packageIdx] + : "main"; + registers[rd] = RuntimeCode.prototype(registers[rs].scalar(), packageName); + return pc; + } + case Opcodes.QUOTE_REGEX: { + int rd = bytecode[pc++]; + int patternReg = bytecode[pc++]; + int flagsReg = bytecode[pc++]; + registers[rd] = RuntimeRegex.getQuotedRegex(registers[patternReg].scalar(), registers[flagsReg].scalar()); + return pc; + } + default: + throw new RuntimeException("Unknown type opcode: " + opcode); + } + } + /** * Execute slice operations (opcodes 114-121). * Handles: DEREF_ARRAY, DEREF_HASH, *_SLICE, *_SLICE_SET, *_SLICE_DELETE, LIST_SLICE_FROM @@ -3118,6 +2807,26 @@ private static int executeScopeOps(int opcode, int[] bytecode, int pc, return SlowOpcodeHandler.executeRetrieveBeginHash(bytecode, pc, registers, code); case Opcodes.LOCAL_SCALAR: return SlowOpcodeHandler.executeLocalScalar(bytecode, pc, registers, code); + case Opcodes.LOCAL_ARRAY: { + int rd = bytecode[pc++]; + int nameIdx = bytecode[pc++]; + String fullName = code.stringPool[nameIdx]; + + RuntimeArray arr = GlobalVariable.getGlobalArray(fullName); + DynamicVariableManager.pushLocalVariable(arr); + registers[rd] = GlobalVariable.getGlobalArray(fullName); + return pc; + } + case Opcodes.LOCAL_HASH: { + int rd = bytecode[pc++]; + int nameIdx = bytecode[pc++]; + String fullName = code.stringPool[nameIdx]; + + RuntimeHash hash = GlobalVariable.getGlobalHash(fullName); + DynamicVariableManager.pushLocalVariable(hash); + registers[rd] = GlobalVariable.getGlobalHash(fullName); + return pc; + } default: throw new RuntimeException("Unknown scope opcode: " + opcode); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java index 7030e59ff..0185767b0 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InterpretedCode.java @@ -1365,6 +1365,16 @@ public String disassemble() { nameIdx = bytecode[pc++]; sb.append("LOCAL_SCALAR r").append(rd).append(" = local $").append(stringPool[nameIdx]).append("\n"); break; + case Opcodes.LOCAL_ARRAY: + rd = bytecode[pc++]; + nameIdx = bytecode[pc++]; + sb.append("LOCAL_ARRAY r").append(rd).append(" = local @").append(stringPool[nameIdx]).append("\n"); + break; + case Opcodes.LOCAL_HASH: + rd = bytecode[pc++]; + nameIdx = bytecode[pc++]; + sb.append("LOCAL_HASH r").append(rd).append(" = local %").append(stringPool[nameIdx]).append("\n"); + break; case Opcodes.LOCAL_SCALAR_SAVE_LEVEL: { rd = bytecode[pc++]; int levelReg = 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 02c9c7318..e809cc50e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -545,6 +545,10 @@ public class Opcodes { public static final short RETRIEVE_BEGIN_HASH = 130; /** Localize global variable: rd = GlobalRuntimeScalar.makeLocal(var_name) */ public static final short LOCAL_SCALAR = 131; + /** Localize global array: rd = GlobalVariable.getGlobalArray(var_name) (dynamicSaveState via DynamicVariableManager) */ + public static final short LOCAL_ARRAY = 345; + /** Localize global hash: rd = GlobalVariable.getGlobalHash(var_name) (dynamicSaveState via DynamicVariableManager) */ + public static final short LOCAL_HASH = 346; // Group 6: System Calls (132-141) - CONTIGUOUS /** chown(list, uid, gid) */ diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitEval.java b/src/main/java/org/perlonjava/backend/jvm/EmitEval.java index dfbdaaf88..860c79f47 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitEval.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitEval.java @@ -546,14 +546,18 @@ private static void emitEvalInterpreterPath(EmitterVisitor emitterVisitor, Strin // Stack: [RuntimeScalar(String), String, Object[]] // Fill the runtime values array with actual variable values from local variables - for (Integer index : newSymbolTable.getAllVisibleVariables().keySet()) { - if (index >= skipVariables) { - String varName = newEnv[index]; - mv.visitInsn(Opcodes.DUP); - mv.visitIntInsn(Opcodes.BIPUSH, index - skipVariables); - mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.symbolTable.getVariableIndex(varName)); - mv.visitInsn(Opcodes.AASTORE); + // IMPORTANT: Fill in capturedEnv index order so runtimeValues[] matches newEnv[] ordering. + // Iterating a Map keySet() is not guaranteed to be deterministic and can break + // lexical capture across nested eval boundaries. + for (int index = skipVariables; index < newEnv.length; index++) { + String varName = newEnv[index]; + if (varName == null) { + continue; } + mv.visitInsn(Opcodes.DUP); + mv.visitIntInsn(Opcodes.BIPUSH, index - skipVariables); + mv.visitVarInsn(Opcodes.ALOAD, emitterVisitor.ctx.symbolTable.getVariableIndex(varName)); + mv.visitInsn(Opcodes.AASTORE); } // Stack: [RuntimeScalar(String), String, Object[]] diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java b/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java index a439f286f..491a814aa 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitForeach.java @@ -44,7 +44,6 @@ private static void pushGotoLabelsForBlock(EmitterVisitor emitterVisitor, BlockN for (int i = 0; i < blockNode.labels.size(); i++) { emitterVisitor.ctx.javaClassInfo.pushGotoLabels(blockNode.labels.get(i), new Label()); } - // Some labels may appear as explicit LabelNode statements. for (Node element : blockNode.elements) { if (element instanceof LabelNode labelNode) { @@ -65,6 +64,30 @@ private static void popGotoLabelsForBlock(EmitterVisitor emitterVisitor, BlockNo } } + private static String extractSimpleVariableName(Node node) { + // Handle wrappers introduced by declared_refs and/or refaliasing. + // Examples we need to support: + // - $x / @x / %x + // - \$x / \@x / \%x + // - my $x / state $x / our $x / local $x + // - my \$x (declared_refs) + if (node instanceof OperatorNode opNode) { + String op = opNode.operator; + if ("my".equals(op) || "state".equals(op) || "our".equals(op) || "local".equals(op)) { + return extractSimpleVariableName(opNode.operand); + } + if ("\\".equals(op)) { + return extractSimpleVariableName(opNode.operand); + } + if ("$".equals(op) || "@".equals(op) || "%".equals(op)) { + if (opNode.operand instanceof IdentifierNode idNode) { + return op + idNode.name; + } + } + } + return null; + } + public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { emitterVisitor.ctx.logDebug("FOR1 start"); @@ -388,7 +411,10 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { // Assign to variable Node varNode = varList.elements.get(i); if (varNode instanceof OperatorNode operatorNode) { - String varName = operatorNode.operator + ((IdentifierNode) operatorNode.operand).name; + String varName = extractSimpleVariableName(operatorNode); + if (varName == null) { + continue; + } int varIndex = emitterVisitor.ctx.symbolTable.getVariableIndex(varName); emitterVisitor.ctx.logDebug("FOR1 multi var name:" + varName + " index:" + varIndex); mv.visitVarInsn(Opcodes.ASTORE, varIndex); @@ -459,10 +485,14 @@ public static void emitFor1(EmitterVisitor emitterVisitor, For1Node node) { } } else if (variableNode instanceof OperatorNode operatorNode) { // Local variable case - String varName = operatorNode.operator + ((IdentifierNode) operatorNode.operand).name; - int varIndex = emitterVisitor.ctx.symbolTable.getVariableIndex(varName); - emitterVisitor.ctx.logDebug("FOR1 single var name:" + varName + " index:" + varIndex); - mv.visitVarInsn(Opcodes.ASTORE, varIndex); + String varName = extractSimpleVariableName(operatorNode); + if (varName == null) { + // Unsupported variable shape; skip assignment rather than failing compilation. + } else { + int varIndex = emitterVisitor.ctx.symbolTable.getVariableIndex(varName); + emitterVisitor.ctx.logDebug("FOR1 single var name:" + varName + " index:" + varIndex); + mv.visitVarInsn(Opcodes.ASTORE, varIndex); + } } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 51d6e7d88..bc1a18868 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -377,6 +377,11 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // call site is looking up via reflection. ScopedSymbolTable capturedSymbolTable = ctx.symbolTable; + // %^H is the compile-time hints hash. eval STRING must not leak modifications to the + // caller scope, so snapshot and restore it across compilation. + RuntimeHash capturedHintHash = GlobalVariable.getGlobalHash(GlobalContext.encodeSpecialVar("H")); + Map savedHintHash = new HashMap<>(capturedHintHash.elements); + // eval may include lexical pragmas (use strict/warnings/features). We need those flags // during codegen of the eval body, but they must NOT leak back into the caller scope. BitSet savedWarningFlags = (BitSet) capturedSymbolTable.warningFlagsStack.peek().clone(); @@ -545,6 +550,10 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje capturedSymbolTable.strictOptionsStack.pop(); capturedSymbolTable.strictOptionsStack.push(savedStrictOptions); + // Restore %^H (compile-time hints hash) to the caller snapshot. + capturedHintHash.elements.clear(); + capturedHintHash.elements.putAll(savedHintHash); + setCurrentScope(capturedSymbolTable); // Store source lines in symbol table if $^P flags are set @@ -725,7 +734,6 @@ public static RuntimeList evalStringWithInterpreter( try { String evalString = code.toString(); - // Handle Unicode source detection (same logic as evalStringHelper) boolean hasUnicode = false; if (!ctx.isEvalbytes && code.type != RuntimeScalarType.BYTE_STRING) { @@ -814,15 +822,20 @@ public static RuntimeList evalStringWithInterpreter( adjustedRegistry.put("@_", 1); adjustedRegistry.put("wantarray", 2); - // Add captured variables starting at register 3 - int captureIndex = 3; - Map capturedVariables = capturedSymbolTable.getAllVisibleVariables(); - for (Map.Entry entry : capturedVariables.entrySet()) { - int index = entry.getKey(); - if (index >= 3) { // Skip reserved registers - String varName = entry.getValue().name(); - adjustedRegistry.put(varName, captureIndex); - captureIndex++; + // IMPORTANT: Captured variables are loaded into registers starting at 3 + // (see BytecodeInterpreter's `System.arraycopy(code.capturedVars, 0, registers, 3, ...)`). + // Therefore the variable registry must map captured variable names to packed + // register indices 3+(i-skipVariables), in the same order as runtimeValues[]. + // Do NOT pack indices: runtimeValues[] is sized for the full capturedEnv range + // and may contain null gaps, so the corresponding registers must keep the same + // slot numbering. + if (ctx.capturedEnv != null) { + for (int i = 3; i < ctx.capturedEnv.length; i++) { + String varName = ctx.capturedEnv[i]; + if (varName == null) { + continue; + } + adjustedRegistry.put(varName, i); } } @@ -866,6 +879,14 @@ public static RuntimeList evalStringWithInterpreter( // If EVAL_VERBOSE is set, print the error to stderr for debugging if (EVAL_VERBOSE) { System.err.println("eval compilation error: " + e.getMessage()); + String src = evalString; + if (src != null) { + int maxLen = 4000; + if (src.length() > maxLen) { + src = src.substring(0, maxLen) + "\n..."; + } + System.err.println("eval source:\n" + src); + } if (e.getCause() != null) { System.err.println("Caused by: " + e.getCause().getMessage()); } @@ -935,6 +956,18 @@ public static RuntimeList evalStringWithInterpreter( err.set("Died"); } + if (EVAL_VERBOSE) { + System.err.println("eval runtime error: " + err); + String src = evalString; + if (src != null) { + int maxLen = 4000; + if (src.length() > maxLen) { + src = src.substring(0, maxLen) + "\n..."; + } + System.err.println("eval source:\n" + src); + } + } + // Return undef/empty list if (callContext == RuntimeContextType.LIST) { return new RuntimeList();