diff --git a/dev/interpreter/SKILL.md b/dev/interpreter/SKILL.md index 72735fd8e..f4018773d 100644 --- a/dev/interpreter/SKILL.md +++ b/dev/interpreter/SKILL.md @@ -8,6 +8,18 @@ **Opcodes:** 0-157 (contiguous) for JVM tableswitch optimization **Runtime:** 100% API compatibility with compiler (zero duplication) +### Testing Modes + +**JPERL_EVAL_USE_INTERPRETER=1** - Forces all eval STRING to use the interpreter +- Used for testing interpreter implementation of operators in eval context +- Compiler still used for main code, only eval STRING uses interpreter +- Example: `JPERL_EVAL_USE_INTERPRETER=1 ./jperl test.pl` + +**--interpreter** - Forces the interpreter EVERYWHERE +- All code (main and eval) runs in interpreter mode +- Used for full interpreter testing and development +- Example: `./jperl --interpreter test.pl` + ## Core Files - `Opcodes.java` - Opcode constants (0-157, contiguous) diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java index db4ba2156..ba5c94ceb 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java @@ -3625,12 +3625,12 @@ else if (node.right instanceof BinaryOperatorNode) { } /** - * Compile variable declaration operators (my, our, local). + * Compile variable declaration operators (my, our, local, state). * Extracted to reduce visit(OperatorNode) bytecode size. */ private void compileVariableDeclaration(OperatorNode node, String op) { - if (op.equals("my")) { - // my $x / my @x / my %x - variable declaration + if (op.equals("my") || op.equals("state")) { + // my $x / my @x / my %x / state $x / state @x / state %x - variable declaration // The operand will be OperatorNode("$"/"@"/"%", IdentifierNode("x")) if (node.operand instanceof OperatorNode) { OperatorNode sigilOp = (OperatorNode) node.operand; @@ -3639,14 +3639,19 @@ private void compileVariableDeclaration(OperatorNode node, String op) { if (sigilOp.operand instanceof IdentifierNode) { String varName = sigil + ((IdentifierNode) sigilOp.operand).name; - // Check if this variable is captured by closures (sigilOp.id != 0) - if (sigilOp.id != 0) { - // Variable is captured by compiled named subs + // Check if this is a declared reference (my \$x or state \$x) + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); + + // Check if this variable is captured by closures (sigilOp.id != 0) or is a state variable + // State variables always use persistent storage + if (sigilOp.id != 0 || op.equals("state")) { + // Variable is captured by compiled named subs or is a state variable // Store as persistent variable so both interpreted and compiled code can access it // Don't use a local register; instead load/store through persistent globals - // For now, retrieve the persistent variable and store in register - // This handles BEGIN-initialized variables + // For state variables, retrieve or initialize the persistent variable + // For captured variables, retrieve the BEGIN-initialized variable int reg = allocateRegister(); int nameIdx = addToStringPool(varName); @@ -3656,7 +3661,7 @@ private void compileVariableDeclaration(OperatorNode node, String op) { emitReg(reg); emit(nameIdx); emit(sigilOp.id); - // Track this as a captured variable - map to the register we allocated + // Track this as a captured/state variable - map to the register we allocated variableScopes.peek().put(varName, reg); allDeclaredVariables.put(varName, reg); // Track for variableRegistry } @@ -3679,11 +3684,20 @@ private void compileVariableDeclaration(OperatorNode node, String op) { default -> throwCompilerException("Unsupported variable type: " + sigil); } - lastResultReg = reg; + // If this is a declared reference, create a reference to it + if (isDeclaredReference && currentCallContext != RuntimeContextType.VOID) { + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(reg); + lastResultReg = refReg; + } else { + lastResultReg = reg; + } return; } - // Regular lexical variable (not captured) + // Regular lexical variable (not captured, not state) int reg = addVariable(varName, "my"); // Normal initialization: load undef/empty array/empty hash @@ -3703,25 +3717,159 @@ private void compileVariableDeclaration(OperatorNode node, String op) { default -> throwCompilerException("Unsupported variable type: " + sigil); } - lastResultReg = reg; + // If this is a declared reference, create a reference to it + if (isDeclaredReference && currentCallContext != RuntimeContextType.VOID) { + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(reg); + lastResultReg = refReg; + } else { + lastResultReg = reg; + } return; } + + // Handle my \\$x - reference to a declared reference (double backslash) + if (sigil.equals("\\")) { + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); + + if (isDeclaredReference) { + // This is my \\$x which means: create a declared reference and then take a reference to it + // The operand is \$x, so we recursively compile it + sigilOp.accept(this); + // The result is now in lastResultReg + return; + } + } } else if (node.operand instanceof ListNode) { // my ($x, $y, @rest) - list of variable declarations ListNode listNode = (ListNode) node.operand; List varRegs = new ArrayList<>(); + // Check if this is a declared reference (my \($x, $y)) + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); + + // Track if we found any backslash operators inside the list (my (\($x, $y))) + boolean foundBackslashInList = false; + for (Node element : listNode.elements) { if (element instanceof OperatorNode) { OperatorNode sigilOp = (OperatorNode) element; String sigil = sigilOp.operator; + // Handle backslash operator (reference constructor): my (\$x) or my (\($x, $y)) + if (sigil.equals("\\")) { + // Check if it's a double backslash first: my (\\$x) + if (sigilOp.operand instanceof OperatorNode innerBackslash && + innerBackslash.operator.equals("\\")) { + // Double backslash: my (\\$x) + // This creates a reference to a reference + // We handle this completely here and add the final result to varRegs + // Don't set foundBackslashInList since we're creating references ourselves + + // Recursively compile the inner part + // Create a temporary my node for the inner backslash + OperatorNode innerMy = new OperatorNode("my", innerBackslash, node.getIndex()); + if (node.annotations != null && Boolean.TRUE.equals(node.annotations.get("isDeclaredReference"))) { + innerMy.setAnnotation("isDeclaredReference", true); + } + innerMy.accept(this); + + // The inner result is in lastResultReg, now create a reference to it + if (currentCallContext != RuntimeContextType.VOID && lastResultReg != -1) { + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(lastResultReg); + varRegs.add(refReg); + } + continue; + } + + // Single backslash - mark that we need to create references later + foundBackslashInList = true; + + // Check if it's a nested list: my (\($d, $e)) + if (sigilOp.operand instanceof ListNode nestedList) { + // Process each element in the nested list as a declared reference + for (Node nestedElement : nestedList.elements) { + if (nestedElement instanceof OperatorNode nestedVarNode && + "$@%".contains(nestedVarNode.operator)) { + // Get the variable name + if (nestedVarNode.operand instanceof IdentifierNode idNode) { + // For declared refs, variable is always scalar holding a ref + String varName = "$" + idNode.name; + + // Declare the variable + int reg = addVariable(varName, op); + + // Initialize based on original sigil + String originalSigil = nestedVarNode.operator; + switch (originalSigil) { + case "$" -> { + emit(Opcodes.LOAD_UNDEF); + emitReg(reg); + } + case "@" -> { + // Create an array ref + emit(Opcodes.NEW_ARRAY); + emitReg(reg); + } + case "%" -> { + // Create a hash ref + emit(Opcodes.NEW_HASH); + emitReg(reg); + } + } + + varRegs.add(reg); + } + } + } + } else if (sigilOp.operand instanceof OperatorNode varNode && + "$@%".contains(varNode.operator)) { + // Single variable: my (\$x) or state (\$x) or my (\@x) or my (\%x) + if (varNode.operand instanceof IdentifierNode idNode) { + // For declared refs, variable is always scalar holding a ref + String varName = "$" + idNode.name; + + // Declare the variable + int reg = addVariable(varName, op); + + // Initialize based on original sigil + String originalSigil = varNode.operator; + switch (originalSigil) { + case "$" -> { + emit(Opcodes.LOAD_UNDEF); + emitReg(reg); + } + case "@" -> { + // Create an array ref + emit(Opcodes.NEW_ARRAY); + emitReg(reg); + } + case "%" -> { + // Create a hash ref + emit(Opcodes.NEW_HASH); + emitReg(reg); + } + } + + varRegs.add(reg); + } + } + continue; + } + if (sigilOp.operand instanceof IdentifierNode) { String varName = sigil + ((IdentifierNode) sigilOp.operand).name; - // Check if this variable is captured by closures - if (sigilOp.id != 0) { - // Variable is captured - use persistent storage + // Check if this variable is captured by closures or is a state variable + if (sigilOp.id != 0 || op.equals("state")) { + // Variable is captured or is a state variable - use persistent storage int reg = allocateRegister(); int nameIdx = addToStringPool(varName); @@ -3755,8 +3903,8 @@ private void compileVariableDeclaration(OperatorNode node, String op) { varRegs.add(reg); } else { - // Regular lexical variable - int reg = addVariable(varName, "my"); + // Regular lexical variable (not captured, not state) + int reg = addVariable(varName, op); // Initialize the variable switch (sigil) { @@ -3777,21 +3925,52 @@ private void compileVariableDeclaration(OperatorNode node, String op) { varRegs.add(reg); } + } else if (sigilOp.operand instanceof OperatorNode) { + // Handle declared references with backslash: my (\$x) + // The backslash operator wraps the variable + // For now, skip these as they require special handling + continue; } else { throwCompilerException("my list declaration requires identifier: " + sigilOp.operand.getClass().getSimpleName()); } + } else if (element instanceof ListNode) { + // Nested list: my (($x, $y)) + // Skip nested lists for now + continue; } else { throwCompilerException("my list declaration requires scalar/array/hash: " + element.getClass().getSimpleName()); } } - // Return a list of the declared variables + // Return a list of the declared variables (or their references if isDeclaredReference) int resultReg = allocateRegister(); - emit(Opcodes.CREATE_LIST); - emitReg(resultReg); - emit(varRegs.size()); - for (int varReg : varRegs) { - emitReg(varReg); + + if ((isDeclaredReference || foundBackslashInList) && currentCallContext != RuntimeContextType.VOID) { + // Create references to all variables first + List refRegs = new ArrayList<>(); + for (int varReg : varRegs) { + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(varReg); + refRegs.add(refReg); + } + + // Create a list of the references + emit(Opcodes.CREATE_LIST); + emitReg(resultReg); + emit(refRegs.size()); + for (int refReg : refRegs) { + emitReg(refReg); + } + } else { + // Regular list of variables + emit(Opcodes.CREATE_LIST); + emitReg(resultReg); + emit(varRegs.size()); + for (int varReg : varRegs) { + emitReg(varReg); + } } lastResultReg = resultReg; @@ -3808,10 +3987,25 @@ private void compileVariableDeclaration(OperatorNode node, String op) { if (sigilOp.operand instanceof IdentifierNode) { String varName = sigil + ((IdentifierNode) sigilOp.operand).name; + // Check if this is a declared reference (our \$x) + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); + // Check if already declared in current scope if (hasVariable(varName)) { // Already declared, just return the existing register - lastResultReg = getVariableRegister(varName); + int reg = getVariableRegister(varName); + + // If this is a declared reference, create a reference to it + if (isDeclaredReference && currentCallContext != RuntimeContextType.VOID) { + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(reg); + lastResultReg = refReg; + } else { + lastResultReg = reg; + } return; } @@ -3844,19 +4038,159 @@ private void compileVariableDeclaration(OperatorNode node, String op) { default -> throwCompilerException("Unsupported variable type: " + sigil); } - lastResultReg = reg; + // If this is a declared reference, create a reference to it + if (isDeclaredReference && currentCallContext != RuntimeContextType.VOID) { + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(reg); + lastResultReg = refReg; + } else { + lastResultReg = reg; + } return; } + + // Handle our \\$x - reference to a declared reference (double backslash) + if (sigil.equals("\\")) { + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); + + if (isDeclaredReference) { + // This is our \\$x which means: create a declared reference and then take a reference to it + // The operand is \$x, so we recursively compile it + sigilOp.accept(this); + // The result is now in lastResultReg + return; + } + } } else if (node.operand instanceof ListNode) { // our ($x, $y) - list of package variable declarations ListNode listNode = (ListNode) node.operand; List varRegs = new ArrayList<>(); + // Check if this is a declared reference (our \($x, $y)) + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); + + // Track if we found any backslash operators inside the list + boolean foundBackslashInList = false; + for (Node element : listNode.elements) { if (element instanceof OperatorNode) { OperatorNode sigilOp = (OperatorNode) element; String sigil = sigilOp.operator; + // Handle backslash operator (reference constructor): our (\$x) or our (\($x, $y)) + if (sigil.equals("\\")) { + // Check if it's a double backslash first: our (\\$x) + if (sigilOp.operand instanceof OperatorNode innerBackslash && + innerBackslash.operator.equals("\\")) { + // Double backslash: our (\\$x) + // Recursively compile the inner part + OperatorNode innerOur = new OperatorNode("our", innerBackslash, node.getIndex()); + if (node.annotations != null && Boolean.TRUE.equals(node.annotations.get("isDeclaredReference"))) { + innerOur.setAnnotation("isDeclaredReference", true); + } + innerOur.accept(this); + + // Create a reference to the result + if (currentCallContext != RuntimeContextType.VOID && lastResultReg != -1) { + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(lastResultReg); + varRegs.add(refReg); + } + continue; + } + + // Single backslash - mark that we need to create references later + foundBackslashInList = true; + + // Check if it's a nested list: our (\($d, $e)) + if (sigilOp.operand instanceof ListNode nestedList) { + // Process each element in the nested list as a declared reference + for (Node nestedElement : nestedList.elements) { + if (nestedElement instanceof OperatorNode nestedVarNode && + "$@%".contains(nestedVarNode.operator)) { + if (nestedVarNode.operand instanceof IdentifierNode idNode) { + // For declared refs, variable is always scalar holding a ref + String varName = "$" + idNode.name; + + // Declare and load the package variable + int reg = addVariable(varName, "our"); + String globalVarName = NameNormalizer.normalizeVariableName( + idNode.name, + getCurrentPackage() + ); + int nameIdx = addToStringPool(globalVarName); + + // Load based on original sigil + String originalSigil = nestedVarNode.operator; + switch (originalSigil) { + case "$" -> { + emit(Opcodes.LOAD_GLOBAL_SCALAR); + emitReg(reg); + emit(nameIdx); + } + case "@" -> { + emit(Opcodes.LOAD_GLOBAL_ARRAY); + emitReg(reg); + emit(nameIdx); + } + case "%" -> { + emit(Opcodes.LOAD_GLOBAL_HASH); + emitReg(reg); + emit(nameIdx); + } + } + + varRegs.add(reg); + } + } + } + } else if (sigilOp.operand instanceof OperatorNode varNode && + "$@%".contains(varNode.operator)) { + // Single variable: our (\$x) or our (\@x) or our (\%x) + if (varNode.operand instanceof IdentifierNode idNode) { + // For declared refs, variable is always scalar holding a ref + String varName = "$" + idNode.name; + + // Declare and load the package variable + int reg = addVariable(varName, "our"); + String globalVarName = NameNormalizer.normalizeVariableName( + idNode.name, + getCurrentPackage() + ); + int nameIdx = addToStringPool(globalVarName); + + // Load based on original sigil + String originalSigil = varNode.operator; + switch (originalSigil) { + case "$" -> { + emit(Opcodes.LOAD_GLOBAL_SCALAR); + emitReg(reg); + emit(nameIdx); + } + case "@" -> { + emit(Opcodes.LOAD_GLOBAL_ARRAY); + emitReg(reg); + emit(nameIdx); + } + case "%" -> { + emit(Opcodes.LOAD_GLOBAL_HASH); + emitReg(reg); + emit(nameIdx); + } + } + + varRegs.add(reg); + } + } + continue; + } + if (sigilOp.operand instanceof IdentifierNode) { String varName = sigil + ((IdentifierNode) sigilOp.operand).name; @@ -3905,13 +4239,35 @@ private void compileVariableDeclaration(OperatorNode node, String op) { } } - // Return a list of the declared variables + // Return a list of the declared variables (or their references if isDeclaredReference) int resultReg = allocateRegister(); - emit(Opcodes.CREATE_LIST); - emitReg(resultReg); - emit(varRegs.size()); - for (int varReg : varRegs) { - emitReg(varReg); + + if ((isDeclaredReference || foundBackslashInList) && currentCallContext != RuntimeContextType.VOID) { + // Create references to all variables first + List refRegs = new ArrayList<>(); + for (int varReg : varRegs) { + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(varReg); + refRegs.add(refReg); + } + + // Create a list of the references + emit(Opcodes.CREATE_LIST); + emitReg(resultReg); + emit(refRegs.size()); + for (int refReg : refRegs) { + emitReg(refReg); + } + } else { + // Regular list of variables + emit(Opcodes.CREATE_LIST); + emitReg(resultReg); + emit(varRegs.size()); + for (int varReg : varRegs) { + emitReg(varReg); + } } lastResultReg = resultReg; @@ -3919,11 +4275,13 @@ private void compileVariableDeclaration(OperatorNode node, String op) { } throwCompilerException("Unsupported our operand: " + node.operand.getClass().getSimpleName()); } else if (op.equals("local")) { - // local $x - temporarily localize a global variable // The operand will be OperatorNode("$", IdentifierNode("x")) + // local $x - temporarily localize a global variable + // The operand will be OperatorNode("$", IdentifierNode("x")) if (node.operand instanceof OperatorNode) { OperatorNode sigilOp = (OperatorNode) node.operand; + String sigil = sigilOp.operator; - if (sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode) { + if (sigil.equals("$") && sigilOp.operand instanceof IdentifierNode) { String varName = "$" + ((IdentifierNode) sigilOp.operand).name; // Check if it's a lexical variable (should not be localized) @@ -3932,6 +4290,10 @@ private void compileVariableDeclaration(OperatorNode node, String op) { return; } + // Check if this is a declared reference (local \$x) + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); + // It's a global variable - emit SLOW_OP to call GlobalRuntimeScalar.makeLocal() String packageName = getCurrentPackage(); String globalVarName = packageName + "::" + ((IdentifierNode) sigilOp.operand).name; @@ -3942,9 +4304,253 @@ private void compileVariableDeclaration(OperatorNode node, String op) { emitReg(rd); emit(nameIdx); - lastResultReg = rd; + // If this is a declared reference, create a reference to it + if (isDeclaredReference && currentCallContext != RuntimeContextType.VOID) { + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(rd); + lastResultReg = refReg; + } else { + lastResultReg = rd; + } return; } + + // Handle local \$x or local \\$x - backslash operator + if (sigil.equals("\\")) { + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); + + if (isDeclaredReference) { + // Check if operand is a variable operator ($, @, %) + if (sigilOp.operand instanceof OperatorNode innerOp && + "$@%".contains(innerOp.operator) && + innerOp.operand instanceof IdentifierNode idNode) { + + // For declared refs, always use scalar sigil + String varName = "$" + idNode.name; + + // Check if it's a lexical variable + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + return; + } + + // Localize global variable + String packageName = getCurrentPackage(); + String globalVarName = packageName + "::" + idNode.name; + int nameIdx = addToStringPool(globalVarName); + + int rd = allocateRegister(); + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); + + // Create first reference + int refReg1 = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg1); + emitReg(rd); + + // Check if the backslash operator itself has isDeclaredReference annotation + // which indicates this is \\$x (double backslash) + boolean backslashIsDeclaredRef = sigilOp.annotations != null && + Boolean.TRUE.equals(sigilOp.annotations.get("isDeclaredReference")); + + if (backslashIsDeclaredRef) { + // Double backslash: create second reference + int refReg2 = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg2); + emitReg(refReg1); + lastResultReg = refReg2; + } else { + // Single backslash: just one reference + lastResultReg = refReg1; + } + return; + } + } + } + } else if (node.operand instanceof ListNode) { + // local ($x, $y) - list of localized global variables + ListNode listNode = (ListNode) node.operand; + List varRegs = new ArrayList<>(); + + // Check if this is a declared reference + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); + + // Track if we found any backslash operators inside the list + boolean foundBackslashInList = false; + + for (Node element : listNode.elements) { + if (element instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) element; + String sigil = sigilOp.operator; + + // Handle backslash operator: local (\$x) or local (\($x, $y)) + if (sigil.equals("\\")) { + // Check if backslash itself has isDeclaredReference (indicates \\$ in source) + boolean backslashIsDeclaredRef = sigilOp.annotations != null && + Boolean.TRUE.equals(sigilOp.annotations.get("isDeclaredReference")); + + if (backslashIsDeclaredRef) { + // Double backslash: local (\\$x) + // Localize, create ref, create ref to ref + if (sigilOp.operand instanceof OperatorNode varNode && + "$@%".contains(varNode.operator) && + varNode.operand instanceof IdentifierNode idNode) { + + String varName = "$" + idNode.name; + + // Check if it's a lexical variable + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + } + + // Localize global variable + String packageName = getCurrentPackage(); + String globalVarName = packageName + "::" + idNode.name; + int nameIdx = addToStringPool(globalVarName); + + int rd = allocateRegister(); + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); + + // Create first reference + int refReg1 = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg1); + emitReg(rd); + + // Create second reference + int refReg2 = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg2); + emitReg(refReg1); + + varRegs.add(refReg2); + } + continue; + } + + // Single backslash + foundBackslashInList = true; + + // Check if it's a nested list + if (sigilOp.operand instanceof ListNode nestedList) { + for (Node nestedElement : nestedList.elements) { + if (nestedElement instanceof OperatorNode nestedVarNode && + "$@%".contains(nestedVarNode.operator) && + nestedVarNode.operand instanceof IdentifierNode idNode) { + // For declared refs, always use scalar sigil + String varName = "$" + idNode.name; + + // Check if it's a lexical variable + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + } + + // Localize global variable + String packageName = getCurrentPackage(); + String globalVarName = packageName + "::" + idNode.name; + int nameIdx = addToStringPool(globalVarName); + + int rd = allocateRegister(); + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); + + varRegs.add(rd); + } + } + } else if (sigilOp.operand instanceof OperatorNode varNode && + "$@%".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; + + // Check if it's a lexical variable + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + } + + // Localize global variable + String packageName = getCurrentPackage(); + String globalVarName = packageName + "::" + idNode.name; + int nameIdx = addToStringPool(globalVarName); + + int rd = allocateRegister(); + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); + + varRegs.add(rd); + } + continue; + } + + // Regular scalar variable in list + if (sigil.equals("$") && sigilOp.operand instanceof IdentifierNode idNode) { + String varName = "$" + idNode.name; + + // Check if it's a lexical variable + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + } + + // Localize global variable + String packageName = getCurrentPackage(); + String globalVarName = packageName + "::" + idNode.name; + int nameIdx = addToStringPool(globalVarName); + + int rd = allocateRegister(); + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); + + varRegs.add(rd); + } + } + } + + // Return a list of the localized variables (or their references) + int resultReg = allocateRegister(); + + if ((isDeclaredReference || foundBackslashInList) && currentCallContext != RuntimeContextType.VOID) { + // Create references to all variables first + List refRegs = new ArrayList<>(); + for (int varReg : varRegs) { + int refReg = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg); + emitReg(varReg); + refRegs.add(refReg); + } + + // Create a list of the references + emit(Opcodes.CREATE_LIST); + emitReg(resultReg); + emit(refRegs.size()); + for (int refReg : refRegs) { + emitReg(refReg); + } + } else { + // Regular list of variables + emit(Opcodes.CREATE_LIST); + emitReg(resultReg); + emit(varRegs.size()); + for (int varReg : varRegs) { + emitReg(varReg); + } + } + + lastResultReg = resultReg; + return; } throwCompilerException("Unsupported local operand: " + node.operand.getClass().getSimpleName()); } @@ -4006,6 +4612,19 @@ private void compileVariableReference(OperatorNode node, String op) { emitReg(rd); emitReg(blockResultReg); + lastResultReg = rd; + } else if (node.operand instanceof OperatorNode) { + // Operator dereference: $$x, $${expr}, etc. + // Compile the operand expression (e.g., $x returns a reference) + node.operand.accept(this); + int refReg = lastResultReg; + + // Dereference the result + int rd = allocateRegister(); + emitWithToken(Opcodes.DEREF, node.getIndex()); + emitReg(rd); + emitReg(refReg); + lastResultReg = rd; } else { throwCompilerException("Unsupported $ operand: " + node.operand.getClass().getSimpleName()); @@ -4215,8 +4834,8 @@ public void visit(OperatorNode node) { String op = node.operator; - // Group 1: Variable declarations (my, our, local) - if (op.equals("my") || op.equals("our") || op.equals("local")) { + // Group 1: Variable declarations (my, our, local, state) + if (op.equals("my") || op.equals("our") || op.equals("local") || op.equals("state")) { compileVariableDeclaration(node, op); return; } @@ -6415,6 +7034,56 @@ public void visit(OperatorNode node) { emitReg(argReg); lastResultReg = rd; // GENERATED_OPERATORS_END + } else if (op.equals("tr") || op.equals("y")) { + // tr/// or y/// transliteration operator + // Pattern: tr/search/replace/modifiers on $variable + if (!(node.operand instanceof ListNode)) { + throwCompilerException("tr operator requires list operand"); + } + + ListNode list = (ListNode) node.operand; + if (list.elements.size() < 3) { + throwCompilerException("tr operator requires search, replace, and modifiers"); + } + + // Compile search pattern + list.elements.get(0).accept(this); + int searchReg = lastResultReg; + + // Compile replace pattern + list.elements.get(1).accept(this); + int replaceReg = lastResultReg; + + // Compile modifiers + list.elements.get(2).accept(this); + int modifiersReg = lastResultReg; + + // Compile target variable (element [3] or default to $_) + int targetReg; + if (list.elements.size() > 3 && list.elements.get(3) != null) { + list.elements.get(3).accept(this); + targetReg = lastResultReg; + } else { + // Default to $_ - need to load it + targetReg = allocateRegister(); + String underscoreName = NameNormalizer.normalizeVariableName("_", getCurrentPackage()); + int nameIdx = addToStringPool(underscoreName); + emit(Opcodes.LOAD_GLOBAL_SCALAR); + emitReg(targetReg); + emit(nameIdx); + } + + // Emit TR_TRANSLITERATE operation + int rd = allocateRegister(); + emit(Opcodes.TR_TRANSLITERATE); + emitReg(rd); + emitReg(searchReg); + emitReg(replaceReg); + emitReg(modifiersReg); + emitReg(targetReg); + emitInt(currentCallContext); + + lastResultReg = rd; } else { throwCompilerException("Unsupported operator: " + op); } diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java index b34f79e4e..6c61b45c3 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java @@ -1707,10 +1707,19 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.CREATE_REF: { // Create reference: rd = rs.createReference() + // For multi-element lists, create references to each element int rd = bytecode[pc++]; int rs = bytecode[pc++]; RuntimeBase value = registers[rs]; - registers[rd] = value.createReference(); + + // Special handling for RuntimeList + if (value instanceof RuntimeList list && list.elements.size() != 1) { + // Multi-element or empty list: create list of references + registers[rd] = list.createListReference(); + } else { + // Single value or single-element list: create single reference + registers[rd] = value.createReference(); + } break; } @@ -2158,6 +2167,11 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c case Opcodes.EXIT: pc = ScalarUnaryOpcodeHandler.execute(opcode, bytecode, pc, registers); break; + + case Opcodes.TR_TRANSLITERATE: + pc = SlowOpcodeHandler.executeTransliterate(bytecode, pc, registers); + break; + // GENERATED_HANDLERS_END default: @@ -2305,7 +2319,15 @@ private static int executeTypeOps(short opcode, short[] bytecode, int pc, int rd = bytecode[pc++]; int rs = bytecode[pc++]; RuntimeBase value = registers[rs]; - registers[rd] = value.createReference(); + + // Special handling for RuntimeList + if (value instanceof RuntimeList list && list.elements.size() != 1) { + // Multi-element or empty list: create list of references + registers[rd] = list.createListReference(); + } else { + // Single value or single-element list: create single reference + registers[rd] = value.createReference(); + } return pc; } diff --git a/src/main/java/org/perlonjava/interpreter/InterpretedCode.java b/src/main/java/org/perlonjava/interpreter/InterpretedCode.java index 460fbe200..786c7b361 100644 --- a/src/main/java/org/perlonjava/interpreter/InterpretedCode.java +++ b/src/main/java/org/perlonjava/interpreter/InterpretedCode.java @@ -1231,6 +1231,21 @@ public String disassemble() { case Opcodes.EXIT: pc = ScalarUnaryOpcodeHandler.disassemble(opcode, bytecode, pc, sb); break; + + case Opcodes.TR_TRANSLITERATE: + rd = bytecode[pc++]; + int searchReg = bytecode[pc++]; + int replaceReg = bytecode[pc++]; + int modifiersReg = bytecode[pc++]; + int targetReg = bytecode[pc++]; + int context = readInt(bytecode, pc); + pc += 2; // Skip the 2 shorts that make up the int + sb.append("TR_TRANSLITERATE r").append(rd).append(" = tr(r") + .append(searchReg).append(", r").append(replaceReg).append(", r") + .append(modifiersReg).append(") on r").append(targetReg) + .append(" ctx=").append(context).append("\n"); + break; + // GENERATED_DISASM_END default: diff --git a/src/main/java/org/perlonjava/interpreter/Opcodes.java b/src/main/java/org/perlonjava/interpreter/Opcodes.java index b75bbc08f..1d5312495 100644 --- a/src/main/java/org/perlonjava/interpreter/Opcodes.java +++ b/src/main/java/org/perlonjava/interpreter/Opcodes.java @@ -817,10 +817,20 @@ public class Opcodes { * labelIndex: index into stringPool for label name (or -1 for unlabeled) */ public static final short REDO = 220; + /** Transliterate operator: Apply tr/// or y/// pattern to string + * Format: TR_TRANSLITERATE rd searchReg replaceReg modifiersReg targetReg context + * rd: destination register for result (count of transliterated characters) + * searchReg: register containing search pattern (RuntimeScalar) + * replaceReg: register containing replacement pattern (RuntimeScalar) + * modifiersReg: register containing modifiers string (RuntimeScalar) + * targetReg: register containing target variable to modify (RuntimeScalar) + * context: call context (SCALAR/LIST/VOID) */ + public static final short TR_TRANSLITERATE = 221; + // ================================================================= // BUILT-IN FUNCTION OPCODES - after LASTOP // Last manually-assigned opcode (for tool reference) - private static final short LASTOP = 220; + private static final short LASTOP = 221; // ================================================================= // Generated by dev/tools/generate_opcode_handlers.pl diff --git a/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java index a6b552ebe..d427ab733 100644 --- a/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java @@ -1,5 +1,6 @@ package org.perlonjava.interpreter; +import org.perlonjava.operators.RuntimeTransliterate; import org.perlonjava.runtime.*; /** @@ -970,6 +971,38 @@ public static int executeLength( return pc; } + /** + * TR_TRANSLITERATE: rd = tr///pattern applied to target + * Format: [TR_TRANSLITERATE] [rd] [searchReg] [replaceReg] [modifiersReg] [targetReg] [context] + * Effect: Applies transliteration pattern to target variable + * Returns: Count of transliterated characters + */ + public static int executeTransliterate( + short[] bytecode, + int pc, + RuntimeBase[] registers) { + + int rd = bytecode[pc++]; + int searchReg = bytecode[pc++]; + int replaceReg = bytecode[pc++]; + int modifiersReg = bytecode[pc++]; + int targetReg = bytecode[pc++]; + + // Read context (4 bytes = 1 int) + int context = ((bytecode[pc++] & 0xFFFF) << 16) | (bytecode[pc++] & 0xFFFF); + + RuntimeScalar search = (RuntimeScalar) registers[searchReg]; + RuntimeScalar replace = (RuntimeScalar) registers[replaceReg]; + RuntimeScalar modifiers = (RuntimeScalar) registers[modifiersReg]; + RuntimeScalar target = (RuntimeScalar) registers[targetReg]; + + // Compile and apply transliteration + RuntimeTransliterate tr = RuntimeTransliterate.compile(search, replace, modifiers); + registers[rd] = tr.transliterate(target, context); + + return pc; + } + private SlowOpcodeHandler() { // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/runtime/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/RuntimeCode.java index 6b5d63b5d..a57dd742b 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/RuntimeCode.java @@ -54,6 +54,17 @@ public class RuntimeCode extends RuntimeBase implements RuntimeScalarReference { public static final boolean EVAL_USE_INTERPRETER = System.getenv("JPERL_EVAL_USE_INTERPRETER") != null; + /** + * Flag to control whether eval compilation errors should be printed to stderr. + * By default, eval failures are silent (errors only stored in $@). + * + * Set environment variable JPERL_EVAL_VERBOSE=1 to enable verbose error reporting. + * This is useful for debugging eval compilation issues, especially when testing + * the interpreter path. + */ + public static final boolean EVAL_VERBOSE = + System.getenv("JPERL_EVAL_VERBOSE") != null; + /** * ThreadLocal storage for runtime values of captured variables during eval STRING compilation. * @@ -454,6 +465,14 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje RuntimeScalar err = GlobalVariable.getGlobalVariable("main::@"); err.set(e.getMessage()); + // If EVAL_VERBOSE is set, print the error to stderr for debugging + if (EVAL_VERBOSE) { + System.err.println("eval compilation error: " + e.getMessage()); + if (e.getCause() != null) { + System.err.println("Caused by: " + e.getCause().getMessage()); + } + } + // Check if $SIG{__DIE__} handler is defined RuntimeScalar sig = GlobalVariable.getGlobalHash("main::SIG").get("__DIE__"); if (sig.getDefinedBoolean()) { @@ -801,6 +820,14 @@ public static RuntimeList evalStringWithInterpreter( RuntimeScalar err = GlobalVariable.getGlobalVariable("main::@"); err.set(e.getMessage()); + // If EVAL_VERBOSE is set, print the error to stderr for debugging + if (EVAL_VERBOSE) { + System.err.println("eval compilation error: " + e.getMessage()); + if (e.getCause() != null) { + System.err.println("Caused by: " + e.getCause().getMessage()); + } + } + // Check if $SIG{__DIE__} handler is defined RuntimeScalar sig = GlobalVariable.getGlobalHash("main::SIG").get("__DIE__"); if (sig.getDefinedBoolean()) {