diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java index 6e8a00636..2afe2e686 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java @@ -3,12 +3,9 @@ import org.perlonjava.astnode.*; import org.perlonjava.astvisitor.Visitor; import org.perlonjava.codegen.EmitterContext; -import org.perlonjava.lexer.LexerToken; import org.perlonjava.runtime.*; import org.perlonjava.symbols.ScopedSymbolTable; -import org.perlonjava.symbols.SymbolTable; -import java.io.ByteArrayOutputStream; import java.util.*; /** @@ -26,7 +23,7 @@ public class BytecodeCompiler implements Visitor { // Pre-allocate with reasonable initial capacity to reduce resizing // Typical small eval/subroutine needs 20-50 bytecodes, 5-10 constants, 3-8 strings - private final List bytecode = new ArrayList<>(64); + final List bytecode = new ArrayList<>(64); private final List constants = new ArrayList<>(16); private final List stringPool = new ArrayList<>(16); private final Map stringPoolIndex = new HashMap<>(16); // O(1) lookup @@ -34,11 +31,11 @@ public class BytecodeCompiler implements Visitor { // Simple variable-to-register mapping for the interpreter // Each scope is a Map mapping variable names to register indices - private final Stack> variableScopes = new Stack<>(); + final Stack> variableScopes = new Stack<>(); // Symbol table for package/class tracking // Tracks current package, class flag, and package versions like the compiler does - private final ScopedSymbolTable symbolTable = new ScopedSymbolTable(); + final ScopedSymbolTable symbolTable = new ScopedSymbolTable(); // Stack to save/restore register state when entering/exiting scopes private final Stack savedNextRegister = new Stack<>(); @@ -71,10 +68,10 @@ private static class LoopInfo { // Token index tracking for error reporting private final TreeMap pcToTokenIndex = new TreeMap<>(); - private int currentTokenIndex = -1; // Track current token for error reporting + int currentTokenIndex = -1; // Track current token for error reporting // Error reporting - private final ErrorMessageUtil errorUtil; + final ErrorMessageUtil errorUtil; // EmitterContext for strict checks and other compile-time options private EmitterContext emitterContext; @@ -85,27 +82,27 @@ private static class LoopInfo { private int maxRegisterEverUsed = 2; // Track highest register ever allocated // Track last result register for expression chaining - private int lastResultReg = -1; + int lastResultReg = -1; // Track current calling context for subroutine calls - private int currentCallContext = RuntimeContextType.LIST; // Default to LIST + int currentCallContext = RuntimeContextType.LIST; // Default to LIST // Closure support private RuntimeBase[] capturedVars; // Captured variable values private String[] capturedVarNames; // Parallel array of names - private Map capturedVarIndices; // Name → register index + Map capturedVarIndices; // Name → register index // Track ALL variables ever declared (for variableRegistry) // This is needed because inner scopes get popped before variableRegistry is built - private final Map allDeclaredVariables = new HashMap<>(); + final Map allDeclaredVariables = new HashMap<>(); // BEGIN support for named subroutine closures private int currentSubroutineBeginId = 0; // BEGIN ID for current named subroutine (0 = not in named sub) private Set currentSubroutineClosureVars = new HashSet<>(); // Variables captured from outer scope // Source information - private final String sourceName; - private final int sourceLine; + final String sourceName; + final int sourceLine; public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil errorUtil) { this.sourceName = sourceName; @@ -182,7 +179,7 @@ public BytecodeCompiler(String sourceName, int sourceLine, ErrorMessageUtil erro /** * Helper: Check if a variable exists in any scope. */ - private boolean hasVariable(String name) { + boolean hasVariable(String name) { for (int i = variableScopes.size() - 1; i >= 0; i--) { if (variableScopes.get(i).containsKey(name)) { return true; @@ -195,7 +192,7 @@ private boolean hasVariable(String name) { * Helper: Get the register index for a variable. * Returns -1 if not found. */ - private int getVariableRegister(String name) { + int getVariableRegister(String name) { for (int i = variableScopes.size() - 1; i >= 0; i--) { Integer reg = variableScopes.get(i).get(name); if (reg != null) { @@ -209,7 +206,7 @@ private int getVariableRegister(String name) { * Helper: Add a variable to the current scope and return its register index. * Allocates a new register. */ - private int addVariable(String name, String declType) { + int addVariable(String name, String declType) { int reg = allocateRegister(); variableScopes.peek().put(name, reg); allDeclaredVariables.put(name, reg); // Track for variableRegistry @@ -251,7 +248,7 @@ private void exitScope() { * Helper: Get current package name for global variable resolution. * Uses symbolTable for proper package/class tracking. */ - private String getCurrentPackage() { + String getCurrentPackage() { return symbolTable.getCurrentPackage(); } @@ -358,7 +355,7 @@ private boolean isNonAsciiLengthOneScalarAllowedUnderNoUtf8(String sigil, String * @param varName The variable name with sigil (e.g., "$A", "@array") * @return true if access should be blocked under strict vars */ - private boolean shouldBlockGlobalUnderStrictVars(String varName) { + boolean shouldBlockGlobalUnderStrictVars(String varName) { // Only check if strict vars is enabled if (emitterContext == null || emitterContext.symbolTable == null) { return false; // No context, allow access @@ -416,7 +413,7 @@ private boolean shouldBlockGlobalUnderStrictVars(String varName) { * @param message The error message * @param tokenIndex The token index where the error occurred */ - private void throwCompilerException(String message, int tokenIndex) { + void throwCompilerException(String message, int tokenIndex) { if (errorUtil != null && tokenIndex >= 0) { throw new PerlCompilerException(tokenIndex, message, errorUtil); } else { @@ -425,83 +422,12 @@ private void throwCompilerException(String message, int tokenIndex) { } } - /** - * Handle loop control operators: last, next, redo - * Emits appropriate opcode with label reference - * - * @param node The operator node - * @param op The operator name (last/next/redo) - */ - private void handleLoopControlOperator(OperatorNode node, String op) { - // Extract label if present - String labelStr = null; - if (node.operand instanceof ListNode labelNode && !labelNode.elements.isEmpty()) { - Node arg = labelNode.elements.getFirst(); - if (arg instanceof IdentifierNode) { - labelStr = ((IdentifierNode) arg).name; - } else { - throwCompilerException("Not implemented: " + node, node.getIndex()); - } - } - - // Find the target loop - LoopInfo targetLoop = null; - if (labelStr == null) { - // Unlabeled: find innermost loop - if (!loopStack.isEmpty()) { - targetLoop = loopStack.peek(); - } - } else { - // Labeled: search for matching label - for (int i = loopStack.size() - 1; i >= 0; i--) { - LoopInfo loop = loopStack.get(i); - if (labelStr.equals(loop.label)) { - targetLoop = loop; - break; - } - } - } - - if (targetLoop == null) { - // No matching loop found - non-local control flow - // For now, throw an error. Later we can implement RuntimeControlFlowList - if (labelStr != null) { - throwCompilerException("Can't find label \"" + labelStr + "\"", node.getIndex()); - } else { - throwCompilerException("Can't \"" + op + "\" outside a loop block", node.getIndex()); - } - } - - // Check if this is a pseudo-loop (do-while/bare block) which doesn't support last/next/redo - if (!targetLoop.isTrueLoop) { - throwCompilerException("Can't \"" + op + "\" outside a loop block", node.getIndex()); - } - - // Emit the opcode and record the PC to be patched later - short opcode = op.equals("last") ? Opcodes.LAST - : op.equals("next") ? Opcodes.NEXT - : Opcodes.REDO; - - emitWithToken(opcode, node.getIndex()); - int jumpPc = bytecode.size(); - emitInt(0); // Placeholder for jump target (will be patched) - - // Record this PC in the appropriate list for later patching - if (op.equals("last")) { - targetLoop.breakPcs.add(jumpPc); - } else if (op.equals("next")) { - targetLoop.nextPcs.add(jumpPc); - } else { // redo - targetLoop.redoPcs.add(jumpPc); - } - } - /** * Throw a compiler exception using the current token index. * * @param message The error message */ - private void throwCompilerException(String message) { + void throwCompilerException(String message) { throwCompilerException(message, currentTokenIndex); } @@ -929,7 +855,7 @@ public void visit(IdentifierNode node) { * Handle array element access: $array[index] * Example: $ARGV[0] or $array[$i] */ - private void handleArrayElementAccess(BinaryOperatorNode node, OperatorNode leftOp) { + void handleArrayElementAccess(BinaryOperatorNode node, OperatorNode leftOp) { // Get the array variable name if (!(leftOp.operand instanceof IdentifierNode)) { throwCompilerException("Array element access requires identifier"); @@ -990,7 +916,7 @@ private void handleArrayElementAccess(BinaryOperatorNode node, OperatorNode left * Handle array slice: @array[indices] * Example: @array[0,2,4] or @array[1..5] */ - private void handleArraySlice(BinaryOperatorNode node, OperatorNode leftOp) { + void handleArraySlice(BinaryOperatorNode node, OperatorNode leftOp) { int arrayReg; // Get the array - either direct @array or dereferenced @$arrayref @@ -1076,7 +1002,7 @@ private void handleArraySlice(BinaryOperatorNode node, OperatorNode leftOp) { * Handle hash element access: $hash{key} * Example: $hash{foo} or $hash{$key} */ - private void handleHashElementAccess(BinaryOperatorNode node, OperatorNode leftOp) { + void handleHashElementAccess(BinaryOperatorNode node, OperatorNode leftOp) { // Get the hash variable name if (!(leftOp.operand instanceof IdentifierNode)) { throwCompilerException("Hash element access requires identifier"); @@ -1154,7 +1080,7 @@ private void handleHashElementAccess(BinaryOperatorNode node, OperatorNode leftO } } - private void handleHashSlice(BinaryOperatorNode node, OperatorNode leftOp) { + void handleHashSlice(BinaryOperatorNode node, OperatorNode leftOp) { int hashReg; // Hash slice: @hash{'key1', 'key2'} returns array of values @@ -1248,7 +1174,7 @@ private void handleHashSlice(BinaryOperatorNode node, OperatorNode leftOp) { * Handle compound assignment operators: +=, -=, *=, /=, %= * Example: $sum += $elem means $sum = $sum + $elem */ - private void handleCompoundAssignment(BinaryOperatorNode node) { + void handleCompoundAssignment(BinaryOperatorNode node) { String op = node.operator; // Compile the right operand first (the value to add/subtract/etc.) @@ -1362,7 +1288,7 @@ private void handleCompoundAssignment(BinaryOperatorNode node) { * Handle general array access: expr[index] * Example: $matrix[1][0] where $matrix[1] returns an arrayref */ - private void handleGeneralArrayAccess(BinaryOperatorNode node) { + void handleGeneralArrayAccess(BinaryOperatorNode node) { // Compile the left side (the expression that should yield an array or arrayref) node.left.accept(this); int baseReg = lastResultReg; @@ -1413,7 +1339,7 @@ private void handleGeneralArrayAccess(BinaryOperatorNode node) { * Handle general hash access: expr{key} * Example: $hash{outer}{inner} where $hash{outer} returns a hashref */ - private void handleGeneralHashAccess(BinaryOperatorNode node) { + void handleGeneralHashAccess(BinaryOperatorNode node) { // Compile the left side (the expression that should yield a hash or hashref) node.left.accept(this); int baseReg = lastResultReg; @@ -1494,7 +1420,7 @@ private void handleGeneralHashAccess(BinaryOperatorNode node) { * Handle push/unshift operators: push @array, values or unshift @array, values * Example: push @arr, 1, 2, 3 */ - private void handlePushUnshift(BinaryOperatorNode node) { + void handlePushUnshift(BinaryOperatorNode node) { boolean isPush = node.operator.equals("push"); // Left operand is the array (@array or @$arrayref) @@ -1571,5874 +1497,1223 @@ private void handlePushUnshift(BinaryOperatorNode node) { lastResultReg = -1; // No result register needed } + @Override + public void visit(BinaryOperatorNode node) { + CompileBinaryOperator.visitBinaryOperator(this, node); + } + /** - * Helper method to compile assignment operators (=). - * Extracted from visit(BinaryOperatorNode) to reduce method size. - * Handles all forms of assignment including my/our/local, scalars, arrays, hashes, and slices. + * Compile variable declaration operators (my, our, local, state). + * Extracted to reduce visit(OperatorNode) bytecode size. */ - private void compileAssignmentOperator(BinaryOperatorNode node) { - // Determine the calling context for the RHS based on LHS type - int rhsContext = RuntimeContextType.LIST; // Default - - // Check if LHS is a scalar assignment (my $x = ... or our $x = ...) - if (node.left instanceof OperatorNode) { - OperatorNode leftOp = (OperatorNode) node.left; - if ((leftOp.operator.equals("my") || leftOp.operator.equals("our")) && leftOp.operand instanceof OperatorNode) { - OperatorNode sigilOp = (OperatorNode) leftOp.operand; - if (sigilOp.operator.equals("$")) { - // Scalar assignment: use SCALAR context for RHS - rhsContext = RuntimeContextType.SCALAR; - } - } else if (leftOp.operator.equals("$")) { - // Regular scalar assignment: $x = ... - rhsContext = RuntimeContextType.SCALAR; - } - } + void compileVariableDeclaration(OperatorNode node, String op) { + 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; + String sigil = sigilOp.operator; - // Set the context for subroutine calls in RHS - int savedContext = currentCallContext; - try { - currentCallContext = rhsContext; + if (sigilOp.operand instanceof IdentifierNode) { + String varName = sigil + ((IdentifierNode) sigilOp.operand).name; - // Special case: my $x = value - if (node.left instanceof OperatorNode) { - OperatorNode leftOp = (OperatorNode) node.left; - if (leftOp.operator.equals("my")) { - // Extract variable name from "my" operand - Node myOperand = leftOp.operand; - - // Handle my $x (where $x is OperatorNode("$", IdentifierNode("x"))) - if (myOperand instanceof OperatorNode) { - OperatorNode sigilOp = (OperatorNode) myOperand; - if (sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode) { - String varName = "$" + ((IdentifierNode) sigilOp.operand).name; - - // Check if this variable is captured by named subs (Parser marks with id) - if (sigilOp.id != 0) { - // RETRIEVE the persistent variable (creates if doesn't exist) - int beginId = sigilOp.id; - int nameIdx = addToStringPool(varName); - int reg = allocateRegister(); - - emitWithToken(Opcodes.RETRIEVE_BEGIN_SCALAR, node.getIndex()); - emitReg(reg); - emit(nameIdx); - emit(beginId); + // Check if this is a declared reference (my \$x or state \$x) + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); - // Now register contains a reference to the persistent RuntimeScalar - // Store the initializer value INTO that RuntimeScalar - node.right.accept(this); - int valueReg = lastResultReg; + // 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 - // Set the value in the persistent scalar using SET_SCALAR - // This calls .set() on the RuntimeScalar without overwriting the reference - emit(Opcodes.SET_SCALAR); - emitReg(reg); - emitReg(valueReg); + // For state variables, retrieve or initialize the persistent variable + // For captured variables, retrieve the BEGIN-initialized variable + int reg = allocateRegister(); + int nameIdx = addToStringPool(varName); - // Track this variable - map the name to the register we already allocated - variableScopes.peek().put(varName, reg); + switch (sigil) { + case "$" -> { + emitWithToken(Opcodes.RETRIEVE_BEGIN_SCALAR, node.getIndex()); + emitReg(reg); + emit(nameIdx); + emit(sigilOp.id); + // 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 - lastResultReg = reg; - return; - } - - // Regular lexical variable (not captured) - // Allocate register for new lexical variable and add to symbol table - int reg = addVariable(varName, "my"); - - // Compile RHS in the appropriate context - // @ operator will check currentCallContext and emit ARRAY_SIZE if needed - node.right.accept(this); - int valueReg = lastResultReg; - - // Move to variable register - emit(Opcodes.MOVE); - emitReg(reg); - emitReg(valueReg); - - lastResultReg = reg; - return; - } else if (sigilOp.operator.equals("@") && sigilOp.operand instanceof IdentifierNode) { - // Handle my @array = ... - String varName = "@" + ((IdentifierNode) sigilOp.operand).name; - - // Check if this variable is captured by named subs - if (sigilOp.id != 0) { - // RETRIEVE the persistent array - int beginId = sigilOp.id; - int nameIdx = addToStringPool(varName); - int arrayReg = allocateRegister(); - - emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); - emitReg(arrayReg); - emit(nameIdx); - emit(beginId); - - // Compile RHS (should evaluate to a list) - node.right.accept(this); - int listReg = lastResultReg; - - // Populate array from list - emit(Opcodes.ARRAY_SET_FROM_LIST); - emitReg(arrayReg); - emitReg(listReg); - - // Track this variable - map the name to the register we already allocated - variableScopes.peek().put(varName, arrayReg); - allDeclaredVariables.put(varName, arrayReg); // Track for variableRegistry - - // In scalar context, return the count of elements assigned - // In list/void context, return the array - if (currentCallContext == RuntimeContextType.SCALAR) { - int countReg = allocateRegister(); - emit(Opcodes.ARRAY_SIZE); - emitReg(countReg); - emitReg(listReg); - lastResultReg = countReg; - } else { - lastResultReg = arrayReg; } - return; + case "@" -> { + emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); + emitReg(reg); + emit(nameIdx); + emit(sigilOp.id); + variableScopes.peek().put(varName, reg); + allDeclaredVariables.put(varName, reg); // Track for variableRegistry + } + case "%" -> { + emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); + emitReg(reg); + emit(nameIdx); + emit(sigilOp.id); + variableScopes.peek().put(varName, reg); + allDeclaredVariables.put(varName, reg); // Track for variableRegistry + } + default -> throwCompilerException("Unsupported variable type: " + sigil); } - // Regular lexical array (not captured) - // Allocate register for new lexical array and add to symbol table - int arrayReg = addVariable(varName, "my"); - - // Create empty array - emit(Opcodes.NEW_ARRAY); - emitReg(arrayReg); - - // Compile RHS (should evaluate to a list) - node.right.accept(this); - int listReg = lastResultReg; - - // Populate array from list using setFromList - emit(Opcodes.ARRAY_SET_FROM_LIST); - emitReg(arrayReg); - emitReg(listReg); - - // In scalar context, return the count of elements assigned - // In list/void context, return the array - if (currentCallContext == RuntimeContextType.SCALAR) { - int countReg = allocateRegister(); - emit(Opcodes.ARRAY_SIZE); - emitReg(countReg); - emitReg(listReg); - lastResultReg = countReg; + // 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 = arrayReg; + lastResultReg = reg; } return; - } else if (sigilOp.operator.equals("%") && sigilOp.operand instanceof IdentifierNode) { - // Handle my %hash = ... - String varName = "%" + ((IdentifierNode) sigilOp.operand).name; - - // Check if this variable is captured by named subs - if (sigilOp.id != 0) { - // RETRIEVE the persistent hash - int beginId = sigilOp.id; - int nameIdx = addToStringPool(varName); - int hashReg = allocateRegister(); - - emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); - emitReg(hashReg); - emit(nameIdx); - emit(beginId); - - // Compile RHS (should evaluate to a list) - node.right.accept(this); - int listReg = lastResultReg; + } - // Populate hash from list - emit(Opcodes.HASH_SET_FROM_LIST); - emitReg(hashReg); - emitReg(listReg); + // Regular lexical variable (not captured, not state) + int reg = addVariable(varName, "my"); - // Track this variable - map the name to the register we already allocated - variableScopes.peek().put(varName, hashReg); - allDeclaredVariables.put(varName, hashReg); // Track for variableRegistry - lastResultReg = hashReg; - return; + // Normal initialization: load undef/empty array/empty hash + switch (sigil) { + case "$" -> { + emit(Opcodes.LOAD_UNDEF); + emitReg(reg); } + case "@" -> { + emit(Opcodes.NEW_ARRAY); + emitReg(reg); + } + case "%" -> { + emit(Opcodes.NEW_HASH); + emitReg(reg); + } + default -> throwCompilerException("Unsupported variable type: " + sigil); + } - // Regular lexical hash (not captured) - // Allocate register for new lexical hash and add to symbol table - int hashReg = addVariable(varName, "my"); - - // Create empty hash - emit(Opcodes.NEW_HASH); - emitReg(hashReg); - - // Compile RHS (should evaluate to a list) - node.right.accept(this); - int listReg = lastResultReg; + // 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; + } - // Populate hash from list - emit(Opcodes.HASH_SET_FROM_LIST); - emitReg(hashReg); - emitReg(listReg); + // 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")); - lastResultReg = hashReg; + 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<>(); - // Handle my x (direct identifier without sigil) - if (myOperand instanceof IdentifierNode) { - String varName = ((IdentifierNode) myOperand).name; - - // Allocate register for new lexical variable and add to symbol table - int reg = addVariable(varName, "my"); - - // Compile RHS - node.right.accept(this); - int valueReg = lastResultReg; + // Check if this is a declared reference (my \($x, $y)) + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); - // Move to variable register - emit(Opcodes.MOVE); - emitReg(reg); - emitReg(valueReg); + // Track if we found any backslash operators inside the list (my (\($x, $y))) + boolean foundBackslashInList = false; - lastResultReg = reg; - return; - } + for (Node element : listNode.elements) { + if (element instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) element; + String sigil = sigilOp.operator; - // Handle my ($x, $y, @rest) = ... - list declaration with assignment - if (myOperand instanceof ListNode) { - ListNode listNode = (ListNode) myOperand; + // 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 - // Compile RHS first - node.right.accept(this); - int listReg = lastResultReg; + // 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); - // Convert to list if needed - int rhsListReg = allocateRegister(); - emit(Opcodes.SCALAR_TO_LIST); - emitReg(rhsListReg); - emitReg(listReg); + // 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; + } - // Declare and assign each variable - for (int i = 0; i < listNode.elements.size(); i++) { - Node element = listNode.elements.get(i); - if (element instanceof OperatorNode) { - OperatorNode sigilOp = (OperatorNode) element; - String sigil = sigilOp.operator; + // Single backslash - mark that we need to create references later + foundBackslashInList = true; - if (sigilOp.operand instanceof IdentifierNode) { - String varName = sigil + ((IdentifierNode) sigilOp.operand).name; + // 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; - int varReg; + // Declare the variable + int reg = addVariable(varName, op); - // Check if this variable is captured by named subs (Parser marks with id) - if (sigilOp.id != 0) { - // This variable is captured - use RETRIEVE_BEGIN to get persistent storage - int beginId = sigilOp.id; - int nameIdx = addToStringPool(varName); - varReg = allocateRegister(); + // 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); + } + } - switch (sigil) { - case "$" -> { - emitWithToken(Opcodes.RETRIEVE_BEGIN_SCALAR, node.getIndex()); - emitReg(varReg); - emit(nameIdx); - emit(beginId); - } - case "@" -> { - emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); - emitReg(varReg); - emit(nameIdx); - emit(beginId); - } - case "%" -> { - emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); - emitReg(varReg); - emit(nameIdx); - emit(beginId); + 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; - // Track this variable - variableScopes.peek().put(varName, varReg); - allDeclaredVariables.put(varName, varReg); // Track for variableRegistry - } else { - // Regular lexical variable (not captured) // Declare the variable - varReg = addVariable(varName, "my"); + int reg = addVariable(varName, op); - // Initialize based on sigil - switch (sigil) { + // Initialize based on original sigil + String originalSigil = varNode.operator; + switch (originalSigil) { case "$" -> { emit(Opcodes.LOAD_UNDEF); - emitReg(varReg); + emitReg(reg); } case "@" -> { + // Create an array ref emit(Opcodes.NEW_ARRAY); - emitReg(varReg); + emitReg(reg); } case "%" -> { + // Create a hash ref emit(Opcodes.NEW_HASH); - emitReg(varReg); + emitReg(reg); } } - } - // Get i-th element from RHS - int indexReg = allocateRegister(); - emit(Opcodes.LOAD_INT); - emitReg(indexReg); - emitInt(i); - - int elemReg = allocateRegister(); - emit(Opcodes.ARRAY_GET); - emitReg(elemReg); - emitReg(rhsListReg); - emitReg(indexReg); - - // Assign to variable - if (sigil.equals("$")) { - if (sigilOp.id != 0) { - // Captured variable - use SET_SCALAR to preserve aliasing - emit(Opcodes.SET_SCALAR); - emitReg(varReg); - emitReg(elemReg); - } else { - // Regular variable - use MOVE - emit(Opcodes.MOVE); - emitReg(varReg); - emitReg(elemReg); - } - } else if (sigil.equals("@")) { - emit(Opcodes.ARRAY_SET_FROM_LIST); - emitReg(varReg); - emitReg(elemReg); - } else if (sigil.equals("%")) { - emit(Opcodes.HASH_SET_FROM_LIST); - emitReg(varReg); - emitReg(elemReg); + varRegs.add(reg); } } + continue; } - } - - lastResultReg = rhsListReg; - return; - } - } - - // Special case: local $x = value - if (leftOp.operator.equals("local")) { - // Extract variable from "local" operand - Node localOperand = leftOp.operand; - - // Handle local $hash{key} = value (localizing hash element) - if (localOperand instanceof BinaryOperatorNode) { - BinaryOperatorNode hashAccess = (BinaryOperatorNode) localOperand; - if (hashAccess.operator.equals("{")) { - // Compile the hash access to get the hash element reference - // This returns a RuntimeScalar that is aliased to the hash slot - hashAccess.accept(this); - int elemReg = lastResultReg; - - // Push this hash element to the local variable stack - emit(Opcodes.PUSH_LOCAL_VARIABLE); - emitReg(elemReg); - - // Compile RHS - node.right.accept(this); - int valueReg = lastResultReg; - - // Assign value to the hash element (which is already localized) - emit(Opcodes.SET_SCALAR); - emitReg(elemReg); - emitReg(valueReg); - - lastResultReg = elemReg; - return; - } - } - - // Handle local $x (where $x is OperatorNode("$", IdentifierNode("x"))) - if (localOperand instanceof OperatorNode) { - OperatorNode sigilOp = (OperatorNode) localOperand; - if (sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode) { - String varName = "$" + ((IdentifierNode) sigilOp.operand).name; - - // Check if it's a lexical variable (should not be localized) - if (hasVariable(varName)) { - throwCompilerException("Can't localize lexical variable " + varName); - return; - } - - // Compile RHS first - node.right.accept(this); - int valueReg = lastResultReg; - - // It's a global variable - call makeLocal which returns the localized scalar - String packageName = getCurrentPackage(); - String globalVarName = packageName + "::" + ((IdentifierNode) sigilOp.operand).name; - int nameIdx = addToStringPool(globalVarName); - - int localReg = allocateRegister(); - emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); - emitReg(localReg); - emit(nameIdx); - - // Assign value to the localized scalar (not to the global!) - emit(Opcodes.SET_SCALAR); - emitReg(localReg); - emitReg(valueReg); - - lastResultReg = localReg; - return; - } else if (sigilOp.operator.equals("@") && sigilOp.operand instanceof IdentifierNode) { - // Handle local @array = value - String varName = "@" + ((IdentifierNode) sigilOp.operand).name; - - // Check if it's a lexical variable (should not be localized) - if (hasVariable(varName)) { - throwCompilerException("Can't localize lexical variable " + varName); - return; - } - - // Compile RHS first - node.right.accept(this); - int valueReg = lastResultReg; - - // It's a global array - get it and push to local stack - String packageName = getCurrentPackage(); - String globalVarName = packageName + "::" + ((IdentifierNode) sigilOp.operand).name; - int nameIdx = addToStringPool(globalVarName); - - int arrayReg = allocateRegister(); - emit(Opcodes.LOAD_GLOBAL_ARRAY); - emitReg(arrayReg); - emit(nameIdx); - - // Push to local variable stack - emit(Opcodes.PUSH_LOCAL_VARIABLE); - emitReg(arrayReg); - - // Populate array from list - emit(Opcodes.ARRAY_SET_FROM_LIST); - emitReg(arrayReg); - emitReg(valueReg); - - lastResultReg = arrayReg; - return; - } else if (sigilOp.operator.equals("%") && sigilOp.operand instanceof IdentifierNode) { - // Handle local %hash = value - String varName = "%" + ((IdentifierNode) sigilOp.operand).name; - - // Check if it's a lexical variable (should not be localized) - if (hasVariable(varName)) { - throwCompilerException("Can't localize lexical variable " + varName); - return; - } - - // Compile RHS first - node.right.accept(this); - int valueReg = lastResultReg; - - // It's a global hash - get it and push to local stack - String packageName = getCurrentPackage(); - String globalVarName = packageName + "::" + ((IdentifierNode) sigilOp.operand).name; - int nameIdx = addToStringPool(globalVarName); - - int hashReg = allocateRegister(); - emit(Opcodes.LOAD_GLOBAL_HASH); - emitReg(hashReg); - emit(nameIdx); - // Push to local variable stack - emit(Opcodes.PUSH_LOCAL_VARIABLE); - emitReg(hashReg); + if (sigilOp.operand instanceof IdentifierNode) { + String varName = sigil + ((IdentifierNode) sigilOp.operand).name; - // Populate hash from list - emit(Opcodes.HASH_SET_FROM_LIST); - emitReg(hashReg); - emitReg(valueReg); + // 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); - lastResultReg = hashReg; - return; - } - } else if (localOperand instanceof ListNode) { - // Handle local($x) = value or local($x, $y) = (v1, v2) - ListNode listNode = (ListNode) localOperand; - - // Special case: single element list local($x) = value - if (listNode.elements.size() == 1) { - Node element = listNode.elements.get(0); - if (element instanceof OperatorNode) { - OperatorNode sigilOp = (OperatorNode) element; - if (sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode) { - String varName = "$" + ((IdentifierNode) sigilOp.operand).name; - - // Check if it's a lexical variable (should not be localized) - if (hasVariable(varName)) { - throwCompilerException("Can't localize lexical variable " + varName); - return; + switch (sigil) { + case "$" -> { + emitWithToken(Opcodes.RETRIEVE_BEGIN_SCALAR, node.getIndex()); + emitReg(reg); + emit(nameIdx); + emit(sigilOp.id); + variableScopes.peek().put(varName, reg); + allDeclaredVariables.put(varName, reg); // Track for variableRegistry + } + case "@" -> { + emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); + emitReg(reg); + emit(nameIdx); + emit(sigilOp.id); + variableScopes.peek().put(varName, reg); + allDeclaredVariables.put(varName, reg); // Track for variableRegistry + } + case "%" -> { + emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); + emitReg(reg); + emit(nameIdx); + emit(sigilOp.id); + variableScopes.peek().put(varName, reg); + allDeclaredVariables.put(varName, reg); // Track for variableRegistry + } + default -> throwCompilerException("Unsupported variable type in list declaration: " + sigil); } - // Compile RHS first - node.right.accept(this); - int valueReg = lastResultReg; - - // Get the global variable and localize it - String packageName = getCurrentPackage(); - String globalVarName = packageName + "::" + ((IdentifierNode) sigilOp.operand).name; - int nameIdx = addToStringPool(globalVarName); - - int localReg = allocateRegister(); - emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); - emitReg(localReg); - emit(nameIdx); - - // Assign value to the localized variable - emit(Opcodes.SET_SCALAR); - emitReg(localReg); - emitReg(valueReg); - - lastResultReg = localReg; - return; - } - } - } - - // Multi-element case: local($x, $y) = (v1, v2) - // Compile RHS first - node.right.accept(this); - int valueReg = lastResultReg; - - // For each element in the list, localize and assign - for (int i = 0; i < listNode.elements.size(); i++) { - Node element = listNode.elements.get(i); - - if (element instanceof OperatorNode) { - OperatorNode sigilOp = (OperatorNode) element; - if (sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode) { - String varName = "$" + ((IdentifierNode) sigilOp.operand).name; + varRegs.add(reg); + } else { + // Regular lexical variable (not captured, not state) + int reg = addVariable(varName, op); - // Check if it's a lexical variable (should not be localized) - if (hasVariable(varName)) { - throwCompilerException("Can't localize lexical variable " + varName); - return; + // Initialize the variable + switch (sigil) { + case "$" -> { + emit(Opcodes.LOAD_UNDEF); + emitReg(reg); + } + case "@" -> { + emit(Opcodes.NEW_ARRAY); + emitReg(reg); + } + case "%" -> { + emit(Opcodes.NEW_HASH); + emitReg(reg); + } + default -> throwCompilerException("Unsupported variable type in list declaration: " + sigil); } - // Get the global variable - String packageName = getCurrentPackage(); - String globalVarName = packageName + "::" + ((IdentifierNode) sigilOp.operand).name; - int nameIdx = addToStringPool(globalVarName); - - int localReg = allocateRegister(); - emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); - emitReg(localReg); - emit(nameIdx); - - // Extract element from RHS list - int elemReg = allocateRegister(); - emit(Opcodes.ARRAY_GET); - emitReg(elemReg); - emitReg(valueReg); - emitInt(i); - - // Assign to the localized variable - emit(Opcodes.SET_SCALAR); - emitReg(localReg); - emitReg(elemReg); - - if (i == 0) { - // Return the first localized variable - lastResultReg = localReg; - } + 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; } - } - } - - // Regular assignment: $x = value - // OPTIMIZATION: Detect $x = $x + $y and emit ADD_ASSIGN instead of ADD_SCALAR + MOVE - if (node.left instanceof OperatorNode && node.right instanceof BinaryOperatorNode) { - OperatorNode leftOp = (OperatorNode) node.left; - BinaryOperatorNode rightBin = (BinaryOperatorNode) node.right; - - if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode && - rightBin.operator.equals("+") && - rightBin.left instanceof OperatorNode) { - - String leftVarName = "$" + ((IdentifierNode) leftOp.operand).name; - OperatorNode rightLeftOp = (OperatorNode) rightBin.left; - - if (rightLeftOp.operator.equals("$") && rightLeftOp.operand instanceof IdentifierNode) { - String rightLeftVarName = "$" + ((IdentifierNode) rightLeftOp.operand).name; - // Pattern match: $x = $x + $y (emit ADD_ASSIGN) - // Skip optimization for captured variables (need SET_SCALAR) - boolean isCaptured = capturedVarIndices != null && - capturedVarIndices.containsKey(leftVarName); - - if (leftVarName.equals(rightLeftVarName) && hasVariable(leftVarName) && !isCaptured) { - int targetReg = getVariableRegister(leftVarName); - - // Compile RHS operand ($y) - rightBin.right.accept(this); - int rhsReg = lastResultReg; + // Return a list of the declared variables (or their references if isDeclaredReference) + int resultReg = allocateRegister(); - // Emit ADD_ASSIGN instead of ADD_SCALAR + MOVE - emit(Opcodes.ADD_ASSIGN); - emitReg(targetReg); - emitReg(rhsReg); + 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); + } - lastResultReg = targetReg; - return; + // 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); } } - } - } - - // Handle ${block} = value and $$var = value (symbolic references) - // We need to evaluate the LHS FIRST to get the variable name, - // then evaluate the RHS, to ensure the RHS doesn't clobber the LHS registers - if (node.left instanceof OperatorNode leftOp && leftOp.operator.equals("$")) { - if (leftOp.operand instanceof BlockNode) { - // ${block} = value - BlockNode block = (BlockNode) leftOp.operand; - block.accept(this); - int nameReg = lastResultReg; - - // Now compile the RHS - node.right.accept(this); - int valueReg = lastResultReg; - // Use STORE_SYMBOLIC_SCALAR to store via symbolic reference - emit(Opcodes.STORE_SYMBOLIC_SCALAR); - emitReg(nameReg); - emitReg(valueReg); - - lastResultReg = valueReg; - return; - } else if (leftOp.operand instanceof OperatorNode) { - // $$var = value (scalar dereference assignment) - // Evaluate the inner expression to get the variable name - leftOp.operand.accept(this); - int nameReg = lastResultReg; - - // Now compile the RHS - node.right.accept(this); - int valueReg = lastResultReg; - - // Use STORE_SYMBOLIC_SCALAR to store via symbolic reference - emit(Opcodes.STORE_SYMBOLIC_SCALAR); - emitReg(nameReg); - emitReg(valueReg); - - lastResultReg = valueReg; + lastResultReg = resultReg; return; } - } + throwCompilerException("Unsupported my operand: " + node.operand.getClass().getSimpleName()); + } else if (op.equals("our")) { + // our $x / our @x / our %x - package variable declaration + // The operand will be OperatorNode("$"/"@"/"%", IdentifierNode("x")) + if (node.operand instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) node.operand; + String sigil = sigilOp.operator; - // Regular assignment: $x = value (no optimization) - // Compile RHS first - node.right.accept(this); - int valueReg = lastResultReg; + if (sigilOp.operand instanceof IdentifierNode) { + String varName = sigil + ((IdentifierNode) sigilOp.operand).name; - // Assign to LHS - if (node.left instanceof OperatorNode) { - OperatorNode leftOp = (OperatorNode) node.left; - if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode) { - String varName = "$" + ((IdentifierNode) leftOp.operand).name; + // Check if this is a declared reference (our \$x) + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); - if (hasVariable(varName)) { - // Lexical variable - check if it's captured - int targetReg = getVariableRegister(varName); - - if (capturedVarIndices != null && capturedVarIndices.containsKey(varName)) { - // Captured variable - use SET_SCALAR to preserve aliasing - emit(Opcodes.SET_SCALAR); - emitReg(targetReg); - emitReg(valueReg); - } else { - // Regular lexical - use MOVE - emit(Opcodes.MOVE); - emitReg(targetReg); - emitReg(valueReg); - } + // Check if already declared in current scope + if (hasVariable(varName)) { + // Already declared, just return the existing register + int reg = getVariableRegister(varName); - lastResultReg = targetReg; - } else { - // Global variable - // Check strict vars before assignment - if (shouldBlockGlobalUnderStrictVars(varName)) { - throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); - } - - // Strip sigil and normalize name (e.g., "$x" → "main::x") - String bareVarName = varName.substring(1); // Remove sigil - String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage()); - int nameIdx = addToStringPool(normalizedName); - emit(Opcodes.STORE_GLOBAL_SCALAR); - emit(nameIdx); - emitReg(valueReg); - lastResultReg = valueReg; - } - } else if (leftOp.operator.equals("@") && leftOp.operand instanceof IdentifierNode) { - // Array assignment: @array = ... - String varName = "@" + ((IdentifierNode) leftOp.operand).name; - - int arrayReg; - if (hasVariable(varName)) { - // Lexical array - arrayReg = getVariableRegister(varName); - } else { - // Global array - load it - arrayReg = allocateRegister(); - String globalArrayName = NameNormalizer.normalizeVariableName(((IdentifierNode) leftOp.operand).name, getCurrentPackage()); - int nameIdx = addToStringPool(globalArrayName); - emit(Opcodes.LOAD_GLOBAL_ARRAY); - emitReg(arrayReg); - emit(nameIdx); - } - - // Populate array from list using setFromList - emit(Opcodes.ARRAY_SET_FROM_LIST); - emitReg(arrayReg); - emitReg(valueReg); - - lastResultReg = arrayReg; - } else if (leftOp.operator.equals("%") && leftOp.operand instanceof IdentifierNode) { - // Hash assignment: %hash = ... - String varName = "%" + ((IdentifierNode) leftOp.operand).name; - - int hashReg; - if (hasVariable(varName)) { - // Lexical hash - hashReg = getVariableRegister(varName); - } else { - // Global hash - load it - hashReg = allocateRegister(); - String globalHashName = NameNormalizer.normalizeVariableName(((IdentifierNode) leftOp.operand).name, getCurrentPackage()); - int nameIdx = addToStringPool(globalHashName); - emit(Opcodes.LOAD_GLOBAL_HASH); - emitReg(hashReg); - emit(nameIdx); - } - - // Populate hash from list using setFromList - emit(Opcodes.HASH_SET_FROM_LIST); - emitReg(hashReg); - emitReg(valueReg); - - lastResultReg = hashReg; - } else if (leftOp.operator.equals("our")) { - // Assignment to our variable: our $x = value or our @x = value or our %x = value - // Compile the our declaration first (which loads the global into a register) - leftOp.accept(this); - int targetReg = lastResultReg; - - // Now assign the RHS value to the target register - // The target register contains either a scalar, array, or hash - // We need to determine which and use the appropriate assignment - - // Extract the sigil from our operand - if (leftOp.operand instanceof OperatorNode) { - OperatorNode sigilOp = (OperatorNode) leftOp.operand; - String sigil = sigilOp.operator; - - if (sigil.equals("$")) { - // Scalar: use SET_SCALAR to modify value without breaking alias - emit(Opcodes.SET_SCALAR); - emitReg(targetReg); - emitReg(valueReg); - } else if (sigil.equals("@")) { - // Array: use ARRAY_SET_FROM_LIST - emit(Opcodes.ARRAY_SET_FROM_LIST); - emitReg(targetReg); - emitReg(valueReg); - } else if (sigil.equals("%")) { - // Hash: use HASH_SET_FROM_LIST - emit(Opcodes.HASH_SET_FROM_LIST); - emitReg(targetReg); - emitReg(valueReg); - } - } else if (leftOp.operand instanceof ListNode) { - // our ($a, $b) = ... - list declaration with assignment - // The our statement already declared the variables and returned a list - // We need to assign the RHS values to each variable - ListNode listNode = (ListNode) leftOp.operand; - - // Convert RHS to list - int rhsListReg = allocateRegister(); - emit(Opcodes.SCALAR_TO_LIST); - emitReg(rhsListReg); - emitReg(valueReg); - - // Assign each element - for (int i = 0; i < listNode.elements.size(); i++) { - Node element = listNode.elements.get(i); - if (element instanceof OperatorNode) { - OperatorNode sigilOp = (OperatorNode) element; - String sigil = sigilOp.operator; - - if (sigilOp.operand instanceof IdentifierNode) { - String varName = sigil + ((IdentifierNode) sigilOp.operand).name; - int varReg = getVariableRegister(varName); - - // Get i-th element from RHS - int indexReg = allocateRegister(); - emit(Opcodes.LOAD_INT); - emitReg(indexReg); - emitInt(i); - - int elemReg = allocateRegister(); - emit(Opcodes.ARRAY_GET); - emitReg(elemReg); - emitReg(rhsListReg); - emitReg(indexReg); - - // Assign to variable - if (sigil.equals("$")) { - emit(Opcodes.MOVE); - emitReg(varReg); - emitReg(elemReg); - } else if (sigil.equals("@")) { - emit(Opcodes.ARRAY_SET_FROM_LIST); - emitReg(varReg); - emitReg(elemReg); - } else if (sigil.equals("%")) { - emit(Opcodes.HASH_SET_FROM_LIST); - emitReg(varReg); - emitReg(elemReg); - } - } - } - } - lastResultReg = valueReg; - currentCallContext = savedContext; - return; - } - - lastResultReg = targetReg; - } else if (leftOp.operator.equals("*") && leftOp.operand instanceof IdentifierNode) { - // Typeglob assignment: *foo = value - String varName = ((IdentifierNode) leftOp.operand).name; - String globalName = NameNormalizer.normalizeVariableName(varName, getCurrentPackage()); - int nameIdx = addToStringPool(globalName); - - // Load the glob - int globReg = allocateRegister(); - emit(Opcodes.LOAD_GLOB); - emitReg(globReg); - emit(nameIdx); - - // Store value to glob - emit(Opcodes.STORE_GLOB); - emitReg(globReg); - emitReg(valueReg); - - lastResultReg = globReg; - } else if (leftOp.operator.equals("pos")) { - // pos($var) = value - lvalue assignment to regex position - // pos() returns a PosLvalueScalar that can be assigned to - node.left.accept(this); - int lvalueReg = lastResultReg; - - // Use SET_SCALAR to assign through the lvalue - emit(Opcodes.SET_SCALAR); - emitReg(lvalueReg); - emitReg(valueReg); - - lastResultReg = valueReg; - } else { - throwCompilerException("Assignment to unsupported operator: " + leftOp.operator); - } - } else if (node.left instanceof IdentifierNode) { - String varName = ((IdentifierNode) node.left).name; - - if (hasVariable(varName)) { - // Lexical variable - copy to its register - int targetReg = getVariableRegister(varName); - emit(Opcodes.MOVE); - emitReg(targetReg); - emitReg(valueReg); - lastResultReg = targetReg; - } else { - // Global variable (varName has no sigil here) - // Check strict vars - add sigil for checking - String varNameWithSigil = "$" + varName; - if (shouldBlockGlobalUnderStrictVars(varNameWithSigil)) { - throwCompilerException("Global symbol \"" + varNameWithSigil + "\" requires explicit package name"); - } - - String normalizedName = NameNormalizer.normalizeVariableName(varName, getCurrentPackage()); - int nameIdx = addToStringPool(normalizedName); - emit(Opcodes.STORE_GLOBAL_SCALAR); - emit(nameIdx); - emitReg(valueReg); - lastResultReg = valueReg; - } - } else if (node.left instanceof BinaryOperatorNode) { - BinaryOperatorNode leftBin = (BinaryOperatorNode) node.left; - - // Handle array slice assignment: @array[1, 3, 5] = (20, 30, 40) - if (leftBin.operator.equals("[") && leftBin.left instanceof OperatorNode) { - OperatorNode arrayOp = (OperatorNode) leftBin.left; - - // Must be @array (not $array) - if (arrayOp.operator.equals("@") && arrayOp.operand instanceof IdentifierNode) { - String varName = "@" + ((IdentifierNode) arrayOp.operand).name; - - // Get the array register - int arrayReg; - if (hasVariable(varName)) { - // Lexical array - arrayReg = getVariableRegister(varName); - } else { - // Global array - load it - arrayReg = allocateRegister(); - String globalArrayName = NameNormalizer.normalizeVariableName( - ((IdentifierNode) arrayOp.operand).name, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalArrayName); - emit(Opcodes.LOAD_GLOBAL_ARRAY); - emitReg(arrayReg); - emit(nameIdx); - } - - // Compile indices (right side of []) - // ArrayLiteralNode contains the indices - if (!(leftBin.right instanceof ArrayLiteralNode)) { - throwCompilerException("Array slice assignment requires index list"); - } - - ArrayLiteralNode indicesNode = (ArrayLiteralNode) leftBin.right; - List indexRegs = new ArrayList<>(); - for (Node indexNode : indicesNode.elements) { - indexNode.accept(this); - indexRegs.add(lastResultReg); - } - - // Create indices list - int indicesReg = allocateRegister(); - emit(Opcodes.CREATE_LIST); - emitReg(indicesReg); - emit(indexRegs.size()); - for (int indexReg : indexRegs) { - emitReg(indexReg); - } - - // Compile values (RHS of assignment) - node.right.accept(this); - int valuesReg = lastResultReg; - - // Emit direct opcode ARRAY_SLICE_SET - emit(Opcodes.ARRAY_SLICE_SET); - emitReg(arrayReg); - emitReg(indicesReg); - emitReg(valuesReg); - - lastResultReg = arrayReg; - currentCallContext = savedContext; - return; - } - } - - // Handle single element array assignment - // For: $array[index] = value or $matrix[3][0] = value - if (leftBin.operator.equals("[")) { - int arrayReg; - - // Check if left side is a variable or multidimensional access - if (leftBin.left instanceof OperatorNode) { - OperatorNode arrayOp = (OperatorNode) leftBin.left; - - // Single element assignment: $array[index] = value - if (arrayOp.operator.equals("$") && arrayOp.operand instanceof IdentifierNode) { - String varName = ((IdentifierNode) arrayOp.operand).name; - String arrayVarName = "@" + varName; - - // Get the array register - if (hasVariable(arrayVarName)) { - // Lexical array - arrayReg = getVariableRegister(arrayVarName); + // 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 { - // Global array - load it - arrayReg = allocateRegister(); - String globalArrayName = NameNormalizer.normalizeVariableName( - varName, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalArrayName); - emit(Opcodes.LOAD_GLOBAL_ARRAY); - emitReg(arrayReg); - emit(nameIdx); + lastResultReg = reg; } - } else { - throwCompilerException("Assignment requires scalar dereference: $var[index]"); return; } - } else if (leftBin.left instanceof BinaryOperatorNode) { - // Multidimensional case: $matrix[3][0] = value - // Compile left side (which returns a scalar containing an array reference) - leftBin.left.accept(this); - int scalarReg = lastResultReg; - - // Dereference the array reference to get the actual array - arrayReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(arrayReg); - emitReg(scalarReg); - } else { - throwCompilerException("Array assignment requires variable or expression on left side"); - return; - } - - // Compile index expression - if (!(leftBin.right instanceof ArrayLiteralNode)) { - throwCompilerException("Array assignment requires ArrayLiteralNode on right side"); - } - ArrayLiteralNode indexNode = (ArrayLiteralNode) leftBin.right; - if (indexNode.elements.isEmpty()) { - throwCompilerException("Array assignment requires index expression"); - } - indexNode.elements.get(0).accept(this); - int indexReg = lastResultReg; - - // Compile RHS value - node.right.accept(this); - int assignValueReg = lastResultReg; - - // Emit ARRAY_SET - emit(Opcodes.ARRAY_SET); - emitReg(arrayReg); - emitReg(indexReg); - emitReg(assignValueReg); + // Allocate register and add to symbol table + int reg = addVariable(varName, "our"); - lastResultReg = assignValueReg; - currentCallContext = savedContext; - return; - } else if (leftBin.operator.equals("{")) { - // Hash element/slice assignment - // $hash{key} = value (scalar element) - // @hash{keys} = values (slice) - - // 1. Get hash variable (leftBin.left) - int hashReg; - if (leftBin.left instanceof OperatorNode) { - OperatorNode hashOp = (OperatorNode) leftBin.left; - - // Check for hash slice assignment: @hash{keys} = values - if (hashOp.operator.equals("@")) { - // Hash slice assignment - if (!(hashOp.operand instanceof IdentifierNode)) { - throwCompilerException("Hash slice assignment requires identifier"); - return; - } - String varName = ((IdentifierNode) hashOp.operand).name; - String hashVarName = "%" + varName; + // Load from global variable using normalized name + String globalVarName = NameNormalizer.normalizeVariableName( + ((IdentifierNode) sigilOp.operand).name, + getCurrentPackage() + ); + int nameIdx = addToStringPool(globalVarName); - // Get the hash - check lexical first, then global - if (hasVariable(hashVarName)) { - // Lexical hash - hashReg = getVariableRegister(hashVarName); - } else { - // Global hash - load it - hashReg = allocateRegister(); - String globalHashName = NameNormalizer.normalizeVariableName( - varName, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalHashName); - emit(Opcodes.LOAD_GLOBAL_HASH); - emitReg(hashReg); + switch (sigil) { + case "$" -> { + emit(Opcodes.LOAD_GLOBAL_SCALAR); + emitReg(reg); emit(nameIdx); } - - // Get the keys from HashLiteralNode - if (!(leftBin.right instanceof HashLiteralNode)) { - throwCompilerException("Hash slice assignment requires HashLiteralNode"); - return; - } - HashLiteralNode keysNode = (HashLiteralNode) leftBin.right; - if (keysNode.elements.isEmpty()) { - throwCompilerException("Hash slice assignment requires at least one key"); - return; - } - - // Compile all keys into a list - List keyRegs = new ArrayList<>(); - for (Node keyElement : keysNode.elements) { - if (keyElement instanceof IdentifierNode) { - // Bareword key - autoquote - String keyString = ((IdentifierNode) keyElement).name; - int keyReg = allocateRegister(); - int keyIdx = addToStringPool(keyString); - emit(Opcodes.LOAD_STRING); - emitReg(keyReg); - emit(keyIdx); - keyRegs.add(keyReg); - } else { - // Expression key - keyElement.accept(this); - keyRegs.add(lastResultReg); - } - } - - // Create a RuntimeList from key registers - int keysListReg = allocateRegister(); - emit(Opcodes.CREATE_LIST); - emitReg(keysListReg); - emit(keyRegs.size()); - for (int keyReg : keyRegs) { - emitReg(keyReg); - } - - // Compile RHS values - node.right.accept(this); - int valuesReg = lastResultReg; - - // Emit direct opcode HASH_SLICE_SET - emit(Opcodes.HASH_SLICE_SET); - emitReg(hashReg); - emitReg(keysListReg); - emitReg(valuesReg); - - lastResultReg = valuesReg; - currentCallContext = savedContext; - return; - } else if (hashOp.operator.equals("$")) { - // $hash{key} - dereference to get hash - if (!(hashOp.operand instanceof IdentifierNode)) { - throwCompilerException("Hash assignment requires identifier"); - return; + case "@" -> { + emit(Opcodes.LOAD_GLOBAL_ARRAY); + emitReg(reg); + emit(nameIdx); } - String varName = ((IdentifierNode) hashOp.operand).name; - String hashVarName = "%" + varName; - - if (hasVariable(hashVarName)) { - // Lexical hash - hashReg = getVariableRegister(hashVarName); - } else { - // Global hash - load it - hashReg = allocateRegister(); - String globalHashName = NameNormalizer.normalizeVariableName( - varName, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalHashName); + case "%" -> { emit(Opcodes.LOAD_GLOBAL_HASH); - emitReg(hashReg); + emitReg(reg); emit(nameIdx); } + default -> throwCompilerException("Unsupported variable type: " + sigil); + } + + // 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 { - throwCompilerException("Hash assignment requires scalar dereference: $var{key}"); - return; + lastResultReg = reg; } - } else if (leftBin.left instanceof BinaryOperatorNode) { - // Nested: $hash{outer}{inner} = value - // Compile left side (returns scalar containing hash reference or autovivifies) - leftBin.left.accept(this); - int scalarReg = lastResultReg; - - // Dereference to get the hash (with autovivification) - hashReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); - emitReg(hashReg); - emitReg(scalarReg); - } else { - throwCompilerException("Hash assignment requires variable or expression on left side"); return; } - // 2. Compile key expression - if (!(leftBin.right instanceof HashLiteralNode)) { - throwCompilerException("Hash assignment requires HashLiteralNode on right side"); - return; - } - HashLiteralNode keyNode = (HashLiteralNode) leftBin.right; - if (keyNode.elements.isEmpty()) { - throwCompilerException("Hash key required for assignment"); - 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")); - // Compile the key - // Special case: IdentifierNode in hash access is autoquoted (bareword key) - int keyReg; - Node keyElement = keyNode.elements.get(0); - if (keyElement instanceof IdentifierNode) { - // Bareword key: $hash{key} -> key is autoquoted to "key" - String keyString = ((IdentifierNode) keyElement).name; - keyReg = allocateRegister(); - int keyIdx = addToStringPool(keyString); - emit(Opcodes.LOAD_STRING); - emitReg(keyReg); - emit(keyIdx); - } else { - // Expression key: $hash{$var} or $hash{func()} - keyElement.accept(this); - keyReg = lastResultReg; + 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<>(); - // 3. Compile RHS value - node.right.accept(this); - int hashValueReg = lastResultReg; - - // 4. Emit HASH_SET - emit(Opcodes.HASH_SET); - emitReg(hashReg); - emitReg(keyReg); - emitReg(hashValueReg); - - lastResultReg = hashValueReg; - currentCallContext = savedContext; - return; - } - - // Handle lvalue subroutine: f() = value - // When a function is called in lvalue context, it returns a RuntimeBaseProxy - // that wraps a mutable reference. We can assign to it using SET_SCALAR. - if (leftBin.operator.equals("(")) { - // Call the function (which returns a RuntimeBaseProxy in lvalue context) - node.left.accept(this); - int lvalueReg = lastResultReg; - - // Compile RHS - node.right.accept(this); - int rhsReg = lastResultReg; - - // Assign to the lvalue using SET_SCALAR - emit(Opcodes.SET_SCALAR); - emitReg(lvalueReg); - emitReg(rhsReg); - - lastResultReg = rhsReg; - currentCallContext = savedContext; - return; - } + // Check if this is a declared reference (our \($x, $y)) + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); - throwCompilerException("Assignment to non-identifier not yet supported: " + node.left.getClass().getSimpleName()); - } else if (node.left instanceof ListNode) { - // List assignment: ($a, $b) = ... or () = ... - // In scalar context, returns the number of elements on RHS - // In list context, returns the RHS list - ListNode listNode = (ListNode) node.left; + // Track if we found any backslash operators inside the list + boolean foundBackslashInList = false; - // Compile RHS in LIST context to get all elements - int savedRhsContext = currentCallContext; - currentCallContext = RuntimeContextType.LIST; - node.right.accept(this); - int rhsReg = lastResultReg; - currentCallContext = savedRhsContext; - - // Convert RHS to RuntimeList if needed - int rhsListReg = allocateRegister(); - emit(Opcodes.SCALAR_TO_LIST); - emitReg(rhsListReg); - emitReg(rhsReg); - - // If the list is not empty, perform the assignment - if (!listNode.elements.isEmpty()) { - // Assign each RHS element to corresponding LHS variable - for (int i = 0; i < listNode.elements.size(); i++) { - Node lhsElement = listNode.elements.get(i); - - // Get the i-th element from RHS list - int indexReg = allocateRegister(); - emit(Opcodes.LOAD_INT); - emitReg(indexReg); - emitInt(i); - - int elementReg = allocateRegister(); - emit(Opcodes.ARRAY_GET); - emitReg(elementReg); - emitReg(rhsListReg); - emitReg(indexReg); - - // Assign to LHS element - if (lhsElement instanceof OperatorNode) { - OperatorNode lhsOp = (OperatorNode) lhsElement; - if (lhsOp.operator.equals("$") && lhsOp.operand instanceof IdentifierNode) { - String varName = "$" + ((IdentifierNode) lhsOp.operand).name; + for (Node element : listNode.elements) { + if (element instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) element; + String sigil = sigilOp.operator; - if (hasVariable(varName)) { - int targetReg = getVariableRegister(varName); - if (capturedVarIndices != null && capturedVarIndices.containsKey(varName)) { - emit(Opcodes.SET_SCALAR); - emitReg(targetReg); - emitReg(elementReg); - } else { - emit(Opcodes.MOVE); - emitReg(targetReg); - emitReg(elementReg); - } - } else { - // Normalize global variable name (remove sigil, add package) - // Check strict vars before list assignment - if (shouldBlockGlobalUnderStrictVars(varName)) { - throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); + // 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); - String bareVarName = varName.substring(1); // Remove "$" - String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage()); - int nameIdx = addToStringPool(normalizedName); - emit(Opcodes.STORE_GLOBAL_SCALAR); - emit(nameIdx); - emitReg(elementReg); - } - } else if (lhsOp.operator.equals("@") && lhsOp.operand instanceof IdentifierNode) { - // Array slurp: ($a, @rest) = ... - // Collect remaining elements into a RuntimeList - String varName = "@" + ((IdentifierNode) lhsOp.operand).name; - - int arrayReg; - if (hasVariable(varName)) { - arrayReg = getVariableRegister(varName); - } else { - arrayReg = allocateRegister(); - String globalArrayName = NameNormalizer.normalizeVariableName( - ((IdentifierNode) lhsOp.operand).name, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalArrayName); - emit(Opcodes.LOAD_GLOBAL_ARRAY); - emitReg(arrayReg); - emit(nameIdx); - } - - // Create a list of remaining indices - // Use SLOWOP_LIST_SLICE_FROM to get list[i..] - int remainingListReg = allocateRegister(); - emit(Opcodes.LIST_SLICE_FROM); - emitReg(remainingListReg); - emitReg(rhsListReg); - emitInt(i); // Start index - - // Populate array from remaining elements - emit(Opcodes.ARRAY_SET_FROM_LIST); - emitReg(arrayReg); - emitReg(remainingListReg); - - // Array slurp consumes all remaining elements - break; - } else if (lhsOp.operator.equals("%") && lhsOp.operand instanceof IdentifierNode) { - // Hash slurp: ($a, %rest) = ... - String varName = "%" + ((IdentifierNode) lhsOp.operand).name; - - int hashReg; - if (hasVariable(varName)) { - hashReg = getVariableRegister(varName); - } else { - hashReg = allocateRegister(); - String globalHashName = NameNormalizer.normalizeVariableName( - ((IdentifierNode) lhsOp.operand).name, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalHashName); - emit(Opcodes.LOAD_GLOBAL_HASH); - emitReg(hashReg); - emit(nameIdx); + // 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; } - // Get remaining elements from list - int remainingListReg = allocateRegister(); - emit(Opcodes.LIST_SLICE_FROM); - emitReg(remainingListReg); - emitReg(rhsListReg); - emitInt(i); // Start index - - // Populate hash from remaining elements - emit(Opcodes.HASH_SET_FROM_LIST); - emitReg(hashReg); - emitReg(remainingListReg); + // Single backslash - mark that we need to create references later + foundBackslashInList = true; - // Hash slurp consumes all remaining elements - break; - } - } - } - } + // 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; - // Return value depends on savedContext (the context this assignment was called in) - if (savedContext == RuntimeContextType.SCALAR) { - // In scalar context, list assignment returns the count of RHS elements - int countReg = allocateRegister(); - emit(Opcodes.ARRAY_SIZE); - emitReg(countReg); - emitReg(rhsListReg); - lastResultReg = countReg; - } else { - // In list context, return the RHS value - lastResultReg = rhsListReg; - } + // Declare and load the package variable + int reg = addVariable(varName, "our"); + String globalVarName = NameNormalizer.normalizeVariableName( + idNode.name, + getCurrentPackage() + ); + int nameIdx = addToStringPool(globalVarName); - currentCallContext = savedContext; - return; - } else { - throwCompilerException("Assignment to non-identifier not yet supported: " + node.left.getClass().getSimpleName()); - } + // 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); + } + } - } finally { - // Always restore the calling context - currentCallContext = savedContext; - } - } + 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; - /** - * Helper method to compile binary operator switch statement. - * Extracted from visit(BinaryOperatorNode) to reduce method size. - * Handles the giant switch statement for all binary operators. - * - * @param operator The binary operator string - * @param rs1 Left operand register - * @param rs2 Right operand register - * @param tokenIndex Token index for error reporting - * @return Result register containing the operation result - */ - private int compileBinaryOperatorSwitch(String operator, int rs1, int rs2, int tokenIndex) { - // Allocate result register - int rd = allocateRegister(); + // Declare and load the package variable + int reg = addVariable(varName, "our"); + String globalVarName = NameNormalizer.normalizeVariableName( + idNode.name, + getCurrentPackage() + ); + int nameIdx = addToStringPool(globalVarName); - // Emit opcode based on operator - switch (operator) { - case "+" -> { - emit(Opcodes.ADD_SCALAR); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "-" -> { - emit(Opcodes.SUB_SCALAR); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "*" -> { - emit(Opcodes.MUL_SCALAR); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "%" -> { - emit(Opcodes.MOD_SCALAR); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "/" -> { - emit(Opcodes.DIV_SCALAR); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "**" -> { - emit(Opcodes.POW_SCALAR); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "." -> { - emit(Opcodes.CONCAT); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "x" -> { - emit(Opcodes.REPEAT); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "<=>" -> { - emit(Opcodes.COMPARE_NUM); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "cmp" -> { - emit(Opcodes.COMPARE_STR); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "bless" -> { - // bless $ref, "Package" or bless $ref (defaults to current package) - // rs1 = reference to bless - // rs2 = package name (or undef for current package) - emit(Opcodes.BLESS); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "isa" -> { - // $obj isa "Package" - check if object is instance of package - // rs1 = object/reference - // rs2 = package name - emit(Opcodes.ISA); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "==" -> { - emit(Opcodes.EQ_NUM); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "<" -> { - emit(Opcodes.LT_NUM); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case ">" -> { - emit(Opcodes.GT_NUM); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "<=" -> { - emit(Opcodes.LE_NUM); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case ">=" -> { - emit(Opcodes.GE_NUM); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "!=" -> { - emit(Opcodes.NE_NUM); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "eq" -> { - // String equality: $a eq $b - emit(Opcodes.EQ_STR); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "ne" -> { - // String inequality: $a ne $b - emit(Opcodes.NE_STR); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "lt", "gt", "le", "ge" -> { - // String comparisons using COMPARE_STR (like cmp) - // cmp returns: -1 if $a lt $b, 0 if equal, 1 if $a gt $b - int cmpReg = allocateRegister(); - emit(Opcodes.COMPARE_STR); - emitReg(cmpReg); - emitReg(rs1); - emitReg(rs2); - - // Compare result to 0 - int zeroReg = allocateRegister(); - emit(Opcodes.LOAD_INT); - emitReg(zeroReg); - emitInt(0); - - // Emit appropriate comparison - switch (operator) { - case "lt" -> emit(Opcodes.LT_NUM); // cmp < 0 - case "gt" -> emit(Opcodes.GT_NUM); // cmp > 0 - case "le" -> { - // le: cmp <= 0, which is !(cmp > 0) - int gtReg = allocateRegister(); - emit(Opcodes.GT_NUM); - emitReg(gtReg); - emitReg(cmpReg); - emitReg(zeroReg); - emit(Opcodes.NOT); - emitReg(rd); - emitReg(gtReg); - return rd; + // 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; + + int reg; + // Check if already declared in current scope + if (hasVariable(varName)) { + // Already declared, just use existing register + reg = getVariableRegister(varName); + } else { + // Allocate register and add to symbol table + reg = addVariable(varName, "our"); + + // Load from global variable using normalized name + String globalVarName = NameNormalizer.normalizeVariableName( + ((IdentifierNode) sigilOp.operand).name, + getCurrentPackage() + ); + int nameIdx = addToStringPool(globalVarName); + + switch (sigil) { + 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); + } + default -> throwCompilerException("Unsupported variable type in list declaration: " + sigil); + } + } + + varRegs.add(reg); + } else { + throwCompilerException("our list declaration requires identifier: " + sigilOp.operand.getClass().getSimpleName()); + } + } else { + throwCompilerException("our list declaration requires scalar/array/hash: " + element.getClass().getSimpleName()); } - case "ge" -> { - // ge: cmp >= 0, which is !(cmp < 0) - int ltReg = allocateRegister(); - emit(Opcodes.LT_NUM); - emitReg(ltReg); - emitReg(cmpReg); - emitReg(zeroReg); - emit(Opcodes.NOT); - emitReg(rd); - emitReg(ltReg); - return rd; + } + + // Return a list of the declared variables (or their references if isDeclaredReference) + 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); } } - emitReg(rd); - emitReg(cmpReg); - emitReg(zeroReg); - } - case "(", "()" -> { - // Apply operator: $coderef->(args) or &subname(args) or foo(args) - // left (rs1) = code reference (RuntimeScalar containing RuntimeCode or SubroutineNode) - // right (rs2) = arguments (should be RuntimeList from ListNode) - - // Note: rs2 should contain a RuntimeList (from visiting the ListNode) - // We need to convert it to RuntimeArray for the CALL_SUB opcode - - // For now, rs2 is a RuntimeList - we'll pass it directly and let - // BytecodeInterpreter convert it to RuntimeArray - - // Emit CALL_SUB: rd = coderef.apply(args, context) - emit(Opcodes.CALL_SUB); - emitReg(rd); // Result register - emitReg(rs1); // Code reference register - emitReg(rs2); // Arguments register (RuntimeList to be converted to RuntimeArray) - emit(currentCallContext); // Use current calling context - - // Note: CALL_SUB may return RuntimeControlFlowList - // The interpreter will handle control flow propagation + + lastResultReg = resultReg; + return; } - case ".." -> { - // Range operator: start..end - // Create a PerlRange object which can be iterated or converted to a list + 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")) + if (node.operand instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) node.operand; + String sigil = sigilOp.operator; - // Optimization: if both operands are constant numbers, create range at compile time - // (This optimization would need access to the original nodes, which we don't have here) - // So we always use runtime range creation + if (sigil.equals("$") && sigilOp.operand instanceof IdentifierNode) { + String varName = "$" + ((IdentifierNode) sigilOp.operand).name; - // Runtime range creation using RANGE opcode - // rs1 and rs2 already contain the start and end values - emit(Opcodes.RANGE); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "map" -> { - // Map operator: map { block } list - // rs1 = closure (SubroutineNode compiled to code reference) - // rs2 = list expression + // Check if it's a lexical variable (should not be localized) + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + return; + } - // Emit MAP opcode - emit(Opcodes.MAP); - emitReg(rd); - emitReg(rs2); // List register - emitReg(rs1); // Closure register - emit(RuntimeContextType.LIST); // Map always uses list context - } - case "grep" -> { - // Grep operator: grep { block } list - // rs1 = closure (SubroutineNode compiled to code reference) - // rs2 = list expression + // Check if this is a declared reference (local \$x) + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); - // Emit GREP opcode - emit(Opcodes.GREP); - emitReg(rd); - emitReg(rs2); // List register - emitReg(rs1); // Closure register - emit(RuntimeContextType.LIST); // Grep uses list context - } - case "sort" -> { - // Sort operator: sort { block } list - // rs1 = closure (SubroutineNode compiled to code reference) - // rs2 = list expression + // It's a global variable - emit SLOW_OP to call GlobalRuntimeScalar.makeLocal() + String packageName = getCurrentPackage(); + String globalVarName = packageName + "::" + ((IdentifierNode) sigilOp.operand).name; + int nameIdx = addToStringPool(globalVarName); - // Emit SORT opcode - emit(Opcodes.SORT); - emitReg(rd); - emitReg(rs2); // List register - emitReg(rs1); // Closure register - emitInt(addToStringPool(getCurrentPackage())); // Package name for sort - } - case "split" -> { - // Split operator: split pattern, string - // rs1 = pattern (string or regex) - // rs2 = list containing string to split (and optional limit) + int rd = allocateRegister(); + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); - // Emit direct opcode SPLIT - emit(Opcodes.SPLIT); - emitReg(rd); - emitReg(rs1); // Pattern register - emitReg(rs2); // Args register - emit(RuntimeContextType.LIST); // Split uses list context - } - case "[" -> { - // Array element access: $a[10] means get element 10 from array @a - // Array slice: @a[1,2,3] or @a[1..3] means get multiple elements - // Also handles multidimensional: $a[0][1] means $a[0]->[1] - // This case should NOT be reached because array access is handled specially before this switch - throwCompilerException("Array access [ should be handled before switch", tokenIndex); - } - case "{" -> { - // Hash element access: $h{key} means get element 'key' from hash %h - // Hash slice access: @h{keys} returns multiple values as array - // This case should NOT be reached because hash access is handled specially before this switch - throwCompilerException("Hash access { should be handled before switch", tokenIndex); - } - case "push" -> { - // This should NOT be reached because push is handled specially before this switch - throwCompilerException("push should be handled before switch", tokenIndex); - } - case "unshift" -> { - // This should NOT be reached because unshift is handled specially before this switch - throwCompilerException("unshift should be handled before switch", tokenIndex); - } - case "+=" -> { - // This should NOT be reached because += is handled specially before this switch - throwCompilerException("+= should be handled before switch", tokenIndex); - } - case "-=", "*=", "/=", "%=", ".=" -> { - // This should NOT be reached because compound assignments are handled specially before this switch - throwCompilerException(operator + " should be handled before switch", tokenIndex); - } - case "readline" -> { - // <$fh> - read line from filehandle - // rs1 = filehandle (or undef for ARGV) - // rs2 = unused (ListNode) - emit(Opcodes.READLINE); - emitReg(rd); - emitReg(rs1); - emit(currentCallContext); - } - case "=~" -> { - // $string =~ /pattern/ - regex match - // rs1 = string to match against - // rs2 = compiled regex pattern - emit(Opcodes.MATCH_REGEX); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - emit(currentCallContext); - } - case "!~" -> { - // $string !~ /pattern/ - negated regex match - // rs1 = string to match against - // rs2 = compiled regex pattern - emit(Opcodes.MATCH_REGEX_NOT); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - emit(currentCallContext); - } - case "&" -> { - // Numeric bitwise AND (default): rs1 & rs2 - emit(Opcodes.BITWISE_AND_BINARY); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "binary&" -> { - // Numeric bitwise AND (use integer): rs1 binary& rs2 - // Same as & but explicitly numeric - emit(Opcodes.BITWISE_AND_BINARY); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "|" -> { - // Numeric bitwise OR (default): rs1 | rs2 - emit(Opcodes.BITWISE_OR_BINARY); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "binary|" -> { - // Numeric bitwise OR (use integer): rs1 binary| rs2 - // Same as | but explicitly numeric - emit(Opcodes.BITWISE_OR_BINARY); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "^" -> { - // Numeric bitwise XOR (default): rs1 ^ rs2 - emit(Opcodes.BITWISE_XOR_BINARY); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "binary^" -> { - // Numeric bitwise XOR (use integer): rs1 binary^ rs2 - // Same as ^ but explicitly numeric - emit(Opcodes.BITWISE_XOR_BINARY); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "&." -> { - // String bitwise AND: rs1 &. rs2 - emit(Opcodes.STRING_BITWISE_AND); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "|." -> { - // String bitwise OR: rs1 |. rs2 - emit(Opcodes.STRING_BITWISE_OR); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "^." -> { - // String bitwise XOR: rs1 ^. rs2 - emit(Opcodes.STRING_BITWISE_XOR); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case "<<" -> { - // Left shift: rs1 << rs2 - emit(Opcodes.LEFT_SHIFT); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - case ">>" -> { - // Right shift: rs1 >> rs2 - emit(Opcodes.RIGHT_SHIFT); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - } - default -> throwCompilerException("Unsupported operator: " + operator, tokenIndex); - } + // 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; + } - return rd; - } + // Handle local \$x or local \\$x - backslash operator + if (sigil.equals("\\")) { + boolean isDeclaredReference = node.annotations != null && + Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); - @Override - public void visit(BinaryOperatorNode node) { - // Track token index for error reporting - currentTokenIndex = node.getIndex(); + if (isDeclaredReference) { + // Check if operand is a variable operator ($, @, %) + if (sigilOp.operand instanceof OperatorNode innerOp && + "$@%".contains(innerOp.operator) && + innerOp.operand instanceof IdentifierNode idNode) { - // Handle print/say early (special handling for filehandle) - if (node.operator.equals("print") || node.operator.equals("say")) { - // print/say FILEHANDLE LIST - // left = filehandle reference (\*STDERR) - // right = list to print + // For declared refs, always use scalar sigil + String varName = "$" + idNode.name; - // Compile the filehandle (left operand) - node.left.accept(this); - int filehandleReg = lastResultReg; + // Check if it's a lexical variable + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + return; + } - // Compile the content (right operand) - node.right.accept(this); - int contentReg = lastResultReg; + // Localize global variable + String packageName = getCurrentPackage(); + String globalVarName = packageName + "::" + idNode.name; + int nameIdx = addToStringPool(globalVarName); - // Emit PRINT or SAY with both registers - emit(node.operator.equals("say") ? Opcodes.SAY : Opcodes.PRINT); - emitReg(contentReg); - emitReg(filehandleReg); + int rd = allocateRegister(); + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); - // print/say return 1 on success - int rd = allocateRegister(); - emit(Opcodes.LOAD_INT); - emitReg(rd); - emitInt(1); + // Create first reference + int refReg1 = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg1); + emitReg(rd); - lastResultReg = rd; - return; - } + // 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")); - // Handle compound assignment operators (+=, -=, *=, /=, %=, .=, &=, |=, ^=, &.=, |.=, ^.=, binary&=, binary|=, binary^=, x=, **=, <<=, >>=, &&=, ||=) - if (node.operator.equals("+=") || node.operator.equals("-=") || - node.operator.equals("*=") || node.operator.equals("/=") || - node.operator.equals("%=") || node.operator.equals(".=") || - node.operator.equals("&=") || node.operator.equals("|=") || node.operator.equals("^=") || - node.operator.equals("&.=") || node.operator.equals("|.=") || node.operator.equals("^.=") || - node.operator.equals("x=") || node.operator.equals("**=") || - node.operator.equals("<<=") || node.operator.equals(">>=") || - node.operator.equals("&&=") || node.operator.equals("||=") || - node.operator.startsWith("binary")) { // Handle binary&=, binary|=, binary^= - handleCompoundAssignment(node); - return; - } + 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<>(); - // Handle assignment separately (doesn't follow standard left-right-op pattern) - if (node.operator.equals("=")) { - compileAssignmentOperator(node); - return; - } + // 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; - // Handle -> operator specially for hashref/arrayref dereference - if (node.operator.equals("->")) { - currentTokenIndex = node.getIndex(); // Track token for error reporting + for (Node element : listNode.elements) { + if (element instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) element; + String sigil = sigilOp.operator; - if (node.right instanceof HashLiteralNode) { - // Hashref dereference: $ref->{key} - // left: scalar containing hash reference - // right: HashLiteralNode containing key + // 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")); - // Compile the reference (left side) - node.left.accept(this); - int scalarRefReg = lastResultReg; + 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) { - // Dereference the scalar to get the actual hash - int hashReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); - emitReg(hashReg); - emitReg(scalarRefReg); + String varName = "$" + idNode.name; - // Get the key - HashLiteralNode keyNode = (HashLiteralNode) node.right; - if (keyNode.elements.isEmpty()) { - throwCompilerException("Hash dereference requires key"); - } + // Check if it's a lexical variable + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + } - // Compile the key - handle bareword autoquoting - int keyReg; - Node keyElement = keyNode.elements.get(0); - if (keyElement instanceof IdentifierNode) { - // Bareword key: $ref->{key} -> key is autoquoted - String keyString = ((IdentifierNode) keyElement).name; - keyReg = allocateRegister(); - int keyIdx = addToStringPool(keyString); - emit(Opcodes.LOAD_STRING); - emitReg(keyReg); - emit(keyIdx); - } else { - // Expression key: $ref->{$var} - keyElement.accept(this); - keyReg = lastResultReg; - } + // Localize global variable + String packageName = getCurrentPackage(); + String globalVarName = packageName + "::" + idNode.name; + int nameIdx = addToStringPool(globalVarName); - // Access hash element - int rd = allocateRegister(); - emit(Opcodes.HASH_GET); - emitReg(rd); - emitReg(hashReg); - emitReg(keyReg); + int rd = allocateRegister(); + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); - lastResultReg = rd; - return; - } else if (node.right instanceof ArrayLiteralNode) { - // Arrayref dereference: $ref->[index] - // left: scalar containing array reference - // right: ArrayLiteralNode containing index + // Create first reference + int refReg1 = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg1); + emitReg(rd); - // Compile the reference (left side) - node.left.accept(this); - int scalarRefReg = lastResultReg; + // Create second reference + int refReg2 = allocateRegister(); + emit(Opcodes.CREATE_REF); + emitReg(refReg2); + emitReg(refReg1); - // Dereference the scalar to get the actual array - int arrayReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(arrayReg); - emitReg(scalarRefReg); + varRegs.add(refReg2); + } + continue; + } - // Get the index - ArrayLiteralNode indexNode = (ArrayLiteralNode) node.right; - if (indexNode.elements.isEmpty()) { - throwCompilerException("Array dereference requires index"); - } + // Single backslash + foundBackslashInList = true; - // Compile the index expression - indexNode.elements.get(0).accept(this); - int indexReg = lastResultReg; + // 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; - // Access array element - int rd = allocateRegister(); - emit(Opcodes.ARRAY_GET); - emitReg(rd); - emitReg(arrayReg); - emitReg(indexReg); + // Check if it's a lexical variable + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + } - lastResultReg = rd; - return; - } - // Code reference call: $code->() or $code->(@args) - // right is ListNode with arguments - else if (node.right instanceof ListNode) { - // This is a code reference call: $coderef->(args) - // Compile the code reference in scalar context - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(this); - int coderefReg = lastResultReg; + // Localize global variable + String packageName = getCurrentPackage(); + String globalVarName = packageName + "::" + idNode.name; + int nameIdx = addToStringPool(globalVarName); - // Compile arguments in list context - currentCallContext = RuntimeContextType.LIST; - node.right.accept(this); - int argsReg = lastResultReg; - currentCallContext = savedContext; + int rd = allocateRegister(); + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); - // Allocate result register - int rd = allocateRegister(); + 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; - // Emit CALL_SUB opcode - emit(Opcodes.CALL_SUB); - emitReg(rd); - emitReg(coderefReg); - emitReg(argsReg); - emit(currentCallContext); + // Check if it's a lexical variable + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + } - lastResultReg = rd; - return; - } - // Method call: ->method() or ->$method() - // right is BinaryOperatorNode with operator "(" - else if (node.right instanceof BinaryOperatorNode) { - BinaryOperatorNode rightCall = (BinaryOperatorNode) node.right; - if (rightCall.operator.equals("(")) { - // object.call(method, arguments, context) - Node invocantNode = node.left; - Node methodNode = rightCall.left; - Node argsNode = rightCall.right; - - // Convert class name to string if needed: Class->method() - if (invocantNode instanceof IdentifierNode) { - String className = ((IdentifierNode) invocantNode).name; - invocantNode = new StringNode(className, ((IdentifierNode) invocantNode).getIndex()); - } + // 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); - // Convert method name to string if needed - if (methodNode instanceof OperatorNode) { - OperatorNode methodOp = (OperatorNode) methodNode; - // &method is introduced by parser if method is predeclared - if (methodOp.operator.equals("&")) { - methodNode = methodOp.operand; + varRegs.add(rd); + } + continue; } - } - if (methodNode instanceof IdentifierNode) { - String methodName = ((IdentifierNode) methodNode).name; - methodNode = new StringNode(methodName, ((IdentifierNode) methodNode).getIndex()); - } - // Compile invocant in scalar context - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - invocantNode.accept(this); - int invocantReg = lastResultReg; - - // Compile method name in scalar context - methodNode.accept(this); - int methodReg = lastResultReg; - - // Get currentSub (__SUB__ for SUPER:: resolution) - int currentSubReg = allocateRegister(); - emit(Opcodes.LOAD_GLOBAL_CODE); - emitReg(currentSubReg); - int subIdx = addToStringPool("__SUB__"); - emit(subIdx); - - // Compile arguments in list context - currentCallContext = RuntimeContextType.LIST; - argsNode.accept(this); - int argsReg = lastResultReg; - currentCallContext = savedContext; + // Regular scalar variable in list + if (sigil.equals("$") && sigilOp.operand instanceof IdentifierNode idNode) { + String varName = "$" + idNode.name; - // Allocate result register - int rd = allocateRegister(); + // Check if it's a lexical variable + if (hasVariable(varName)) { + throwCompilerException("Can't localize lexical variable " + varName); + } - // Emit CALL_METHOD - emit(Opcodes.CALL_METHOD); - emitReg(rd); - emitReg(invocantReg); - emitReg(methodReg); - emitReg(currentSubReg); - emitReg(argsReg); - emit(currentCallContext); + // Localize global variable + String packageName = getCurrentPackage(); + String globalVarName = packageName + "::" + idNode.name; + int nameIdx = addToStringPool(globalVarName); - lastResultReg = rd; - return; - } - } - // Otherwise, fall through to normal -> handling (method call) - } + int rd = allocateRegister(); + emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); - // Handle [] operator for array access - // Must be before automatic operand compilation to handle array slices - if (node.operator.equals("[")) { - currentTokenIndex = node.getIndex(); - - // Check if this is an array slice: @array[indices] - if (node.left instanceof OperatorNode) { - OperatorNode leftOp = (OperatorNode) node.left; - if (leftOp.operator.equals("@")) { - // This is an array slice - handle it specially - handleArraySlice(node, leftOp); - return; + varRegs.add(rd); + } + } } - // Handle normal array element access: $array[index] - if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode) { - handleArrayElementAccess(node, leftOp); - return; - } - } + // Return a list of the localized variables (or their references) + int resultReg = allocateRegister(); - // Handle general case: expr[index] - // This covers cases like $matrix[1][0] where $matrix[1] is an expression - handleGeneralArrayAccess(node); - return; - } + 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); + } - // Handle {} operator specially for hash slice operations - // Must be before automatic operand compilation to avoid compiling @ operator - if (node.operator.equals("{")) { - currentTokenIndex = node.getIndex(); - - // Check if this is a hash slice: @hash{keys} or @$hashref{keys} - if (node.left instanceof OperatorNode) { - OperatorNode leftOp = (OperatorNode) node.left; - if (leftOp.operator.equals("@")) { - // This is a hash slice - handle it specially - handleHashSlice(node, leftOp); - return; + // 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); + } } - // Handle normal hash element access: $hash{key} - if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode) { - handleHashElementAccess(node, leftOp); - return; - } + lastResultReg = resultReg; + return; } - - // Handle general case: expr{key} - // This covers cases like $hash{outer}{inner} where $hash{outer} is an expression - handleGeneralHashAccess(node); - return; - } - - // Handle push/unshift operators - if (node.operator.equals("push") || node.operator.equals("unshift")) { - handlePushUnshift(node); - return; - } - - // Handle "join" operator specially to ensure proper context - // Left operand (separator) needs SCALAR context, right operand (list) needs LIST context - if (node.operator.equals("join")) { - // Save and set context for left operand (separator) - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(this); - int rs1 = lastResultReg; - - // Set context for right operand (array/list) - currentCallContext = RuntimeContextType.LIST; - node.right.accept(this); - int rs2 = lastResultReg; - currentCallContext = savedContext; - - // Emit JOIN opcode - int rd = allocateRegister(); - emit(Opcodes.JOIN); - emitReg(rd); - emitReg(rs1); - emitReg(rs2); - - lastResultReg = rd; - return; - } - - // Handle function call operators specially to ensure arguments are in LIST context - if (node.operator.equals("(") || node.operator.equals("()")) { - // Function call: subname(args) or $coderef->(args) - // Save and set context for left operand (code reference) - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(this); - int rs1 = lastResultReg; - - // Arguments must ALWAYS be evaluated in LIST context - // Even if the call itself is in SCALAR context (e.g., scalar(func())) - currentCallContext = RuntimeContextType.LIST; - node.right.accept(this); - int rs2 = lastResultReg; - currentCallContext = savedContext; - - // Emit CALL_SUB opcode - int rd = compileBinaryOperatorSwitch(node.operator, rs1, rs2, node.getIndex()); - lastResultReg = rd; - return; - } - - // Handle short-circuit operators specially - don't compile right operand yet! - if (node.operator.equals("&&") || node.operator.equals("and")) { - // Logical AND with short-circuit evaluation - // Only evaluate right side if left side is true - - // Compile left operand in scalar context (need boolean value) - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(this); - int rs1 = lastResultReg; - currentCallContext = savedContext; - - // Allocate result register and move left value to it - int rd = allocateRegister(); - emit(Opcodes.MOVE); - emitReg(rd); - emitReg(rs1); - - // Mark position for forward jump - int skipRightPos = bytecode.size(); - - // Emit conditional jump: if (!rd) skip right evaluation - emit(Opcodes.GOTO_IF_FALSE); - emitReg(rd); - emitInt(0); // Placeholder for offset (will be patched) - - // NOW compile right operand (only executed if left was true) - node.right.accept(this); - int rs2 = lastResultReg; - - // Move right result to rd (overwriting left value) - emit(Opcodes.MOVE); - emitReg(rd); - emitReg(rs2); - - // Patch the forward jump offset - int skipRightTarget = bytecode.size(); - patchIntOffset(skipRightPos + 2, skipRightTarget); - - lastResultReg = rd; - return; - } - - if (node.operator.equals("||") || node.operator.equals("or")) { - // Logical OR with short-circuit evaluation - // Only evaluate right side if left side is false - - // Compile left operand in scalar context (need boolean value) - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(this); - int rs1 = lastResultReg; - currentCallContext = savedContext; - - // Allocate result register and move left value to it - int rd = allocateRegister(); - emit(Opcodes.MOVE); - emitReg(rd); - emitReg(rs1); - - // Mark position for forward jump - int skipRightPos = bytecode.size(); - - // Emit conditional jump: if (rd) skip right evaluation - emit(Opcodes.GOTO_IF_TRUE); - emitReg(rd); - emitInt(0); // Placeholder for offset (will be patched) - - // NOW compile right operand (only executed if left was false) - node.right.accept(this); - int rs2 = lastResultReg; - - // Move right result to rd (overwriting left value) - emit(Opcodes.MOVE); - emitReg(rd); - emitReg(rs2); - - // Patch the forward jump offset - int skipRightTarget = bytecode.size(); - patchIntOffset(skipRightPos + 2, skipRightTarget); - - lastResultReg = rd; - return; + throwCompilerException("Unsupported local operand: " + node.operand.getClass().getSimpleName()); } + throwCompilerException("Unsupported variable declaration operator: " + op); + } - if (node.operator.equals("//")) { - // Defined-OR with short-circuit evaluation - // Only evaluate right side if left side is undefined - - // Compile left operand in scalar context (need to test definedness) - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - node.left.accept(this); - int rs1 = lastResultReg; - currentCallContext = savedContext; - - // Allocate result register and move left value to it - int rd = allocateRegister(); - emit(Opcodes.MOVE); - emitReg(rd); - emitReg(rs1); - - // Check if left is defined - int definedReg = allocateRegister(); - emit(Opcodes.DEFINED); - emitReg(definedReg); - emitReg(rd); - - // Mark position for forward jump - int skipRightPos = bytecode.size(); - - // Emit conditional jump: if (defined) skip right evaluation - emit(Opcodes.GOTO_IF_TRUE); - emitReg(definedReg); - emitInt(0); // Placeholder for offset (will be patched) + void compileVariableReference(OperatorNode node, String op) { + if (op.equals("$")) { + // Scalar variable dereference: $x + if (node.operand instanceof IdentifierNode) { + String varName = "$" + ((IdentifierNode) node.operand).name; - // NOW compile right operand (only executed if left was undefined) - node.right.accept(this); - int rs2 = lastResultReg; + // Check if this is a closure variable captured from outer scope via PersistentVariable + if (currentSubroutineBeginId != 0 && currentSubroutineClosureVars.contains(varName)) { + // This is a closure variable - use RETRIEVE_BEGIN_SCALAR + int rd = allocateRegister(); + int nameIdx = addToStringPool(varName); - // Move right result to rd (overwriting left value) - emit(Opcodes.MOVE); - emitReg(rd); - emitReg(rs2); + emitWithToken(Opcodes.RETRIEVE_BEGIN_SCALAR, node.getIndex()); + emitReg(rd); + emit(nameIdx); + emit(currentSubroutineBeginId); - // Patch the forward jump offset - int skipRightTarget = bytecode.size(); - patchIntOffset(skipRightPos + 2, skipRightTarget); + lastResultReg = rd; + } else if (hasVariable(varName)) { + // Lexical variable - use existing register + lastResultReg = getVariableRegister(varName); + } else { + // Global variable - load it + // Use NameNormalizer to properly handle special variables (like $&) + // which must always be in the "main" package + String globalVarName = varName.substring(1); // Remove $ sigil first + globalVarName = org.perlonjava.runtime.NameNormalizer.normalizeVariableName( + globalVarName, + getCurrentPackage() + ); - lastResultReg = rd; - return; - } + int rd = allocateRegister(); + int nameIdx = addToStringPool(globalVarName); - // Compile left and right operands (for non-short-circuit operators) - node.left.accept(this); - int rs1 = lastResultReg; + emit(Opcodes.LOAD_GLOBAL_SCALAR); + emitReg(rd); + emit(nameIdx); - node.right.accept(this); - int rs2 = lastResultReg; + lastResultReg = rd; + } + } else if (node.operand instanceof BlockNode) { + // Symbolic reference via block: ${label:expr} or ${expr} + // Execute the block to get a variable name string, then load that variable + BlockNode block = (BlockNode) node.operand; - // Emit opcode based on operator (delegated to helper method) - int rd = compileBinaryOperatorSwitch(node.operator, rs1, rs2, node.getIndex()); + // Compile the block + block.accept(this); + int blockResultReg = lastResultReg; + // Load via symbolic reference + int rd = allocateRegister(); + emitWithToken(Opcodes.LOAD_SYMBOLIC_SCALAR, node.getIndex()); + emitReg(rd); + emitReg(blockResultReg); - lastResultReg = rd; - } + 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; - /** - * 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") || 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; - String sigil = sigilOp.operator; + // Dereference the result + int rd = allocateRegister(); + emitWithToken(Opcodes.DEREF, node.getIndex()); + emitReg(rd); + emitReg(refReg); - if (sigilOp.operand instanceof IdentifierNode) { - String varName = sigil + ((IdentifierNode) sigilOp.operand).name; + lastResultReg = rd; + } else { + throwCompilerException("Unsupported $ operand: " + node.operand.getClass().getSimpleName()); + } + } else if (op.equals("@")) { + // Array variable dereference: @x or @_ or @$arrayref + if (node.operand instanceof IdentifierNode) { + String varName = "@" + ((IdentifierNode) node.operand).name; - // Check if this is a declared reference (my \$x or state \$x) - boolean isDeclaredReference = node.annotations != null && - Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); + // Special case: @_ is register 1 + if (varName.equals("@_")) { + int arrayReg = 1; // @_ is always in register 1 - // 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 + // Check if we're in scalar context - if so, return array size + if (currentCallContext == RuntimeContextType.SCALAR) { + int rd = allocateRegister(); + emit(Opcodes.ARRAY_SIZE); + emitReg(rd); + emitReg(arrayReg); + lastResultReg = rd; + } else { + lastResultReg = arrayReg; + } + return; + } - // For state variables, retrieve or initialize the persistent variable - // For captured variables, retrieve the BEGIN-initialized variable - int reg = allocateRegister(); - int nameIdx = addToStringPool(varName); + // Check if this is a closure variable captured from outer scope via PersistentVariable + int arrayReg; + if (currentSubroutineBeginId != 0 && currentSubroutineClosureVars.contains(varName)) { + // This is a closure variable - use RETRIEVE_BEGIN_ARRAY + arrayReg = allocateRegister(); + int nameIdx = addToStringPool(varName); - switch (sigil) { - case "$" -> { - emitWithToken(Opcodes.RETRIEVE_BEGIN_SCALAR, node.getIndex()); - emitReg(reg); - emit(nameIdx); - emit(sigilOp.id); - // 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 - } - case "@" -> { - emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); - emitReg(reg); - emit(nameIdx); - emit(sigilOp.id); - variableScopes.peek().put(varName, reg); - allDeclaredVariables.put(varName, reg); // Track for variableRegistry - } - case "%" -> { - emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); - emitReg(reg); - emit(nameIdx); - emit(sigilOp.id); - variableScopes.peek().put(varName, reg); - allDeclaredVariables.put(varName, reg); // Track for variableRegistry - } - default -> throwCompilerException("Unsupported variable type: " + sigil); - } - - // 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, not state) - int reg = addVariable(varName, "my"); - - // Normal initialization: load undef/empty array/empty hash - switch (sigil) { - case "$" -> { - emit(Opcodes.LOAD_UNDEF); - emitReg(reg); - } - case "@" -> { - emit(Opcodes.NEW_ARRAY); - emitReg(reg); - } - case "%" -> { - emit(Opcodes.NEW_HASH); - emitReg(reg); - } - default -> throwCompilerException("Unsupported variable type: " + sigil); - } - - // 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 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); - - switch (sigil) { - case "$" -> { - emitWithToken(Opcodes.RETRIEVE_BEGIN_SCALAR, node.getIndex()); - emitReg(reg); - emit(nameIdx); - emit(sigilOp.id); - variableScopes.peek().put(varName, reg); - allDeclaredVariables.put(varName, reg); // Track for variableRegistry - } - case "@" -> { - emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); - emitReg(reg); - emit(nameIdx); - emit(sigilOp.id); - variableScopes.peek().put(varName, reg); - allDeclaredVariables.put(varName, reg); // Track for variableRegistry - } - case "%" -> { - emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); - emitReg(reg); - emit(nameIdx); - emit(sigilOp.id); - variableScopes.peek().put(varName, reg); - allDeclaredVariables.put(varName, reg); // Track for variableRegistry - } - default -> throwCompilerException("Unsupported variable type in list declaration: " + sigil); - } - - varRegs.add(reg); - } else { - // Regular lexical variable (not captured, not state) - int reg = addVariable(varName, op); - - // Initialize the variable - switch (sigil) { - case "$" -> { - emit(Opcodes.LOAD_UNDEF); - emitReg(reg); - } - case "@" -> { - emit(Opcodes.NEW_ARRAY); - emitReg(reg); - } - case "%" -> { - emit(Opcodes.NEW_HASH); - emitReg(reg); - } - default -> throwCompilerException("Unsupported variable type in list declaration: " + sigil); - } - - 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 (or their references if isDeclaredReference) - 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 my operand: " + node.operand.getClass().getSimpleName()); - } else if (op.equals("our")) { - // our $x / our @x / our %x - package variable declaration - // The operand will be OperatorNode("$"/"@"/"%", IdentifierNode("x")) - if (node.operand instanceof OperatorNode) { - OperatorNode sigilOp = (OperatorNode) node.operand; - String sigil = sigilOp.operator; - - 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 - 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; - } - - // Allocate register and add to symbol table - int reg = addVariable(varName, "our"); - - // Load from global variable using normalized name - String globalVarName = NameNormalizer.normalizeVariableName( - ((IdentifierNode) sigilOp.operand).name, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalVarName); - - switch (sigil) { - 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); - } - default -> throwCompilerException("Unsupported variable type: " + sigil); - } - - // 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; - - int reg; - // Check if already declared in current scope - if (hasVariable(varName)) { - // Already declared, just use existing register - reg = getVariableRegister(varName); - } else { - // Allocate register and add to symbol table - reg = addVariable(varName, "our"); - - // Load from global variable using normalized name - String globalVarName = NameNormalizer.normalizeVariableName( - ((IdentifierNode) sigilOp.operand).name, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalVarName); - - switch (sigil) { - 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); - } - default -> throwCompilerException("Unsupported variable type in list declaration: " + sigil); - } - } - - varRegs.add(reg); - } else { - throwCompilerException("our list declaration requires identifier: " + sigilOp.operand.getClass().getSimpleName()); - } - } else { - throwCompilerException("our list declaration requires scalar/array/hash: " + element.getClass().getSimpleName()); - } - } - - // Return a list of the declared variables (or their references if isDeclaredReference) - 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 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")) - if (node.operand instanceof OperatorNode) { - OperatorNode sigilOp = (OperatorNode) node.operand; - String sigil = sigilOp.operator; - - if (sigil.equals("$") && sigilOp.operand instanceof IdentifierNode) { - String varName = "$" + ((IdentifierNode) sigilOp.operand).name; - - // Check if it's a lexical variable (should not be localized) - if (hasVariable(varName)) { - throwCompilerException("Can't localize lexical variable " + varName); - 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; - int nameIdx = addToStringPool(globalVarName); - - int rd = allocateRegister(); - emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); - emitReg(rd); - emit(nameIdx); - - // 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()); - } - throwCompilerException("Unsupported variable declaration operator: " + op); - } - - private void compileVariableReference(OperatorNode node, String op) { - if (op.equals("$")) { - // Scalar variable dereference: $x - if (node.operand instanceof IdentifierNode) { - String varName = "$" + ((IdentifierNode) node.operand).name; - - // Check if this is a closure variable captured from outer scope via PersistentVariable - if (currentSubroutineBeginId != 0 && currentSubroutineClosureVars.contains(varName)) { - // This is a closure variable - use RETRIEVE_BEGIN_SCALAR - int rd = allocateRegister(); - int nameIdx = addToStringPool(varName); - - emitWithToken(Opcodes.RETRIEVE_BEGIN_SCALAR, node.getIndex()); - emitReg(rd); - emit(nameIdx); - emit(currentSubroutineBeginId); - - lastResultReg = rd; - } else if (hasVariable(varName)) { - // Lexical variable - use existing register - lastResultReg = getVariableRegister(varName); - } else { - // Global variable - load it - // Use NameNormalizer to properly handle special variables (like $&) - // which must always be in the "main" package - String globalVarName = varName.substring(1); // Remove $ sigil first - globalVarName = org.perlonjava.runtime.NameNormalizer.normalizeVariableName( - globalVarName, - getCurrentPackage() - ); - - int rd = allocateRegister(); - int nameIdx = addToStringPool(globalVarName); - - emit(Opcodes.LOAD_GLOBAL_SCALAR); - emitReg(rd); - emit(nameIdx); - - lastResultReg = rd; - } - } else if (node.operand instanceof BlockNode) { - // Symbolic reference via block: ${label:expr} or ${expr} - // Execute the block to get a variable name string, then load that variable - BlockNode block = (BlockNode) node.operand; - - // Compile the block - block.accept(this); - int blockResultReg = lastResultReg; - - // Load via symbolic reference - int rd = allocateRegister(); - emitWithToken(Opcodes.LOAD_SYMBOLIC_SCALAR, node.getIndex()); - 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()); - } - } else if (op.equals("@")) { - // Array variable dereference: @x or @_ or @$arrayref - if (node.operand instanceof IdentifierNode) { - String varName = "@" + ((IdentifierNode) node.operand).name; - - // Special case: @_ is register 1 - if (varName.equals("@_")) { - int arrayReg = 1; // @_ is always in register 1 - - // Check if we're in scalar context - if so, return array size - if (currentCallContext == RuntimeContextType.SCALAR) { - int rd = allocateRegister(); - emit(Opcodes.ARRAY_SIZE); - emitReg(rd); - emitReg(arrayReg); - lastResultReg = rd; - } else { - lastResultReg = arrayReg; - } - return; - } - - // Check if this is a closure variable captured from outer scope via PersistentVariable - int arrayReg; - if (currentSubroutineBeginId != 0 && currentSubroutineClosureVars.contains(varName)) { - // This is a closure variable - use RETRIEVE_BEGIN_ARRAY - arrayReg = allocateRegister(); - int nameIdx = addToStringPool(varName); - - emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); - emitReg(arrayReg); - emit(nameIdx); - emit(currentSubroutineBeginId); - } else if (hasVariable(varName)) { - // Lexical array - use existing register - arrayReg = getVariableRegister(varName); - } else { - // Global array - load it - arrayReg = allocateRegister(); - String globalArrayName = NameNormalizer.normalizeVariableName(((IdentifierNode) node.operand).name, getCurrentPackage()); - int nameIdx = addToStringPool(globalArrayName); - - emit(Opcodes.LOAD_GLOBAL_ARRAY); - emitReg(arrayReg); - emit(nameIdx); - } - - // Check if we're in scalar context - if so, return array size - if (currentCallContext == RuntimeContextType.SCALAR) { - int rd = allocateRegister(); - emit(Opcodes.ARRAY_SIZE); - emitReg(rd); - emitReg(arrayReg); - lastResultReg = rd; - } else { - lastResultReg = arrayReg; - } - } else if (node.operand instanceof OperatorNode) { - // Dereference: @$arrayref or @{$hashref} - OperatorNode operandOp = (OperatorNode) node.operand; - - // Compile the reference - operandOp.accept(this); - int refReg = lastResultReg; - - // Dereference to get the array - // The reference should contain a RuntimeArray - // For @$scalar, we need to dereference it - int rd = allocateRegister(); - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(rd); - emitReg(refReg); - - lastResultReg = rd; - // Note: We don't check scalar context here because dereferencing - // should return the array itself. The slice or other operation - // will handle scalar context conversion if needed. - } else if (node.operand instanceof BlockNode) { - // @{ block } - evaluate block and dereference the result - // The block should return an arrayref - BlockNode blockNode = (BlockNode) node.operand; - blockNode.accept(this); - int refReg = lastResultReg; - - // Dereference to get the array - int rd = allocateRegister(); - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(rd); - emitReg(refReg); - - lastResultReg = rd; - } else { - throwCompilerException("Unsupported @ operand: " + node.operand.getClass().getSimpleName()); - } - } else if (op.equals("%")) { - // Hash variable dereference: %x - if (node.operand instanceof IdentifierNode) { - String varName = "%" + ((IdentifierNode) node.operand).name; - - // Check if it's a lexical hash - if (hasVariable(varName)) { - // Lexical hash - use existing register - lastResultReg = getVariableRegister(varName); - return; - } - - // Global hash - load it - int rd = allocateRegister(); - String globalHashName = NameNormalizer.normalizeVariableName(((IdentifierNode) node.operand).name, getCurrentPackage()); - int nameIdx = addToStringPool(globalHashName); - - emit(Opcodes.LOAD_GLOBAL_HASH); - emitReg(rd); - emit(nameIdx); - - lastResultReg = rd; - } else { - throwCompilerException("Unsupported % operand: " + node.operand.getClass().getSimpleName()); - } - } else if (op.equals("*")) { - // Glob variable dereference: *x - if (node.operand instanceof IdentifierNode) { - IdentifierNode idNode = (IdentifierNode) node.operand; - String varName = idNode.name; - - // Add package prefix if not present - if (!varName.contains("::")) { - varName = "main::" + varName; - } - - // Allocate register for glob - int rd = allocateRegister(); - int nameIdx = addToStringPool(varName); - - // Emit direct opcode LOAD_GLOB - emitWithToken(Opcodes.LOAD_GLOB, node.getIndex()); - emitReg(rd); - emit(nameIdx); - - lastResultReg = rd; - } else { - throwCompilerException("Unsupported * operand: " + node.operand.getClass().getSimpleName()); - } - } else if (op.equals("&")) { - // Code reference: &subname - // Gets a reference to a named subroutine - if (node.operand instanceof IdentifierNode) { - IdentifierNode idNode = (IdentifierNode) node.operand; - String subName = idNode.name; - - // Use NameNormalizer to properly handle package prefixes - // This will add the current package if no package is specified - subName = NameNormalizer.normalizeVariableName(subName, getCurrentPackage()); - - // Allocate register for code reference - int rd = allocateRegister(); - int nameIdx = addToStringPool(subName); - - // Emit LOAD_GLOBAL_CODE - emit(Opcodes.LOAD_GLOBAL_CODE); - emitReg(rd); - emit(nameIdx); - - lastResultReg = rd; - } else { - throwCompilerException("Unsupported & operand: " + node.operand.getClass().getSimpleName()); - } - } else if (op.equals("\\")) { - // Reference operator: \$x, \@x, \%x, \*x, etc. - if (node.operand != null) { - // Compile operand in LIST context to get the actual value - // Example: \@array should get a reference to the array itself, - // not its size (which would happen in SCALAR context) - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.LIST; - try { - node.operand.accept(this); - int valueReg = lastResultReg; - - // Allocate register for reference - int rd = allocateRegister(); - - // Emit CREATE_REF - emit(Opcodes.CREATE_REF); - emitReg(rd); - emitReg(valueReg); - - lastResultReg = rd; - } finally { - currentCallContext = savedContext; - } - } else { - throwCompilerException("Reference operator requires operand"); - } - } - } - - @Override - public void visit(OperatorNode node) { - // Track token index for error reporting - currentTokenIndex = node.getIndex(); - - String op = node.operator; - - // 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; - } - - // Group 2: Variable reference operators ($, @, %, *, &, \) - if (op.equals("$") || op.equals("@") || op.equals("%") || op.equals("*") || op.equals("&") || op.equals("\\")) { - compileVariableReference(node, op); - return; - } - - // Handle remaining operators - if (op.equals("scalar")) { - // Force scalar context: scalar(expr) - // Evaluates the operand and converts the result to scalar - if (node.operand != null) { - // Evaluate operand in scalar context - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - try { - node.operand.accept(this); - int operandReg = lastResultReg; - - // Emit ARRAY_SIZE to convert to scalar - // This handles arrays/hashes (converts to size) and passes through scalars - int rd = allocateRegister(); - emit(Opcodes.ARRAY_SIZE); - emitReg(rd); - emitReg(operandReg); - - lastResultReg = rd; - } finally { - currentCallContext = savedContext; - } - } else { - throwCompilerException("scalar operator requires an operand"); - } - return; - } else if (op.equals("package") || op.equals("class")) { - // Package/Class declaration: package Foo; or class Foo; - // This updates the current package context for subsequent variable declarations - if (node.operand instanceof IdentifierNode) { - String packageName = ((IdentifierNode) node.operand).name; - - // Check if this is a class declaration (either "class" operator or isClass annotation) - Boolean isClassAnnotation = (Boolean) node.getAnnotation("isClass"); - boolean isClass = op.equals("class") || (isClassAnnotation != null && isClassAnnotation); - - // Update the current package/class in symbol table - // This tracks package name, isClass flag, and version - symbolTable.setCurrentPackage(packageName, isClass); - - // Register as Perl 5.38+ class for proper stringification if needed - if (isClass) { - org.perlonjava.runtime.ClassRegistry.registerClass(packageName); - } - - lastResultReg = -1; // No runtime value - } else { - throwCompilerException(op + " operator requires an identifier"); - } - } else if (op.equals("say") || op.equals("print")) { - // say/print $x - if (node.operand != null) { - node.operand.accept(this); - int rs = lastResultReg; - - emit(op.equals("say") ? Opcodes.SAY : Opcodes.PRINT); - emitReg(rs); - } - } else if (op.equals("not") || op.equals("!")) { - // Logical NOT operator: not $x or !$x - // Evaluate operand in scalar context (need boolean value) - if (node.operand != null) { - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - node.operand.accept(this); - int rs = lastResultReg; - currentCallContext = savedContext; - - // Allocate result register - int rd = allocateRegister(); - - // Emit NOT opcode - emit(Opcodes.NOT); - emitReg(rd); - emitReg(rs); - - lastResultReg = rd; - } else { - throwCompilerException("NOT operator requires operand"); - } - } else if (op.equals("~") || op.equals("binary~")) { - // Bitwise NOT operator: ~$x or binary~$x - // Evaluate operand and emit BITWISE_NOT_BINARY opcode - if (node.operand != null) { - node.operand.accept(this); - int rs = lastResultReg; - - // Allocate result register - int rd = allocateRegister(); - - // Emit BITWISE_NOT_BINARY opcode - emit(Opcodes.BITWISE_NOT_BINARY); - emitReg(rd); - emitReg(rs); - - lastResultReg = rd; - } else { - throwCompilerException("Bitwise NOT operator requires operand"); - } - } else if (op.equals("~.")) { - // String bitwise NOT operator: ~.$x - // Evaluate operand and emit BITWISE_NOT_STRING opcode - if (node.operand != null) { - node.operand.accept(this); - int rs = lastResultReg; - - // Allocate result register - int rd = allocateRegister(); - - // Emit BITWISE_NOT_STRING opcode - emit(Opcodes.BITWISE_NOT_STRING); - emitReg(rd); - emitReg(rs); - - lastResultReg = rd; - } else { - throwCompilerException("String bitwise NOT operator requires operand"); - } - } else if (op.equals("defined")) { - // Defined operator: defined($x) - // Check if value is defined (not undef) - if (node.operand != null) { - node.operand.accept(this); - int rs = lastResultReg; - - // Allocate result register - int rd = allocateRegister(); - - // Emit DEFINED opcode - emit(Opcodes.DEFINED); - emitReg(rd); - emitReg(rs); - - lastResultReg = rd; - } else { - throwCompilerException("defined operator requires operand"); - } - } else if (op.equals("ref")) { - // Ref operator: ref($x) - // Get reference type (blessed class name or base type) - if (node.operand == null) { - throwCompilerException("ref requires an argument"); - } - - // Compile the operand - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (list.elements.isEmpty()) { - throwCompilerException("ref requires an argument"); - } - // Get first element - list.elements.get(0).accept(this); - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - - // Allocate result register - int rd = allocateRegister(); - - // Emit REF opcode - emit(Opcodes.REF); - emitReg(rd); - emitReg(argReg); - - lastResultReg = rd; - } else if (op.equals("prototype")) { - // Prototype operator: prototype(\&func) or prototype("func_name") - // Returns the prototype string for a subroutine - if (node.operand == null) { - throwCompilerException("prototype requires an argument"); - } - - // Compile the operand (code reference or function name) - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (list.elements.isEmpty()) { - throwCompilerException("prototype requires an argument"); - } - // Get first element - list.elements.get(0).accept(this); - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - - // Allocate result register - int rd = allocateRegister(); - - // Add current package to string pool - int packageIdx = addToStringPool(getCurrentPackage()); - - // Emit PROTOTYPE opcode - emit(Opcodes.PROTOTYPE); - emitReg(rd); - emitReg(argReg); - emitInt(packageIdx); - - lastResultReg = rd; - } else if (op.equals("quoteRegex")) { - // Quote regex operator: qr{pattern}flags - // operand is a ListNode with [pattern, flags] - if (node.operand == null || !(node.operand instanceof ListNode)) { - throwCompilerException("quoteRegex requires pattern and flags"); - } - - ListNode operand = (ListNode) node.operand; - if (operand.elements.size() < 2) { - throwCompilerException("quoteRegex requires pattern and flags"); - } - - // Compile pattern and flags - operand.elements.get(0).accept(this); // Pattern - int patternReg = lastResultReg; - - operand.elements.get(1).accept(this); // Flags - int flagsReg = lastResultReg; - - // Allocate result register - int rd = allocateRegister(); - - // Emit QUOTE_REGEX opcode - emit(Opcodes.QUOTE_REGEX); - emitReg(rd); - emitReg(patternReg); - emitReg(flagsReg); - - lastResultReg = rd; - } else if (op.equals("++") || op.equals("--") || op.equals("++postfix") || op.equals("--postfix")) { - // Pre/post increment/decrement - boolean isPostfix = op.endsWith("postfix"); - boolean isIncrement = op.startsWith("++"); - - if (node.operand instanceof IdentifierNode) { - String varName = ((IdentifierNode) node.operand).name; - - if (hasVariable(varName)) { - int varReg = getVariableRegister(varName); - - // Use optimized autoincrement/decrement opcodes - if (isPostfix) { - // Postfix: returns old value before modifying - // Need TWO registers: one for result (old value), one for variable - int resultReg = allocateRegister(); - if (isIncrement) { - emit(Opcodes.POST_AUTOINCREMENT); - } else { - emit(Opcodes.POST_AUTODECREMENT); - } - emitReg(resultReg); // Destination for old value - emitReg(varReg); // Variable to modify in-place - lastResultReg = resultReg; - } else { - // Prefix: returns new value after modifying - if (isIncrement) { - emit(Opcodes.PRE_AUTOINCREMENT); - } else { - emit(Opcodes.PRE_AUTODECREMENT); - } - emitReg(varReg); - lastResultReg = varReg; - } - } else { - throwCompilerException("Increment/decrement of non-lexical variable not yet supported"); - } - } else if (node.operand instanceof OperatorNode) { - // Handle $x++ - OperatorNode innerOp = (OperatorNode) node.operand; - if (innerOp.operator.equals("$") && innerOp.operand instanceof IdentifierNode) { - String varName = "$" + ((IdentifierNode) innerOp.operand).name; - - if (hasVariable(varName)) { - int varReg = getVariableRegister(varName); - - // Use optimized autoincrement/decrement opcodes - if (isPostfix) { - // Postfix: returns old value before modifying - // Need TWO registers: one for result (old value), one for variable - int resultReg = allocateRegister(); - if (isIncrement) { - emit(Opcodes.POST_AUTOINCREMENT); - } else { - emit(Opcodes.POST_AUTODECREMENT); - } - emitReg(resultReg); // Destination for old value - emitReg(varReg); // Variable to modify in-place - lastResultReg = resultReg; - } else { - if (isIncrement) { - emit(Opcodes.PRE_AUTOINCREMENT); - } else { - emit(Opcodes.PRE_AUTODECREMENT); - } - emitReg(varReg); - lastResultReg = varReg; - } - } else { - // Global variable increment/decrement - // Normalize global variable name (remove sigil, add package) - String bareVarName = varName.substring(1); // Remove "$" - String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, getCurrentPackage()); - int nameIdx = addToStringPool(normalizedName); - - // Load global variable - int globalReg = allocateRegister(); - emit(Opcodes.LOAD_GLOBAL_SCALAR); - emitReg(globalReg); - emit(nameIdx); - - // Apply increment/decrement - if (isPostfix) { - // Postfix: returns old value before modifying - // Need TWO registers: one for result (old value), one for variable - int resultReg = allocateRegister(); - if (isIncrement) { - emit(Opcodes.POST_AUTOINCREMENT); - } else { - emit(Opcodes.POST_AUTODECREMENT); - } - emitReg(resultReg); // Destination for old value - emitReg(globalReg); // Variable to modify in-place - lastResultReg = resultReg; - } else { - if (isIncrement) { - emit(Opcodes.PRE_AUTOINCREMENT); - } else { - emit(Opcodes.PRE_AUTODECREMENT); - } - emitReg(globalReg); - lastResultReg = globalReg; - } - - // NOTE: Do NOT store back to global variable! - // The POST/PRE_AUTO* opcodes modify the global variable directly - // and return the appropriate value (old for postfix, new for prefix). - // Storing back would overwrite the modification with the return value. - } - } else { - throwCompilerException("Invalid operand for increment/decrement operator"); - } - } else { - throwCompilerException("Increment/decrement operator requires operand"); - } - } else if (op.equals("return")) { - // return $expr; - // Also handles 'goto &NAME' tail calls (parsed as 'return (coderef(@_))') - - // Check if this is a 'goto &NAME' or 'goto EXPR' tail call - // Pattern: return with ListNode containing single BinaryOperatorNode("(") - // where left is OperatorNode("&") and right is @_ - if (node.operand instanceof ListNode list && list.elements.size() == 1) { - Node firstElement = list.elements.getFirst(); - if (firstElement instanceof BinaryOperatorNode callNode && callNode.operator.equals("(")) { - Node callTarget = callNode.left; - - // Handle &sub syntax (goto &foo) - if (callTarget instanceof OperatorNode opNode && opNode.operator.equals("&")) { - // This is a tail call: goto &sub - // Evaluate the code reference in scalar context - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - callTarget.accept(this); - int codeRefReg = lastResultReg; - - // Evaluate the arguments in list context (usually @_) - currentCallContext = RuntimeContextType.LIST; - callNode.right.accept(this); - int argsReg = lastResultReg; - currentCallContext = savedContext; - - // Allocate register for call result - int rd = allocateRegister(); - - // Emit CALL_SUB to invoke the code reference with proper context - emit(Opcodes.CALL_SUB); - emitReg(rd); // Result register - emitReg(codeRefReg); // Code reference register - emitReg(argsReg); // Arguments register - emit(savedContext); // Use saved calling context for the tail call - - // Then return the result - emitWithToken(Opcodes.RETURN, node.getIndex()); - emitReg(rd); - - lastResultReg = -1; - return; - } - } - } - - if (node.operand != null) { - // Regular return with expression - node.operand.accept(this); - int exprReg = lastResultReg; - - // Emit RETURN with expression register - emitWithToken(Opcodes.RETURN, node.getIndex()); - emitReg(exprReg); - } else { - // return; (no value - return empty list/undef) - int undefReg = allocateRegister(); - emit(Opcodes.LOAD_UNDEF); - emitReg(undefReg); - - emitWithToken(Opcodes.RETURN, node.getIndex()); - emitReg(undefReg); - } - lastResultReg = -1; // No result after return - } else if (op.equals("last") || op.equals("next") || op.equals("redo")) { - // Loop control operators: last/next/redo [LABEL] - handleLoopControlOperator(node, op); - lastResultReg = -1; // No result after control flow - } else if (op.equals("rand")) { - // rand() or rand($max) - // Calls Random.rand(max) where max defaults to 1 - int rd = allocateRegister(); - - if (node.operand != null) { - // rand($max) - evaluate operand - node.operand.accept(this); - int maxReg = lastResultReg; - - // Emit RAND opcode - emit(Opcodes.RAND); - emitReg(rd); - emitReg(maxReg); - } else { - // rand() with no argument - defaults to 1 - int oneReg = allocateRegister(); - emit(Opcodes.LOAD_INT); - emitReg(oneReg); - emitInt(1); - - // Emit RAND opcode - emit(Opcodes.RAND); - emitReg(rd); - emitReg(oneReg); - } - - lastResultReg = rd; - } else if (op.equals("sleep")) { - // sleep $seconds - // Calls Time.sleep(seconds) - int rd = allocateRegister(); - - if (node.operand != null) { - // sleep($seconds) - evaluate operand - node.operand.accept(this); - int secondsReg = lastResultReg; - - // Emit direct opcode SLEEP_OP - emit(Opcodes.SLEEP_OP); - emitReg(rd); - emitReg(secondsReg); - } else { - // sleep with no argument - defaults to infinity (but we'll use a large number) - int maxReg = allocateRegister(); - emit(Opcodes.LOAD_INT); - emitReg(maxReg); - emitInt(Integer.MAX_VALUE); - - emit(Opcodes.SLEEP_OP); - emitReg(rd); - emitReg(maxReg); - } - - lastResultReg = rd; - } else if (op.equals("study")) { - // study $var - // In modern Perl, study is a no-op that always returns true - // We evaluate the operand for side effects, then return 1 - - if (node.operand != null) { - // Evaluate operand for side effects (though typically there are none) - node.operand.accept(this); - } - - // Return 1 (true) - int rd = allocateRegister(); - emit(Opcodes.LOAD_INT); - emitReg(rd); - emitInt(1); - - lastResultReg = rd; - } else if (op.equals("require")) { - // require MODULE_NAME or require VERSION - // Evaluate operand in scalar context - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - try { - node.operand.accept(this); - int operandReg = lastResultReg; - - // Call ModuleOperators.require() - int rd = allocateRegister(); - emit(Opcodes.REQUIRE); - emitReg(rd); - emitReg(operandReg); - - lastResultReg = rd; - } finally { - currentCallContext = savedContext; - } - } else if (op.equals("pos")) { - // pos($var) - get or set regex match position - // Returns an lvalue that can be assigned to - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - try { - node.operand.accept(this); - int operandReg = lastResultReg; - - // Call RuntimeScalar.pos() - int rd = allocateRegister(); - emit(Opcodes.POS); - emitReg(rd); - emitReg(operandReg); - - lastResultReg = rd; - } finally { - currentCallContext = savedContext; - } - } else if (op.equals("index") || op.equals("rindex")) { - // index(str, substr, pos?) or rindex(str, substr, pos?) - if (node.operand instanceof ListNode) { - ListNode args = (ListNode) node.operand; - - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - try { - // Evaluate first arg (string) - if (args.elements.isEmpty()) { - throwCompilerException("Not enough arguments for " + op); - } - args.elements.get(0).accept(this); - int strReg = lastResultReg; - - // Evaluate second arg (substring) - if (args.elements.size() < 2) { - throwCompilerException("Not enough arguments for " + op); - } - args.elements.get(1).accept(this); - int substrReg = lastResultReg; - - // Evaluate third arg (position) - optional, defaults to undef - int posReg; - if (args.elements.size() >= 3) { - args.elements.get(2).accept(this); - posReg = lastResultReg; - } else { - posReg = allocateRegister(); - emit(Opcodes.LOAD_UNDEF); - emitReg(posReg); - } - - // Call index or rindex - int rd = allocateRegister(); - emit(op.equals("index") ? Opcodes.INDEX : Opcodes.RINDEX); - emitReg(rd); - emitReg(strReg); - emitReg(substrReg); - emitReg(posReg); - - lastResultReg = rd; - } finally { - currentCallContext = savedContext; - } - } else { - throwCompilerException(op + " requires a list of arguments"); - } - } else if (op.equals("stat") || op.equals("lstat")) { - // stat FILE or lstat FILE - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - try { - node.operand.accept(this); - int operandReg = lastResultReg; - - int rd = allocateRegister(); - emit(op.equals("stat") ? Opcodes.STAT : Opcodes.LSTAT); - emitReg(rd); - emitReg(operandReg); - emit(savedContext); // Pass calling context - - lastResultReg = rd; - } finally { - currentCallContext = savedContext; - } - } else if (op.startsWith("-") && op.length() == 2) { - // File test operators: -r, -w, -x, etc. - - // Check if operand is the special filehandle "_" - boolean isUnderscoreOperand = (node.operand instanceof IdentifierNode) - && ((IdentifierNode) node.operand).name.equals("_"); - - if (isUnderscoreOperand) { - // Special case: -r _ uses cached file handle - // Call FileTestOperator.fileTestLastHandle(String) - int rd = allocateRegister(); - int operatorStrIndex = addToStringPool(op); - - // Emit FILETEST_LASTHANDLE opcode - emit(Opcodes.FILETEST_LASTHANDLE); - emitReg(rd); - emit(operatorStrIndex); - - lastResultReg = rd; - } else { - // Normal case: evaluate operand and test it - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - try { - node.operand.accept(this); - int operandReg = lastResultReg; - - int rd = allocateRegister(); - - // Map operator to opcode - char testChar = op.charAt(1); - short opcode; - switch (testChar) { - case 'r': opcode = Opcodes.FILETEST_R; break; - case 'w': opcode = Opcodes.FILETEST_W; break; - case 'x': opcode = Opcodes.FILETEST_X; break; - case 'o': opcode = Opcodes.FILETEST_O; break; - case 'R': opcode = Opcodes.FILETEST_R_REAL; break; - case 'W': opcode = Opcodes.FILETEST_W_REAL; break; - case 'X': opcode = Opcodes.FILETEST_X_REAL; break; - case 'O': opcode = Opcodes.FILETEST_O_REAL; break; - case 'e': opcode = Opcodes.FILETEST_E; break; - case 'z': opcode = Opcodes.FILETEST_Z; break; - case 's': opcode = Opcodes.FILETEST_S; break; - case 'f': opcode = Opcodes.FILETEST_F; break; - case 'd': opcode = Opcodes.FILETEST_D; break; - case 'l': opcode = Opcodes.FILETEST_L; break; - case 'p': opcode = Opcodes.FILETEST_P; break; - case 'S': opcode = Opcodes.FILETEST_S_UPPER; break; - case 'b': opcode = Opcodes.FILETEST_B; break; - case 'c': opcode = Opcodes.FILETEST_C; break; - case 't': opcode = Opcodes.FILETEST_T; break; - case 'u': opcode = Opcodes.FILETEST_U; break; - case 'g': opcode = Opcodes.FILETEST_G; break; - case 'k': opcode = Opcodes.FILETEST_K; break; - case 'T': opcode = Opcodes.FILETEST_T_UPPER; break; - case 'B': opcode = Opcodes.FILETEST_B_UPPER; break; - case 'M': opcode = Opcodes.FILETEST_M; break; - case 'A': opcode = Opcodes.FILETEST_A; break; - case 'C': opcode = Opcodes.FILETEST_C_UPPER; break; - default: - throwCompilerException("Unsupported file test operator: " + op); - return; - } - - emit(opcode); - emitReg(rd); - emitReg(operandReg); - - lastResultReg = rd; - } finally { - currentCallContext = savedContext; - } - } - } else if (op.equals("die")) { - // die $message; - if (node.operand != null) { - // Evaluate die message - node.operand.accept(this); - int msgReg = lastResultReg; - - // Precompute location message at compile time (zero overhead!) - String locationMsg; - // Use annotation from AST node which has the correct line number - Object lineObj = node.getAnnotation("line"); - Object fileObj = node.getAnnotation("file"); - if (lineObj != null && fileObj != null) { - String fileName = fileObj.toString(); - int lineNumber = Integer.parseInt(lineObj.toString()); - locationMsg = " at " + fileName + " line " + lineNumber; - } else if (errorUtil != null) { - // Fallback to errorUtil if annotations not available - String fileName = errorUtil.getFileName(); - int lineNumber = errorUtil.getLineNumberAccurate(node.getIndex()); - locationMsg = " at " + fileName + " line " + lineNumber; - } else { - // Final fallback if neither available - locationMsg = " at " + sourceName + " line " + sourceLine; - } - - int locationReg = allocateRegister(); - emit(Opcodes.LOAD_STRING); - emitReg(locationReg); - emit(addToStringPool(locationMsg)); - - // Emit DIE with both message and precomputed location - emitWithToken(Opcodes.DIE, node.getIndex()); - emitReg(msgReg); - emitReg(locationReg); - } else { - // die; (no message - use $@) - // For now, emit with undef register - int undefReg = allocateRegister(); - emit(Opcodes.LOAD_UNDEF); - emitReg(undefReg); - - // Precompute location message for bare die - String locationMsg; - if (errorUtil != null) { - String fileName = errorUtil.getFileName(); - int lineNumber = errorUtil.getLineNumber(node.getIndex()); - locationMsg = " at " + fileName + " line " + lineNumber; - } else { - locationMsg = " at " + sourceName + " line " + sourceLine; - } - - int locationReg = allocateRegister(); - emit(Opcodes.LOAD_STRING); - emitReg(locationReg); - emitInt(addToStringPool(locationMsg)); - - emitWithToken(Opcodes.DIE, node.getIndex()); - emitReg(undefReg); - emitReg(locationReg); - } - lastResultReg = -1; // No result after die - } else if (op.equals("warn")) { - // warn $message; - if (node.operand != null) { - // Evaluate warn message - node.operand.accept(this); - int msgReg = lastResultReg; - - // Precompute location message at compile time - String locationMsg; - // Use annotation from AST node which has the correct line number - Object lineObj = node.getAnnotation("line"); - Object fileObj = node.getAnnotation("file"); - if (lineObj != null && fileObj != null) { - String fileName = fileObj.toString(); - int lineNumber = Integer.parseInt(lineObj.toString()); - locationMsg = " at " + fileName + " line " + lineNumber; - } else if (errorUtil != null) { - // Fallback to errorUtil if annotations not available - String fileName = errorUtil.getFileName(); - int lineNumber = errorUtil.getLineNumberAccurate(node.getIndex()); - locationMsg = " at " + fileName + " line " + lineNumber; - } else { - // Final fallback if neither available - locationMsg = " at " + sourceName + " line " + sourceLine; - } - - int locationReg = allocateRegister(); - emit(Opcodes.LOAD_STRING); - emitReg(locationReg); - emit(addToStringPool(locationMsg)); - - // Emit WARN with both message and precomputed location - emitWithToken(Opcodes.WARN, node.getIndex()); - emitReg(msgReg); - emitReg(locationReg); - } else { - // warn; (no message - use $@) - int undefReg = allocateRegister(); - emit(Opcodes.LOAD_UNDEF); - emitReg(undefReg); - - // Precompute location message for bare warn - String locationMsg; - if (errorUtil != null) { - String fileName = errorUtil.getFileName(); - int lineNumber = errorUtil.getLineNumber(node.getIndex()); - locationMsg = " at " + fileName + " line " + lineNumber; - } else { - locationMsg = " at " + sourceName + " line " + sourceLine; - } - - int locationReg = allocateRegister(); - emit(Opcodes.LOAD_STRING); - emitReg(locationReg); - emitInt(addToStringPool(locationMsg)); - - emitWithToken(Opcodes.WARN, node.getIndex()); - emitReg(undefReg); - emitReg(locationReg); - } - // warn returns 1 (true) in Perl - int resultReg = allocateRegister(); - emit(Opcodes.LOAD_INT); - emitReg(resultReg); - emitInt(1); - lastResultReg = resultReg; - } else if (op.equals("eval")) { - // eval $string; - if (node.operand != null) { - // Evaluate eval operand (the code string) - node.operand.accept(this); - int stringReg = lastResultReg; - - // Allocate register for result - int rd = allocateRegister(); - - // Emit direct opcode EVAL_STRING - emitWithToken(Opcodes.EVAL_STRING, node.getIndex()); - emitReg(rd); - emitReg(stringReg); - - lastResultReg = rd; - } else { - // eval; (no operand - return undef) - int undefReg = allocateRegister(); - emit(Opcodes.LOAD_UNDEF); - emitReg(undefReg); - lastResultReg = undefReg; - } - } else if (op.equals("select")) { - // select FILEHANDLE or select() - // SELECT is a fast opcode (used in every print statement) - // Format: [SELECT] [rd] [rs_list] - // Effect: rd = IOOperator.select(registers[rs_list], SCALAR) - - int rd = allocateRegister(); - - if (node.operand != null && node.operand instanceof ListNode) { - // select FILEHANDLE or select() with arguments - // Compile the operand (ListNode containing filehandle ref) - node.operand.accept(this); - int listReg = lastResultReg; - - // Emit SELECT opcode - emitWithToken(Opcodes.SELECT, node.getIndex()); - emitReg(rd); - emitReg(listReg); - } else { - // select() with no arguments - returns current filehandle - // Create empty list - emit(Opcodes.CREATE_LIST); - int listReg = allocateRegister(); - emitReg(listReg); - emit(0); // count = 0 - - // Emit SELECT opcode - emitWithToken(Opcodes.SELECT, node.getIndex()); - emitReg(rd); - emitReg(listReg); - } - - lastResultReg = rd; - } else if (op.equals("undef")) { - // undef operator - returns undefined value - // Can be used standalone: undef - // Or with an operand to undef a variable: undef $x (not implemented yet) - int undefReg = allocateRegister(); - emit(Opcodes.LOAD_UNDEF); - emitReg(undefReg); - lastResultReg = undefReg; - } else if (op.equals("unaryMinus")) { - // Unary minus: -$x - // Compile operand - node.operand.accept(this); - int operandReg = lastResultReg; - - // Allocate result register - int rd = allocateRegister(); - - // Emit NEG_SCALAR - emit(Opcodes.NEG_SCALAR); - emitReg(rd); - emitReg(operandReg); - - lastResultReg = rd; - } else if (op.equals("pop")) { - // Array pop: $x = pop @array or $x = pop @$ref - // operand: ListNode containing OperatorNode("@", IdentifierNode or OperatorNode) - if (node.operand == null || !(node.operand instanceof ListNode)) { - throwCompilerException("pop requires array argument"); - } - - ListNode list = (ListNode) node.operand; - if (list.elements.isEmpty() || !(list.elements.get(0) instanceof OperatorNode)) { - throwCompilerException("pop requires array variable"); - } - - OperatorNode arrayOp = (OperatorNode) list.elements.get(0); - if (!arrayOp.operator.equals("@")) { - throwCompilerException("pop requires array variable: pop @array"); - } - - int arrayReg = -1; // Will be assigned in if/else blocks - - if (arrayOp.operand instanceof IdentifierNode) { - // pop @array - String varName = "@" + ((IdentifierNode) arrayOp.operand).name; - - // Get the array - check lexical first, then global - if (hasVariable(varName)) { - // Lexical array - arrayReg = getVariableRegister(varName); - } else { - // Global array - load it - arrayReg = allocateRegister(); - String globalArrayName = NameNormalizer.normalizeVariableName(((IdentifierNode) arrayOp.operand).name, getCurrentPackage()); - int nameIdx = addToStringPool(globalArrayName); - emit(Opcodes.LOAD_GLOBAL_ARRAY); - emitReg(arrayReg); - emit(nameIdx); - } - } else if (arrayOp.operand instanceof OperatorNode) { - // pop @$ref - dereference first - arrayOp.operand.accept(this); - int refReg = lastResultReg; - - // Dereference to get the array - arrayReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(arrayReg); - emitReg(refReg); - } else { - throwCompilerException("pop requires array variable or dereferenced array: pop @array or pop @$ref"); - } - - // Allocate result register - int rd = allocateRegister(); - - // Emit ARRAY_POP - emit(Opcodes.ARRAY_POP); - emitReg(rd); - emitReg(arrayReg); - - lastResultReg = rd; - } else if (op.equals("shift")) { - // Array shift: $x = shift @array or $x = shift @$ref - // operand: ListNode containing OperatorNode("@", IdentifierNode or OperatorNode) - if (node.operand == null || !(node.operand instanceof ListNode)) { - throwCompilerException("shift requires array argument"); - } - - ListNode list = (ListNode) node.operand; - if (list.elements.isEmpty() || !(list.elements.get(0) instanceof OperatorNode)) { - throwCompilerException("shift requires array variable"); - } - - OperatorNode arrayOp = (OperatorNode) list.elements.get(0); - if (!arrayOp.operator.equals("@")) { - throwCompilerException("shift requires array variable: shift @array"); - } - - int arrayReg = -1; // Will be assigned in if/else blocks - - if (arrayOp.operand instanceof IdentifierNode) { - // shift @array - String varName = "@" + ((IdentifierNode) arrayOp.operand).name; - - // Get the array - check lexical first, then global - if (hasVariable(varName)) { - // Lexical array - arrayReg = getVariableRegister(varName); - } else { - // Global array - load it - arrayReg = allocateRegister(); - String globalArrayName = NameNormalizer.normalizeVariableName(((IdentifierNode) arrayOp.operand).name, getCurrentPackage()); - int nameIdx = addToStringPool(globalArrayName); - emit(Opcodes.LOAD_GLOBAL_ARRAY); - emitReg(arrayReg); - emit(nameIdx); - } - } else if (arrayOp.operand instanceof OperatorNode) { - // shift @$ref - dereference first - arrayOp.operand.accept(this); - int refReg = lastResultReg; - - // Dereference to get the array - arrayReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(arrayReg); - emitReg(refReg); - } else { - throwCompilerException("shift requires array variable or dereferenced array: shift @array or shift @$ref"); - } - - // Allocate result register - int rd = allocateRegister(); - - // Emit ARRAY_SHIFT - emit(Opcodes.ARRAY_SHIFT); - emitReg(rd); - emitReg(arrayReg); - - lastResultReg = rd; - } else if (op.equals("splice")) { - // Array splice: splice @array, offset, length, @list - // operand: ListNode containing [@array, offset, length, replacement_list] - if (node.operand == null || !(node.operand instanceof ListNode)) { - throwCompilerException("splice requires array and arguments"); - } - - ListNode list = (ListNode) node.operand; - if (list.elements.isEmpty() || !(list.elements.get(0) instanceof OperatorNode)) { - throwCompilerException("splice requires array variable"); - } - - // First element is the array - OperatorNode arrayOp = (OperatorNode) list.elements.get(0); - if (!arrayOp.operator.equals("@")) { - throwCompilerException("splice requires array variable: splice @array, ..."); - } - - int arrayReg = -1; // Will be assigned in if/else blocks - - if (arrayOp.operand instanceof IdentifierNode) { - // splice @array - String varName = "@" + ((IdentifierNode) arrayOp.operand).name; - - // Get the array - check lexical first, then global - if (hasVariable(varName)) { - // Lexical array - arrayReg = getVariableRegister(varName); - } else { - // Global array - load it - arrayReg = allocateRegister(); - String globalArrayName = NameNormalizer.normalizeVariableName( - ((IdentifierNode) arrayOp.operand).name, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalArrayName); - emit(Opcodes.LOAD_GLOBAL_ARRAY); - emitReg(arrayReg); - emit(nameIdx); - } - } else if (arrayOp.operand instanceof OperatorNode) { - // splice @$ref - dereference first - arrayOp.operand.accept(this); - int refReg = lastResultReg; - - // Dereference to get the array - arrayReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(arrayReg); - emitReg(refReg); - } else { - throwCompilerException("splice requires array variable or dereferenced array: splice @array or splice @$ref"); - } - - // Create a list with the remaining arguments (offset, length, replacement values) - // Compile each remaining argument and collect them into a RuntimeList - List argRegs = new ArrayList<>(); - for (int i = 1; i < list.elements.size(); i++) { - list.elements.get(i).accept(this); - argRegs.add(lastResultReg); - } - - // Create a RuntimeList from these registers - int argsListReg = allocateRegister(); - emit(Opcodes.CREATE_LIST); - emitReg(argsListReg); - emit(argRegs.size()); - for (int argReg : argRegs) { - emitReg(argReg); - } - - // Allocate result register - int rd = allocateRegister(); - - // Emit direct opcode SPLICE - emit(Opcodes.SPLICE); - emitReg(rd); - emitReg(arrayReg); - emitReg(argsListReg); - emit(currentCallContext); // Pass context for scalar/list conversion - - lastResultReg = rd; - } else if (op.equals("reverse")) { - // Array/string reverse: reverse @array or reverse $string - // operand: ListNode containing arguments - if (node.operand == null || !(node.operand instanceof ListNode)) { - throwCompilerException("reverse requires arguments"); - } - - ListNode list = (ListNode) node.operand; - - // Compile all arguments into registers - List argRegs = new ArrayList<>(); - for (Node arg : list.elements) { - arg.accept(this); - argRegs.add(lastResultReg); - } - - // Create a RuntimeList from these registers - int argsListReg = allocateRegister(); - emit(Opcodes.CREATE_LIST); - emitReg(argsListReg); - emit(argRegs.size()); - for (int argReg : argRegs) { - emitReg(argReg); - } - - // Allocate result register - int rd = allocateRegister(); - - // Emit direct opcode REVERSE - emit(Opcodes.REVERSE); - emitReg(rd); - emitReg(argsListReg); - emit(RuntimeContextType.LIST); // Context - - lastResultReg = rd; - } else if (op.equals("exists")) { - // exists $hash{key} or exists $array[index] - // operand: ListNode containing the hash/array access - if (node.operand == null || !(node.operand instanceof ListNode)) { - throwCompilerException("exists requires an argument"); - } - - ListNode list = (ListNode) node.operand; - if (list.elements.isEmpty()) { - throwCompilerException("exists requires an argument"); - } - - Node arg = list.elements.get(0); - - // Handle hash access: $hash{key} - if (arg instanceof BinaryOperatorNode && ((BinaryOperatorNode) arg).operator.equals("{")) { - BinaryOperatorNode hashAccess = (BinaryOperatorNode) arg; - - // Get hash register (need to handle $hash{key} -> %hash) - int hashReg; - if (hashAccess.left instanceof OperatorNode) { - OperatorNode leftOp = (OperatorNode) hashAccess.left; - if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode) { - // Simple: exists $hash{key} -> get %hash - String varName = ((IdentifierNode) leftOp.operand).name; - String hashVarName = "%" + varName; - - if (hasVariable(hashVarName)) { - // Lexical hash - hashReg = getVariableRegister(hashVarName); - } else { - // Global hash - load it - hashReg = allocateRegister(); - String globalHashName = NameNormalizer.normalizeVariableName( - varName, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalHashName); - emit(Opcodes.LOAD_GLOBAL_HASH); - emitReg(hashReg); - emit(nameIdx); - } - } else { - // Complex: dereference needed - leftOp.operand.accept(this); - int scalarReg = lastResultReg; - - hashReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); - emitReg(hashReg); - emitReg(scalarReg); - } - } else if (hashAccess.left instanceof BinaryOperatorNode) { - // Nested: exists $hash{outer}{inner} - hashAccess.left.accept(this); - int scalarReg = lastResultReg; - - hashReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); - emitReg(hashReg); - emitReg(scalarReg); - } else { - throwCompilerException("Hash access requires variable or expression on left side"); - return; - } - - // Compile key (right side contains HashLiteralNode) - int keyReg; - if (hashAccess.right instanceof HashLiteralNode) { - HashLiteralNode keyNode = (HashLiteralNode) hashAccess.right; - if (!keyNode.elements.isEmpty()) { - Node keyElement = keyNode.elements.get(0); - if (keyElement instanceof IdentifierNode) { - // Bareword key - autoquote - String keyString = ((IdentifierNode) keyElement).name; - keyReg = allocateRegister(); - int keyIdx = addToStringPool(keyString); - emit(Opcodes.LOAD_STRING); - emitReg(keyReg); - emit(keyIdx); - } else { - // Expression key - keyElement.accept(this); - keyReg = lastResultReg; - } - } else { - throwCompilerException("Hash key required for exists"); - return; - } - } else { - hashAccess.right.accept(this); - keyReg = lastResultReg; - } - - // Emit HASH_EXISTS - int rd = allocateRegister(); - emit(Opcodes.HASH_EXISTS); - emitReg(rd); - emitReg(hashReg); - emitReg(keyReg); - - lastResultReg = rd; - } else { - // For now, use SLOW_OP for other cases (array exists, etc.) - arg.accept(this); - int argReg = lastResultReg; - - int rd = allocateRegister(); - emit(Opcodes.EXISTS); - emitReg(rd); - emitReg(argReg); - - lastResultReg = rd; - } - } else if (op.equals("delete")) { - // delete $hash{key} or delete @hash{@keys} - // operand: ListNode containing the hash/array access - if (node.operand == null || !(node.operand instanceof ListNode)) { - throwCompilerException("delete requires an argument"); - } - - ListNode list = (ListNode) node.operand; - if (list.elements.isEmpty()) { - throwCompilerException("delete requires an argument"); - } - - Node arg = list.elements.get(0); - - // Handle hash access: $hash{key} or hash slice delete: delete @hash{keys} - if (arg instanceof BinaryOperatorNode && ((BinaryOperatorNode) arg).operator.equals("{")) { - BinaryOperatorNode hashAccess = (BinaryOperatorNode) arg; - - // Check if it's a hash slice delete: delete @hash{keys} - if (hashAccess.left instanceof OperatorNode) { - OperatorNode leftOp = (OperatorNode) hashAccess.left; - if (leftOp.operator.equals("@")) { - // Hash slice delete: delete @hash{'key1', 'key2'} - // Use SLOW_OP for slice delete - int hashReg; - - if (leftOp.operand instanceof IdentifierNode) { - String varName = ((IdentifierNode) leftOp.operand).name; - String hashVarName = "%" + varName; - - if (hasVariable(hashVarName)) { - hashReg = getVariableRegister(hashVarName); - } else { - hashReg = allocateRegister(); - String globalHashName = NameNormalizer.normalizeVariableName( - varName, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalHashName); - emit(Opcodes.LOAD_GLOBAL_HASH); - emitReg(hashReg); - emit(nameIdx); - } - } else { - throwCompilerException("Hash slice delete requires identifier"); - return; - } - - // Get keys from HashLiteralNode - if (!(hashAccess.right instanceof HashLiteralNode)) { - throwCompilerException("Hash slice delete requires HashLiteralNode"); - return; - } - HashLiteralNode keysNode = (HashLiteralNode) hashAccess.right; - - // Compile all keys - List keyRegs = new ArrayList<>(); - for (Node keyElement : keysNode.elements) { - if (keyElement instanceof IdentifierNode) { - String keyString = ((IdentifierNode) keyElement).name; - int keyReg = allocateRegister(); - int keyIdx = addToStringPool(keyString); - emit(Opcodes.LOAD_STRING); - emitReg(keyReg); - emit(keyIdx); - keyRegs.add(keyReg); - } else { - keyElement.accept(this); - keyRegs.add(lastResultReg); - } - } - - // Create RuntimeList from keys - int keysListReg = allocateRegister(); - emit(Opcodes.CREATE_LIST); - emitReg(keysListReg); - emit(keyRegs.size()); - for (int keyReg : keyRegs) { - emitReg(keyReg); - } - - // Use SLOW_OP for hash slice delete - int rd = allocateRegister(); - emit(Opcodes.HASH_SLICE_DELETE); - emitReg(rd); - emitReg(hashReg); - emitReg(keysListReg); - - lastResultReg = rd; - return; - } - } - - // Single key delete: delete $hash{key} - // Get hash register (need to handle $hash{key} -> %hash) - int hashReg; - if (hashAccess.left instanceof OperatorNode) { - OperatorNode leftOp = (OperatorNode) hashAccess.left; - if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode) { - // Simple: delete $hash{key} -> get %hash - String varName = ((IdentifierNode) leftOp.operand).name; - String hashVarName = "%" + varName; - - if (hasVariable(hashVarName)) { - // Lexical hash - hashReg = getVariableRegister(hashVarName); - } else { - // Global hash - load it - hashReg = allocateRegister(); - String globalHashName = NameNormalizer.normalizeVariableName( - varName, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalHashName); - emit(Opcodes.LOAD_GLOBAL_HASH); - emitReg(hashReg); - emit(nameIdx); - } - } else { - // Complex: dereference needed - leftOp.operand.accept(this); - int scalarReg = lastResultReg; - - hashReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); - emitReg(hashReg); - emitReg(scalarReg); - } - } else if (hashAccess.left instanceof BinaryOperatorNode) { - // Nested: delete $hash{outer}{inner} - hashAccess.left.accept(this); - int scalarReg = lastResultReg; - - hashReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); - emitReg(hashReg); - emitReg(scalarReg); - } else { - throwCompilerException("Hash access requires variable or expression on left side"); - return; - } - - // Compile key (right side contains HashLiteralNode) - int keyReg; - if (hashAccess.right instanceof HashLiteralNode) { - HashLiteralNode keyNode = (HashLiteralNode) hashAccess.right; - if (!keyNode.elements.isEmpty()) { - Node keyElement = keyNode.elements.get(0); - if (keyElement instanceof IdentifierNode) { - // Bareword key - autoquote - String keyString = ((IdentifierNode) keyElement).name; - keyReg = allocateRegister(); - int keyIdx = addToStringPool(keyString); - emit(Opcodes.LOAD_STRING); - emitReg(keyReg); - emit(keyIdx); - } else { - // Expression key - keyElement.accept(this); - keyReg = lastResultReg; - } - } else { - throwCompilerException("Hash key required for delete"); - return; - } - } else { - hashAccess.right.accept(this); - keyReg = lastResultReg; - } - - // Emit HASH_DELETE - int rd = allocateRegister(); - emit(Opcodes.HASH_DELETE); - emitReg(rd); - emitReg(hashReg); - emitReg(keyReg); - - lastResultReg = rd; - } else { - // For now, use SLOW_OP for other cases (hash slice delete, array delete, etc.) - arg.accept(this); - int argReg = lastResultReg; - - int rd = allocateRegister(); - emit(Opcodes.DELETE); - emitReg(rd); - emitReg(argReg); - - lastResultReg = rd; - } - } else if (op.equals("keys")) { - // keys %hash - // operand: hash variable (OperatorNode("%" ...) or other expression) - if (node.operand == null) { - throwCompilerException("keys requires a hash argument"); - } - - // Compile the hash operand - node.operand.accept(this); - int hashReg = lastResultReg; - - // Emit HASH_KEYS - int rd = allocateRegister(); - emit(Opcodes.HASH_KEYS); - emitReg(rd); - emitReg(hashReg); - - lastResultReg = rd; - } else if (op.equals("values")) { - // values %hash - // operand: hash variable (OperatorNode("%" ...) or other expression) - if (node.operand == null) { - throwCompilerException("values requires a hash argument"); - } - - // Compile the hash operand - node.operand.accept(this); - int hashReg = lastResultReg; - - // Emit HASH_VALUES - int rd = allocateRegister(); - emit(Opcodes.HASH_VALUES); - emitReg(rd); - emitReg(hashReg); - - lastResultReg = rd; - } else if (op.equals("$#")) { - // $#array - get last index of array (size - 1) - // operand: array variable (OperatorNode("@" ...) or IdentifierNode) - if (node.operand == null) { - throwCompilerException("$# requires an array argument"); - } - - int arrayReg = -1; - - // Handle different operand types - if (node.operand instanceof OperatorNode) { - OperatorNode operandOp = (OperatorNode) node.operand; - - if (operandOp.operator.equals("@") && operandOp.operand instanceof IdentifierNode) { - // $#@array or $#array (both work) - String varName = "@" + ((IdentifierNode) operandOp.operand).name; - - if (hasVariable(varName)) { - arrayReg = getVariableRegister(varName); - } else { - arrayReg = allocateRegister(); - String globalArrayName = NameNormalizer.normalizeVariableName( - ((IdentifierNode) operandOp.operand).name, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalArrayName); - emit(Opcodes.LOAD_GLOBAL_ARRAY); - emitReg(arrayReg); - emit(nameIdx); - } - } else if (operandOp.operator.equals("$")) { - // $#$ref - dereference first - operandOp.accept(this); - int refReg = lastResultReg; - - arrayReg = allocateRegister(); - emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); - emitReg(arrayReg); - emitReg(refReg); - } else { - throwCompilerException("$# requires array variable or dereferenced array"); - } - } else if (node.operand instanceof IdentifierNode) { - // $#array (without @) - String varName = "@" + ((IdentifierNode) node.operand).name; - - if (hasVariable(varName)) { - arrayReg = getVariableRegister(varName); - } else { - arrayReg = allocateRegister(); - String globalArrayName = NameNormalizer.normalizeVariableName( - ((IdentifierNode) node.operand).name, - getCurrentPackage() - ); - int nameIdx = addToStringPool(globalArrayName); - emit(Opcodes.LOAD_GLOBAL_ARRAY); - emitReg(arrayReg); - emit(nameIdx); - } - } else { - throwCompilerException("$# requires array variable"); - } - - // Get array size - int sizeReg = allocateRegister(); - emit(Opcodes.ARRAY_SIZE); - emitReg(sizeReg); - emitReg(arrayReg); - - // Subtract 1 to get last index - int oneReg = allocateRegister(); - emit(Opcodes.LOAD_INT); - emitReg(oneReg); - emitInt(1); - - int rd = allocateRegister(); - emit(Opcodes.SUB_SCALAR); - emitReg(rd); - emitReg(sizeReg); - emitReg(oneReg); - - lastResultReg = rd; - } else if (op.equals("length")) { - // length($string) - get string length - // operand: ListNode containing the string argument - if (node.operand == null) { - throwCompilerException("length requires an argument"); - } - - // Compile the operand - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (list.elements.isEmpty()) { - throwCompilerException("length requires an argument"); - } - // Get first element - list.elements.get(0).accept(this); - } else { - node.operand.accept(this); - } - int stringReg = lastResultReg; - - // Call length builtin using SLOW_OP - int rd = allocateRegister(); - emit(Opcodes.LENGTH_OP); - emitReg(rd); - emitReg(stringReg); - - lastResultReg = rd; - } else if (op.equals("open")) { - // open(filehandle, mode, filename) or open(filehandle, expr) - if (node.operand == null || !(node.operand instanceof ListNode)) { - throwCompilerException("open requires arguments"); - } - - ListNode argsList = (ListNode) node.operand; - if (argsList.elements.isEmpty()) { - throwCompilerException("open requires arguments"); - } - - // Compile all arguments into a list - int argsReg = allocateRegister(); - emit(Opcodes.NEW_ARRAY); - emitReg(argsReg); - - for (Node arg : argsList.elements) { - arg.accept(this); - int elemReg = lastResultReg; - - emit(Opcodes.ARRAY_PUSH); - emitReg(argsReg); - emitReg(elemReg); - } - - // Call open with context and args - int rd = allocateRegister(); - emit(Opcodes.OPEN); - emitReg(rd); - emit(currentCallContext); - emitReg(argsReg); - - lastResultReg = rd; - } else if (op.equals("matchRegex")) { - // m/pattern/flags - create a regex and return it (for use with =~) - // operand: ListNode containing pattern string and flags string - if (node.operand == null || !(node.operand instanceof ListNode)) { - throwCompilerException("matchRegex requires pattern and flags"); - } - - ListNode args = (ListNode) node.operand; - if (args.elements.size() < 2) { - throwCompilerException("matchRegex requires pattern and flags"); - } - - // Compile pattern - args.elements.get(0).accept(this); - int patternReg = lastResultReg; - - // Compile flags - args.elements.get(1).accept(this); - int flagsReg = lastResultReg; - - // Create quoted regex using QUOTE_REGEX opcode - int rd = allocateRegister(); - emit(Opcodes.QUOTE_REGEX); - emitReg(rd); - emitReg(patternReg); - emitReg(flagsReg); - - lastResultReg = rd; - } else if (op.equals("chomp")) { - // chomp($x) or chomp - remove trailing newlines - if (node.operand == null) { - // chomp with no args - operates on $_ - String varName = "$_"; - int targetReg; - if (hasVariable(varName)) { - targetReg = getVariableRegister(varName); - } else { - targetReg = allocateRegister(); - int nameIdx = addToStringPool("main::_"); - emit(Opcodes.LOAD_GLOBAL_SCALAR); - emitReg(targetReg); - emit(nameIdx); - } - - int rd = allocateRegister(); - emit(Opcodes.CHOMP); - emitReg(rd); - emitReg(targetReg); - - lastResultReg = rd; - } else { - // chomp with argument - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("chomp requires an argument"); - } - } else { - node.operand.accept(this); - } - int targetReg = lastResultReg; - - int rd = allocateRegister(); - emit(Opcodes.CHOMP); - emitReg(rd); - emitReg(targetReg); - - lastResultReg = rd; - } - } else if (op.equals("+")) { - // Unary + operator: forces numeric context on its operand - // For arrays/hashes in scalar context, this returns the size - // For scalars, this ensures the value is numeric - if (node.operand != null) { - // Evaluate operand in scalar context - int savedContext = currentCallContext; - currentCallContext = RuntimeContextType.SCALAR; - try { - node.operand.accept(this); - int operandReg = lastResultReg; - - // Emit ARRAY_SIZE to convert to scalar - // This handles arrays/hashes (converts to size) and passes through scalars - int rd = allocateRegister(); - emit(Opcodes.ARRAY_SIZE); - emitReg(rd); - emitReg(operandReg); - - lastResultReg = rd; - } finally { - currentCallContext = savedContext; - } - } else { - throwCompilerException("unary + operator requires an operand"); - } - } else if (op.equals("wantarray")) { - // wantarray operator: returns undef in VOID, false in SCALAR, true in LIST - // Read register 2 (wantarray context) and convert to Perl convention - int rd = allocateRegister(); - emit(Opcodes.WANTARRAY); - emitReg(rd); - emitReg(2); // Register 2 contains the calling context - - lastResultReg = rd; - // GENERATED_OPERATORS_START - } else if (op.equals("int")) { - // int($x) - MathOperators.integer - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("int requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.INT); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("log")) { - // log($x) - MathOperators.log - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("log requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.LOG); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("sqrt")) { - // sqrt($x) - MathOperators.sqrt - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("sqrt requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.SQRT); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("cos")) { - // cos($x) - MathOperators.cos - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("cos requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.COS); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("sin")) { - // sin($x) - MathOperators.sin - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("sin requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.SIN); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("exp")) { - // exp($x) - MathOperators.exp - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("exp requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.EXP); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("abs")) { - // abs($x) - MathOperators.abs - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("abs requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.ABS); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("binary~")) { - // binary~($x) - BitwiseOperators.bitwiseNotBinary - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("binary~ requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.BINARY_NOT); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("integerBitwiseNot")) { - // integerBitwiseNot($x) - BitwiseOperators.integerBitwiseNot - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("integerBitwiseNot requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.INTEGER_BITWISE_NOT); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("ord")) { - // ord($x) - ScalarOperators.ord - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("ord requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.ORD); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("ordBytes")) { - // ordBytes($x) - ScalarOperators.ordBytes - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("ordBytes requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.ORD_BYTES); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("oct")) { - // oct($x) - ScalarOperators.oct - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("oct requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.OCT); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("hex")) { - // hex($x) - ScalarOperators.hex - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("hex requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.HEX); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("srand")) { - // srand($x) - Random.srand - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("srand requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.SRAND); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("chr")) { - // chr($x) - StringOperators.chr - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("chr requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.CHR); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("chrBytes")) { - // chrBytes($x) - StringOperators.chrBytes - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("chrBytes requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.CHR_BYTES); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("lengthBytes")) { - // lengthBytes($x) - StringOperators.lengthBytes - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("lengthBytes requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.LENGTH_BYTES); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("quotemeta")) { - // quotemeta($x) - StringOperators.quotemeta - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("quotemeta requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.QUOTEMETA); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("fc")) { - // fc($x) - StringOperators.fc - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("fc requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.FC); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("lc")) { - // lc($x) - StringOperators.lc - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("lc requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.LC); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("lcfirst")) { - // lcfirst($x) - StringOperators.lcfirst - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("lcfirst requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.LCFIRST); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("uc")) { - // uc($x) - StringOperators.uc - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("uc requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.UC); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("ucfirst")) { - // ucfirst($x) - StringOperators.ucfirst - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("ucfirst requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.UCFIRST); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("sleep")) { - // sleep($x) - Time.sleep - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("sleep requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.SLEEP); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("tell")) { - // tell($x) - IOOperator.tell - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("tell requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.TELL); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("rmdir")) { - // rmdir($x) - Directory.rmdir - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("rmdir requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.RMDIR); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("closedir")) { - // closedir($x) - Directory.closedir - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("closedir requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.CLOSEDIR); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("rewinddir")) { - // rewinddir($x) - Directory.rewinddir - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("rewinddir requires an argument"); - } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.REWINDDIR); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("telldir")) { - // telldir($x) - Directory.telldir - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); + emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); + emitReg(arrayReg); + emit(nameIdx); + emit(currentSubroutineBeginId); + } else if (hasVariable(varName)) { + // Lexical array - use existing register + arrayReg = getVariableRegister(varName); } else { - throwCompilerException("telldir requires an argument"); + // Global array - load it + arrayReg = allocateRegister(); + String globalArrayName = NameNormalizer.normalizeVariableName(((IdentifierNode) node.operand).name, getCurrentPackage()); + int nameIdx = addToStringPool(globalArrayName); + + emit(Opcodes.LOAD_GLOBAL_ARRAY); + emitReg(arrayReg); + emit(nameIdx); } - } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.TELLDIR); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("chdir")) { - // chdir($x) - Directory.chdir - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); + + // Check if we're in scalar context - if so, return array size + if (currentCallContext == RuntimeContextType.SCALAR) { + int rd = allocateRegister(); + emit(Opcodes.ARRAY_SIZE); + emitReg(rd); + emitReg(arrayReg); + lastResultReg = rd; } else { - throwCompilerException("chdir requires an argument"); + lastResultReg = arrayReg; } + } else if (node.operand instanceof OperatorNode) { + // Dereference: @$arrayref or @{$hashref} + OperatorNode operandOp = (OperatorNode) node.operand; + + // Compile the reference + operandOp.accept(this); + int refReg = lastResultReg; + + // Dereference to get the array + // The reference should contain a RuntimeArray + // For @$scalar, we need to dereference it + int rd = allocateRegister(); + emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + emitReg(rd); + emitReg(refReg); + + lastResultReg = rd; + // Note: We don't check scalar context here because dereferencing + // should return the array itself. The slice or other operation + // will handle scalar context conversion if needed. + } else if (node.operand instanceof BlockNode) { + // @{ block } - evaluate block and dereference the result + // The block should return an arrayref + BlockNode blockNode = (BlockNode) node.operand; + blockNode.accept(this); + int refReg = lastResultReg; + + // Dereference to get the array + int rd = allocateRegister(); + emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + emitReg(rd); + emitReg(refReg); + + lastResultReg = rd; } else { - node.operand.accept(this); + throwCompilerException("Unsupported @ operand: " + node.operand.getClass().getSimpleName()); } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.CHDIR); - emitReg(rd); - emitReg(argReg); - lastResultReg = rd; - } else if (op.equals("exit")) { - // exit($x) - WarnDie.exit - if (node.operand instanceof ListNode) { - ListNode list = (ListNode) node.operand; - if (!list.elements.isEmpty()) { - list.elements.get(0).accept(this); - } else { - throwCompilerException("exit requires an argument"); + } else if (op.equals("%")) { + // Hash variable dereference: %x + if (node.operand instanceof IdentifierNode) { + String varName = "%" + ((IdentifierNode) node.operand).name; + + // Check if it's a lexical hash + if (hasVariable(varName)) { + // Lexical hash - use existing register + lastResultReg = getVariableRegister(varName); + return; } + + // Global hash - load it + int rd = allocateRegister(); + String globalHashName = NameNormalizer.normalizeVariableName(((IdentifierNode) node.operand).name, getCurrentPackage()); + int nameIdx = addToStringPool(globalHashName); + + emit(Opcodes.LOAD_GLOBAL_HASH); + emitReg(rd); + emit(nameIdx); + + lastResultReg = rd; } else { - node.operand.accept(this); - } - int argReg = lastResultReg; - int rd = allocateRegister(); - emit(Opcodes.EXIT); - emitReg(rd); - 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"); + throwCompilerException("Unsupported % operand: " + node.operand.getClass().getSimpleName()); } + } else if (op.equals("*")) { + // Glob variable dereference: *x + if (node.operand instanceof IdentifierNode) { + IdentifierNode idNode = (IdentifierNode) node.operand; + String varName = idNode.name; + + // Add package prefix if not present + if (!varName.contains("::")) { + varName = "main::" + varName; + } + + // Allocate register for glob + int rd = allocateRegister(); + int nameIdx = addToStringPool(varName); + + // Emit direct opcode LOAD_GLOB + emitWithToken(Opcodes.LOAD_GLOB, node.getIndex()); + emitReg(rd); + emit(nameIdx); - ListNode list = (ListNode) node.operand; - if (list.elements.size() < 3) { - throwCompilerException("tr operator requires search, replace, and modifiers"); + lastResultReg = rd; + } else { + throwCompilerException("Unsupported * operand: " + node.operand.getClass().getSimpleName()); } + } else if (op.equals("&")) { + // Code reference: &subname + // Gets a reference to a named subroutine + if (node.operand instanceof IdentifierNode) { + IdentifierNode idNode = (IdentifierNode) node.operand; + String subName = idNode.name; - // Compile search pattern - list.elements.get(0).accept(this); - int searchReg = lastResultReg; + // Use NameNormalizer to properly handle package prefixes + // This will add the current package if no package is specified + subName = NameNormalizer.normalizeVariableName(subName, getCurrentPackage()); - // Compile replace pattern - list.elements.get(1).accept(this); - int replaceReg = lastResultReg; + // Allocate register for code reference + int rd = allocateRegister(); + int nameIdx = addToStringPool(subName); - // Compile modifiers - list.elements.get(2).accept(this); - int modifiersReg = lastResultReg; + // Emit LOAD_GLOBAL_CODE + emit(Opcodes.LOAD_GLOBAL_CODE); + emitReg(rd); + emit(nameIdx); - // 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; + lastResultReg = rd; } 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); + throwCompilerException("Unsupported & operand: " + node.operand.getClass().getSimpleName()); } + } else if (op.equals("\\")) { + // Reference operator: \$x, \@x, \%x, \*x, etc. + if (node.operand != null) { + // Compile operand in LIST context to get the actual value + // Example: \@array should get a reference to the array itself, + // not its size (which would happen in SCALAR context) + int savedContext = currentCallContext; + currentCallContext = RuntimeContextType.LIST; + try { + node.operand.accept(this); + int valueReg = lastResultReg; - // Emit TR_TRANSLITERATE operation - int rd = allocateRegister(); - emit(Opcodes.TR_TRANSLITERATE); - emitReg(rd); - emitReg(searchReg); - emitReg(replaceReg); - emitReg(modifiersReg); - emitReg(targetReg); - emitInt(currentCallContext); + // Allocate register for reference + int rd = allocateRegister(); - lastResultReg = rd; - } else { - throwCompilerException("Unsupported operator: " + op); + // Emit CREATE_REF + emit(Opcodes.CREATE_REF); + emitReg(rd); + emitReg(valueReg); + + lastResultReg = rd; + } finally { + currentCallContext = savedContext; + } + } else { + throwCompilerException("Reference operator requires operand"); + } } } + @Override + public void visit(OperatorNode node) { + CompileOperator.visitOperator(this, node); + } + // ========================================================================= // HELPER METHODS // ========================================================================= - private int allocateRegister() { + int allocateRegister() { int reg = nextRegister++; if (reg > 65535) { throwCompilerException("Too many registers: exceeded 65535 register limit. " + @@ -7501,7 +2776,7 @@ private void recycleTemporaryRegisters() { // DO NOT reset lastResultReg - BlockNode needs it to return the last statement's result } - private int addToStringPool(String str) { + int addToStringPool(String str) { // Use HashMap for O(1) lookup instead of O(n) ArrayList.indexOf() Integer cached = stringPoolIndex.get(str); if (cached != null) { @@ -7525,7 +2800,7 @@ private int addToConstantPool(Object obj) { return index; } - private void emit(short opcode) { + void emit(short opcode) { bytecode.add(opcode); } @@ -7533,17 +2808,17 @@ private void emit(short opcode) { * Emit opcode and track tokenIndex for error reporting. * Use this for opcodes that may throw exceptions (DIE, method calls, etc.) */ - private void emitWithToken(short opcode, int tokenIndex) { + void emitWithToken(short opcode, int tokenIndex) { int pc = bytecode.size(); pcToTokenIndex.put(pc, tokenIndex); bytecode.add(opcode); } - private void emit(int value) { + void emit(int value) { bytecode.add((short)value); } - private void emitInt(int value) { + void emitInt(int value) { bytecode.add((short)(value >> 16)); // High 16 bits bytecode.add((short)value); // Low 16 bits } @@ -7559,7 +2834,7 @@ private void emitShort(int value) { * Emit a register index as a short value. * Registers are now 16-bit (0-65535) instead of 8-bit (0-255). */ - private void emitReg(int register) { + void emitReg(int register) { bytecode.add((short)register); } @@ -7567,7 +2842,7 @@ private void emitReg(int register) { * Patch a 2-short (4-byte) int offset at the specified position. * Used for forward jumps where the target is unknown at emit time. */ - private void patchIntOffset(int position, int target) { + void patchIntOffset(int position, int target) { // Store absolute target address (not relative offset) as 2 shorts bytecode.set(position, (short)((target >> 16) & 0xFFFF)); // High 16 bits bytecode.set(position + 1, (short)(target & 0xFFFF)); // Low 16 bits @@ -8462,4 +3737,135 @@ public void visit(ListNode node) { currentCallContext = savedContext; } } + + // ================================================================= + // PACKAGE-LEVEL ACCESSORS FOR REFACTORED CLASSES + // ================================================================= + + /** + * Get the symbol table for package/strict/feature tracking. + * Used by refactored compiler classes. + */ + ScopedSymbolTable getSymbolTable() { + return symbolTable; + } + + /** + * Get the source name for error messages. + * Used by refactored compiler classes. + */ + String getSourceName() { + return sourceName; + } + + /** + * Get the source line for error messages. + * Used by refactored compiler classes. + */ + int getSourceLine() { + return sourceLine; + } + + /** + * Get the bytecode list for position tracking. + * Used by refactored compiler classes for jump patching. + */ + List getBytecode() { + return bytecode; + } + + /** + * Get the variable scopes stack. + * Used by refactored compiler classes for variable declaration. + */ + Stack> getVariableScopes() { + return variableScopes; + } + + /** + * Get the all declared variables map. + * Used by refactored compiler classes for variable registry tracking. + */ + Map getAllDeclaredVariables() { + return allDeclaredVariables; + } + + /** + * Get the captured variable indices map. + * Used by refactored compiler classes for closure support. + */ + Map getCapturedVarIndices() { + return capturedVarIndices; + } + + + /** + * Handle loop control operators: last, next, redo + * Extracted for use by CompileOperator. + */ + void handleLoopControlOperator(OperatorNode node, String op) { + // Extract label if present + String labelStr = null; + if (node.operand instanceof ListNode labelNode && !labelNode.elements.isEmpty()) { + Node arg = labelNode.elements.getFirst(); + if (arg instanceof IdentifierNode) { + labelStr = ((IdentifierNode) arg).name; + } else { + throwCompilerException("Not implemented: " + node, node.getIndex()); + } + } + + // Find the target loop + LoopInfo targetLoop = null; + if (labelStr == null) { + // Unlabeled: find innermost loop + if (!loopStack.isEmpty()) { + targetLoop = loopStack.peek(); + } + } else { + // Labeled: search for matching label + for (int i = loopStack.size() - 1; i >= 0; i--) { + LoopInfo loop = loopStack.get(i); + if (labelStr.equals(loop.label)) { + targetLoop = loop; + break; + } + } + } + + if (targetLoop == null) { + // No matching loop found - non-local control flow + // For now, throw an error. Later we can implement RuntimeControlFlowList + if (labelStr != null) { + throwCompilerException("Can't find label \"" + labelStr + "\"", node.getIndex()); + } else { + throwCompilerException("Can't \"" + op + "\" outside a loop block", node.getIndex()); + } + } + + // Check if this is a pseudo-loop (do-while/bare block) which doesn't support last/next/redo + if (!targetLoop.isTrueLoop) { + throwCompilerException("Can't \"" + op + "\" outside a loop block", node.getIndex()); + } + + // Emit the opcode and record the PC to be patched later + short opcode = op.equals("last") ? Opcodes.LAST + : op.equals("next") ? Opcodes.NEXT + : Opcodes.REDO; + + emitWithToken(opcode, node.getIndex()); + + // Record the PC to be patched (it's the PC of the jump offset operand) + int patchPc = bytecode.size(); + emitInt(0); // Placeholder offset to be patched later + + // Add to the appropriate list in the target loop + if (op.equals("last")) { + targetLoop.breakPcs.add(patchPc); + } else if (op.equals("next")) { + targetLoop.nextPcs.add(patchPc); + } else { // redo + targetLoop.redoPcs.add(patchPc); + } + } } diff --git a/src/main/java/org/perlonjava/interpreter/CompileAssignment.java b/src/main/java/org/perlonjava/interpreter/CompileAssignment.java new file mode 100644 index 000000000..acbcef366 --- /dev/null +++ b/src/main/java/org/perlonjava/interpreter/CompileAssignment.java @@ -0,0 +1,1427 @@ +package org.perlonjava.interpreter; + +import org.perlonjava.astnode.*; +import org.perlonjava.runtime.NameNormalizer; +import org.perlonjava.runtime.RuntimeContextType; + +import java.util.ArrayList; +import java.util.List; + +public class CompileAssignment { + /** + * Helper method to compile assignment operators (=). + * Extracted from visit(BinaryOperatorNode) to reduce method size. + * Handles all forms of assignment including my/our/local, scalars, arrays, hashes, and slices. + */ + public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) { + // Determine the calling context for the RHS based on LHS type + int rhsContext = RuntimeContextType.LIST; // Default + + // Check if LHS is a scalar assignment (my $x = ... or our $x = ...) + if (node.left instanceof OperatorNode) { + OperatorNode leftOp = (OperatorNode) node.left; + if ((leftOp.operator.equals("my") || leftOp.operator.equals("our")) && leftOp.operand instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) leftOp.operand; + if (sigilOp.operator.equals("$")) { + // Scalar assignment: use SCALAR context for RHS + rhsContext = RuntimeContextType.SCALAR; + } + } else if (leftOp.operator.equals("$")) { + // Regular scalar assignment: $x = ... + rhsContext = RuntimeContextType.SCALAR; + } + } + + // Set the context for subroutine calls in RHS + int savedContext = bytecodeCompiler.currentCallContext; + try { + bytecodeCompiler.currentCallContext = rhsContext; + + // Special case: my $x = value + if (node.left instanceof OperatorNode) { + OperatorNode leftOp = (OperatorNode) node.left; + if (leftOp.operator.equals("my")) { + // Extract variable name from "my" operand + Node myOperand = leftOp.operand; + + // Handle my $x (where $x is OperatorNode("$", IdentifierNode("x"))) + if (myOperand instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) myOperand; + if (sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode) { + String varName = "$" + ((IdentifierNode) sigilOp.operand).name; + + // Check if this variable is captured by named subs (Parser marks with id) + if (sigilOp.id != 0) { + // RETRIEVE the persistent variable (creates if doesn't exist) + int beginId = sigilOp.id; + int nameIdx = bytecodeCompiler.addToStringPool(varName); + int reg = bytecodeCompiler.allocateRegister(); + + bytecodeCompiler.emitWithToken(Opcodes.RETRIEVE_BEGIN_SCALAR, node.getIndex()); + bytecodeCompiler.emitReg(reg); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.emit(beginId); + + // Now register contains a reference to the persistent RuntimeScalar + // Store the initializer value INTO that RuntimeScalar + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + // Set the value in the persistent scalar using SET_SCALAR + // This calls .set() on the RuntimeScalar without overwriting the reference + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(reg); + bytecodeCompiler.emitReg(valueReg); + + // Track this variable - map the name to the register we already allocated + bytecodeCompiler.variableScopes.peek().put(varName, reg); + bytecodeCompiler.allDeclaredVariables.put(varName, reg); // Track for variableRegistry + bytecodeCompiler.lastResultReg = reg; + return; + } + + // Regular lexical variable (not captured) + // Allocate register for new lexical variable and add to symbol table + int reg = bytecodeCompiler.addVariable(varName, "my"); + + // Compile RHS in the appropriate context + // @ operator will check currentCallContext and emit ARRAY_SIZE if needed + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + // Move to variable register + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(reg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = reg; + return; + } else if (sigilOp.operator.equals("@") && sigilOp.operand instanceof IdentifierNode) { + // Handle my @array = ... + String varName = "@" + ((IdentifierNode) sigilOp.operand).name; + + // Check if this variable is captured by named subs + if (sigilOp.id != 0) { + // RETRIEVE the persistent array + int beginId = sigilOp.id; + int nameIdx = bytecodeCompiler.addToStringPool(varName); + int arrayReg = bytecodeCompiler.allocateRegister(); + + bytecodeCompiler.emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.emit(beginId); + + // Compile RHS (should evaluate to a list) + node.right.accept(bytecodeCompiler); + int listReg = bytecodeCompiler.lastResultReg; + + // Populate array from list + bytecodeCompiler.emit(Opcodes.ARRAY_SET_FROM_LIST); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(listReg); + + // Track this variable - map the name to the register we already allocated + bytecodeCompiler.variableScopes.peek().put(varName, arrayReg); + bytecodeCompiler.allDeclaredVariables.put(varName, arrayReg); // Track for variableRegistry + + // In scalar context, return the count of elements assigned + // In list/void context, return the array + if (bytecodeCompiler.currentCallContext == RuntimeContextType.SCALAR) { + int countReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_SIZE); + bytecodeCompiler.emitReg(countReg); + bytecodeCompiler.emitReg(listReg); + bytecodeCompiler.lastResultReg = countReg; + } else { + bytecodeCompiler.lastResultReg = arrayReg; + } + return; + } + + // Regular lexical array (not captured) + // Allocate register for new lexical array and add to symbol table + int arrayReg = bytecodeCompiler.addVariable(varName, "my"); + + // Create empty array + bytecodeCompiler.emit(Opcodes.NEW_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + + // Compile RHS (should evaluate to a list) + node.right.accept(bytecodeCompiler); + int listReg = bytecodeCompiler.lastResultReg; + + // Populate array from list using setFromList + bytecodeCompiler.emit(Opcodes.ARRAY_SET_FROM_LIST); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(listReg); + + // In scalar context, return the count of elements assigned + // In list/void context, return the array + if (bytecodeCompiler.currentCallContext == RuntimeContextType.SCALAR) { + int countReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_SIZE); + bytecodeCompiler.emitReg(countReg); + bytecodeCompiler.emitReg(listReg); + bytecodeCompiler.lastResultReg = countReg; + } else { + bytecodeCompiler.lastResultReg = arrayReg; + } + return; + } else if (sigilOp.operator.equals("%") && sigilOp.operand instanceof IdentifierNode) { + // Handle my %hash = ... + String varName = "%" + ((IdentifierNode) sigilOp.operand).name; + + // Check if this variable is captured by named subs + if (sigilOp.id != 0) { + // RETRIEVE the persistent hash + int beginId = sigilOp.id; + int nameIdx = bytecodeCompiler.addToStringPool(varName); + int hashReg = bytecodeCompiler.allocateRegister(); + + bytecodeCompiler.emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.emit(beginId); + + // Compile RHS (should evaluate to a list) + node.right.accept(bytecodeCompiler); + int listReg = bytecodeCompiler.lastResultReg; + + // Populate hash from list + bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(listReg); + + // Track this variable - map the name to the register we already allocated + bytecodeCompiler.variableScopes.peek().put(varName, hashReg); + bytecodeCompiler.allDeclaredVariables.put(varName, hashReg); // Track for variableRegistry + bytecodeCompiler.lastResultReg = hashReg; + return; + } + + // Regular lexical hash (not captured) + // Allocate register for new lexical hash and add to symbol table + int hashReg = bytecodeCompiler.addVariable(varName, "my"); + + // Create empty hash + bytecodeCompiler.emit(Opcodes.NEW_HASH); + bytecodeCompiler.emitReg(hashReg); + + // Compile RHS (should evaluate to a list) + node.right.accept(bytecodeCompiler); + int listReg = bytecodeCompiler.lastResultReg; + + // Populate hash from list + bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(listReg); + + bytecodeCompiler.lastResultReg = hashReg; + return; + } + } + + // Handle my x (direct identifier without sigil) + if (myOperand instanceof IdentifierNode) { + String varName = ((IdentifierNode) myOperand).name; + + // Allocate register for new lexical variable and add to symbol table + int reg = bytecodeCompiler.addVariable(varName, "my"); + + // Compile RHS + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + // Move to variable register + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(reg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = reg; + return; + } + + // Handle my ($x, $y, @rest) = ... - list declaration with assignment + if (myOperand instanceof ListNode) { + ListNode listNode = (ListNode) myOperand; + + // Compile RHS first + node.right.accept(bytecodeCompiler); + int listReg = bytecodeCompiler.lastResultReg; + + // Convert to list if needed + int rhsListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.SCALAR_TO_LIST); + bytecodeCompiler.emitReg(rhsListReg); + bytecodeCompiler.emitReg(listReg); + + // Declare and assign each variable + for (int i = 0; i < listNode.elements.size(); i++) { + Node element = listNode.elements.get(i); + if (element instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) element; + String sigil = sigilOp.operator; + + if (sigilOp.operand instanceof IdentifierNode) { + String varName = sigil + ((IdentifierNode) sigilOp.operand).name; + + int varReg; + + // Check if this variable is captured by named subs (Parser marks with id) + if (sigilOp.id != 0) { + // This variable is captured - use RETRIEVE_BEGIN to get persistent storage + int beginId = sigilOp.id; + int nameIdx = bytecodeCompiler.addToStringPool(varName); + varReg = bytecodeCompiler.allocateRegister(); + + switch (sigil) { + case "$" -> { + bytecodeCompiler.emitWithToken(Opcodes.RETRIEVE_BEGIN_SCALAR, node.getIndex()); + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.emit(beginId); + } + case "@" -> { + bytecodeCompiler.emitWithToken(Opcodes.RETRIEVE_BEGIN_ARRAY, node.getIndex()); + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.emit(beginId); + } + case "%" -> { + bytecodeCompiler.emitWithToken(Opcodes.RETRIEVE_BEGIN_HASH, node.getIndex()); + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.emit(beginId); + } + } + + // Track this variable + bytecodeCompiler.variableScopes.peek().put(varName, varReg); + bytecodeCompiler.allDeclaredVariables.put(varName, varReg); // Track for variableRegistry + } else { + // Regular lexical variable (not captured) + // Declare the variable + varReg = bytecodeCompiler.addVariable(varName, "my"); + + // Initialize based on sigil + switch (sigil) { + case "$" -> { + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(varReg); + } + case "@" -> { + bytecodeCompiler.emit(Opcodes.NEW_ARRAY); + bytecodeCompiler.emitReg(varReg); + } + case "%" -> { + bytecodeCompiler.emit(Opcodes.NEW_HASH); + bytecodeCompiler.emitReg(varReg); + } + } + } + + // Get i-th element from RHS + int indexReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_INT); + bytecodeCompiler.emitReg(indexReg); + bytecodeCompiler.emitInt(i); + + int elemReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_GET); + bytecodeCompiler.emitReg(elemReg); + bytecodeCompiler.emitReg(rhsListReg); + bytecodeCompiler.emitReg(indexReg); + + // Assign to variable + if (sigil.equals("$")) { + if (sigilOp.id != 0) { + // Captured variable - use SET_SCALAR to preserve aliasing + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.emitReg(elemReg); + } else { + // Regular variable - use MOVE + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.emitReg(elemReg); + } + } else if (sigil.equals("@")) { + bytecodeCompiler.emit(Opcodes.ARRAY_SET_FROM_LIST); + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.emitReg(elemReg); + } else if (sigil.equals("%")) { + bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.emitReg(elemReg); + } + } + } + } + + bytecodeCompiler.lastResultReg = rhsListReg; + return; + } + } + + // Special case: local $x = value + if (leftOp.operator.equals("local")) { + // Extract variable from "local" operand + Node localOperand = leftOp.operand; + + // Handle local $hash{key} = value (localizing hash element) + if (localOperand instanceof BinaryOperatorNode) { + BinaryOperatorNode hashAccess = (BinaryOperatorNode) localOperand; + if (hashAccess.operator.equals("{")) { + // Compile the hash access to get the hash element reference + // This returns a RuntimeScalar that is aliased to the hash slot + hashAccess.accept(bytecodeCompiler); + int elemReg = bytecodeCompiler.lastResultReg; + + // Push this hash element to the local variable stack + bytecodeCompiler.emit(Opcodes.PUSH_LOCAL_VARIABLE); + bytecodeCompiler.emitReg(elemReg); + + // Compile RHS + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + // Assign value to the hash element (which is already localized) + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(elemReg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = elemReg; + return; + } + } + + // Handle local $x (where $x is OperatorNode("$", IdentifierNode("x"))) + if (localOperand instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) localOperand; + if (sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode) { + String varName = "$" + ((IdentifierNode) sigilOp.operand).name; + + // Check if it's a lexical variable (should not be localized) + if (bytecodeCompiler.hasVariable(varName)) { + bytecodeCompiler.throwCompilerException("Can't localize lexical variable " + varName); + return; + } + + // Compile RHS first + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + // It's a global variable - call makeLocal which returns the localized scalar + String packageName = bytecodeCompiler.getCurrentPackage(); + String globalVarName = packageName + "::" + ((IdentifierNode) sigilOp.operand).name; + int nameIdx = bytecodeCompiler.addToStringPool(globalVarName); + + int localReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + bytecodeCompiler.emitReg(localReg); + bytecodeCompiler.emit(nameIdx); + + // Assign value to the localized scalar (not to the global!) + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(localReg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = localReg; + return; + } else if (sigilOp.operator.equals("@") && sigilOp.operand instanceof IdentifierNode) { + // Handle local @array = value + String varName = "@" + ((IdentifierNode) sigilOp.operand).name; + + // Check if it's a lexical variable (should not be localized) + if (bytecodeCompiler.hasVariable(varName)) { + bytecodeCompiler.throwCompilerException("Can't localize lexical variable " + varName); + return; + } + + // Compile RHS first + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + // It's a global array - get it and push to local stack + String packageName = bytecodeCompiler.getCurrentPackage(); + String globalVarName = packageName + "::" + ((IdentifierNode) sigilOp.operand).name; + int nameIdx = bytecodeCompiler.addToStringPool(globalVarName); + + int arrayReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + + // Push to local variable stack + bytecodeCompiler.emit(Opcodes.PUSH_LOCAL_VARIABLE); + bytecodeCompiler.emitReg(arrayReg); + + // Populate array from list + bytecodeCompiler.emit(Opcodes.ARRAY_SET_FROM_LIST); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = arrayReg; + return; + } else if (sigilOp.operator.equals("%") && sigilOp.operand instanceof IdentifierNode) { + // Handle local %hash = value + String varName = "%" + ((IdentifierNode) sigilOp.operand).name; + + // Check if it's a lexical variable (should not be localized) + if (bytecodeCompiler.hasVariable(varName)) { + bytecodeCompiler.throwCompilerException("Can't localize lexical variable " + varName); + return; + } + + // Compile RHS first + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + // It's a global hash - get it and push to local stack + String packageName = bytecodeCompiler.getCurrentPackage(); + String globalVarName = packageName + "::" + ((IdentifierNode) sigilOp.operand).name; + int nameIdx = bytecodeCompiler.addToStringPool(globalVarName); + + int hashReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_HASH); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emit(nameIdx); + + // Push to local variable stack + bytecodeCompiler.emit(Opcodes.PUSH_LOCAL_VARIABLE); + bytecodeCompiler.emitReg(hashReg); + + // Populate hash from list + bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = hashReg; + return; + } + } else if (localOperand instanceof ListNode) { + // Handle local($x) = value or local($x, $y) = (v1, v2) + ListNode listNode = (ListNode) localOperand; + + // Special case: single element list local($x) = value + if (listNode.elements.size() == 1) { + Node element = listNode.elements.get(0); + if (element instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) element; + if (sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode) { + String varName = "$" + ((IdentifierNode) sigilOp.operand).name; + + // Check if it's a lexical variable (should not be localized) + if (bytecodeCompiler.hasVariable(varName)) { + bytecodeCompiler.throwCompilerException("Can't localize lexical variable " + varName); + return; + } + + // Compile RHS first + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + // Get the global variable and localize it + String packageName = bytecodeCompiler.getCurrentPackage(); + String globalVarName = packageName + "::" + ((IdentifierNode) sigilOp.operand).name; + int nameIdx = bytecodeCompiler.addToStringPool(globalVarName); + + int localReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + bytecodeCompiler.emitReg(localReg); + bytecodeCompiler.emit(nameIdx); + + // Assign value to the localized variable + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(localReg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = localReg; + return; + } + } + } + + // Multi-element case: local($x, $y) = (v1, v2) + // Compile RHS first + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + // For each element in the list, localize and assign + for (int i = 0; i < listNode.elements.size(); i++) { + Node element = listNode.elements.get(i); + + if (element instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) element; + if (sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode) { + String varName = "$" + ((IdentifierNode) sigilOp.operand).name; + + // Check if it's a lexical variable (should not be localized) + if (bytecodeCompiler.hasVariable(varName)) { + bytecodeCompiler.throwCompilerException("Can't localize lexical variable " + varName); + return; + } + + // Get the global variable + String packageName = bytecodeCompiler.getCurrentPackage(); + String globalVarName = packageName + "::" + ((IdentifierNode) sigilOp.operand).name; + int nameIdx = bytecodeCompiler.addToStringPool(globalVarName); + + int localReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.LOCAL_SCALAR, node.getIndex()); + bytecodeCompiler.emitReg(localReg); + bytecodeCompiler.emit(nameIdx); + + // Extract element from RHS list + int elemReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_GET); + bytecodeCompiler.emitReg(elemReg); + bytecodeCompiler.emitReg(valueReg); + bytecodeCompiler.emitInt(i); + + // Assign to the localized variable + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(localReg); + bytecodeCompiler.emitReg(elemReg); + + if (i == 0) { + // Return the first localized variable + bytecodeCompiler.lastResultReg = localReg; + } + } + } + } + return; + } + } + } + + // Regular assignment: $x = value + // OPTIMIZATION: Detect $x = $x + $y and emit ADD_ASSIGN instead of ADD_SCALAR + MOVE + if (node.left instanceof OperatorNode && node.right instanceof BinaryOperatorNode) { + OperatorNode leftOp = (OperatorNode) node.left; + BinaryOperatorNode rightBin = (BinaryOperatorNode) node.right; + + if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode && + rightBin.operator.equals("+") && + rightBin.left instanceof OperatorNode) { + + String leftVarName = "$" + ((IdentifierNode) leftOp.operand).name; + OperatorNode rightLeftOp = (OperatorNode) rightBin.left; + + if (rightLeftOp.operator.equals("$") && rightLeftOp.operand instanceof IdentifierNode) { + String rightLeftVarName = "$" + ((IdentifierNode) rightLeftOp.operand).name; + + // Pattern match: $x = $x + $y (emit ADD_ASSIGN) + // Skip optimization for captured variables (need SET_SCALAR) + boolean isCaptured = bytecodeCompiler.capturedVarIndices != null && + bytecodeCompiler.capturedVarIndices.containsKey(leftVarName); + + if (leftVarName.equals(rightLeftVarName) && bytecodeCompiler.hasVariable(leftVarName) && !isCaptured) { + int targetReg = bytecodeCompiler.getVariableRegister(leftVarName); + + // Compile RHS operand ($y) + rightBin.right.accept(bytecodeCompiler); + int rhsReg = bytecodeCompiler.lastResultReg; + + // Emit ADD_ASSIGN instead of ADD_SCALAR + MOVE + bytecodeCompiler.emit(Opcodes.ADD_ASSIGN); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emitReg(rhsReg); + + bytecodeCompiler.lastResultReg = targetReg; + return; + } + } + } + } + + // Handle ${block} = value and $$var = value (symbolic references) + // We need to evaluate the LHS FIRST to get the variable name, + // then evaluate the RHS, to ensure the RHS doesn't clobber the LHS registers + if (node.left instanceof OperatorNode leftOp && leftOp.operator.equals("$")) { + if (leftOp.operand instanceof BlockNode) { + // ${block} = value + BlockNode block = (BlockNode) leftOp.operand; + block.accept(bytecodeCompiler); + int nameReg = bytecodeCompiler.lastResultReg; + + // Now compile the RHS + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + // Use STORE_SYMBOLIC_SCALAR to store via symbolic reference + bytecodeCompiler.emit(Opcodes.STORE_SYMBOLIC_SCALAR); + bytecodeCompiler.emitReg(nameReg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = valueReg; + return; + } else if (leftOp.operand instanceof OperatorNode) { + // $$var = value (scalar dereference assignment) + // Evaluate the inner expression to get the variable name + leftOp.operand.accept(bytecodeCompiler); + int nameReg = bytecodeCompiler.lastResultReg; + + // Now compile the RHS + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + // Use STORE_SYMBOLIC_SCALAR to store via symbolic reference + bytecodeCompiler.emit(Opcodes.STORE_SYMBOLIC_SCALAR); + bytecodeCompiler.emitReg(nameReg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = valueReg; + return; + } + } + + // Regular assignment: $x = value (no optimization) + // Compile RHS first + node.right.accept(bytecodeCompiler); + int valueReg = bytecodeCompiler.lastResultReg; + + // Assign to LHS + if (node.left instanceof OperatorNode) { + OperatorNode leftOp = (OperatorNode) node.left; + if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode) { + String varName = "$" + ((IdentifierNode) leftOp.operand).name; + + if (bytecodeCompiler.hasVariable(varName)) { + // Lexical variable - check if it's captured + int targetReg = bytecodeCompiler.getVariableRegister(varName); + + if (bytecodeCompiler.capturedVarIndices != null && bytecodeCompiler.capturedVarIndices.containsKey(varName)) { + // Captured variable - use SET_SCALAR to preserve aliasing + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emitReg(valueReg); + } else { + // Regular lexical - use MOVE + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emitReg(valueReg); + } + + bytecodeCompiler.lastResultReg = targetReg; + } else { + // Global variable + // Check strict vars before assignment + if (bytecodeCompiler.shouldBlockGlobalUnderStrictVars(varName)) { + bytecodeCompiler.throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); + } + + // Strip sigil and normalize name (e.g., "$x" → "main::x") + String bareVarName = varName.substring(1); // Remove sigil + String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(normalizedName); + bytecodeCompiler.emit(Opcodes.STORE_GLOBAL_SCALAR); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.emitReg(valueReg); + bytecodeCompiler.lastResultReg = valueReg; + } + } else if (leftOp.operator.equals("@") && leftOp.operand instanceof IdentifierNode) { + // Array assignment: @array = ... + String varName = "@" + ((IdentifierNode) leftOp.operand).name; + + int arrayReg; + if (bytecodeCompiler.hasVariable(varName)) { + // Lexical array + arrayReg = bytecodeCompiler.getVariableRegister(varName); + } else { + // Global array - load it + arrayReg = bytecodeCompiler.allocateRegister(); + String globalArrayName = NameNormalizer.normalizeVariableName(((IdentifierNode) leftOp.operand).name, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(globalArrayName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + } + + // Populate array from list using setFromList + bytecodeCompiler.emit(Opcodes.ARRAY_SET_FROM_LIST); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = arrayReg; + } else if (leftOp.operator.equals("%") && leftOp.operand instanceof IdentifierNode) { + // Hash assignment: %hash = ... + String varName = "%" + ((IdentifierNode) leftOp.operand).name; + + int hashReg; + if (bytecodeCompiler.hasVariable(varName)) { + // Lexical hash + hashReg = bytecodeCompiler.getVariableRegister(varName); + } else { + // Global hash - load it + hashReg = bytecodeCompiler.allocateRegister(); + String globalHashName = NameNormalizer.normalizeVariableName(((IdentifierNode) leftOp.operand).name, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(globalHashName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_HASH); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emit(nameIdx); + } + + // Populate hash from list using setFromList + bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = hashReg; + } else if (leftOp.operator.equals("our")) { + // Assignment to our variable: our $x = value or our @x = value or our %x = value + // Compile the our declaration first (which loads the global into a register) + leftOp.accept(bytecodeCompiler); + int targetReg = bytecodeCompiler.lastResultReg; + + // Now assign the RHS value to the target register + // The target register contains either a scalar, array, or hash + // We need to determine which and use the appropriate assignment + + // Extract the sigil from our operand + if (leftOp.operand instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) leftOp.operand; + String sigil = sigilOp.operator; + + if (sigil.equals("$")) { + // Scalar: use SET_SCALAR to modify value without breaking alias + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emitReg(valueReg); + } else if (sigil.equals("@")) { + // Array: use ARRAY_SET_FROM_LIST + bytecodeCompiler.emit(Opcodes.ARRAY_SET_FROM_LIST); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emitReg(valueReg); + } else if (sigil.equals("%")) { + // Hash: use HASH_SET_FROM_LIST + bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emitReg(valueReg); + } + } else if (leftOp.operand instanceof ListNode) { + // our ($a, $b) = ... - list declaration with assignment + // The our statement already declared the variables and returned a list + // We need to assign the RHS values to each variable + ListNode listNode = (ListNode) leftOp.operand; + + // Convert RHS to list + int rhsListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.SCALAR_TO_LIST); + bytecodeCompiler.emitReg(rhsListReg); + bytecodeCompiler.emitReg(valueReg); + + // Assign each element + for (int i = 0; i < listNode.elements.size(); i++) { + Node element = listNode.elements.get(i); + if (element instanceof OperatorNode) { + OperatorNode sigilOp = (OperatorNode) element; + String sigil = sigilOp.operator; + + if (sigilOp.operand instanceof IdentifierNode) { + String varName = sigil + ((IdentifierNode) sigilOp.operand).name; + int varReg = bytecodeCompiler.getVariableRegister(varName); + + // Get i-th element from RHS + int indexReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_INT); + bytecodeCompiler.emitReg(indexReg); + bytecodeCompiler.emitInt(i); + + int elemReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_GET); + bytecodeCompiler.emitReg(elemReg); + bytecodeCompiler.emitReg(rhsListReg); + bytecodeCompiler.emitReg(indexReg); + + // Assign to variable + if (sigil.equals("$")) { + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.emitReg(elemReg); + } else if (sigil.equals("@")) { + bytecodeCompiler.emit(Opcodes.ARRAY_SET_FROM_LIST); + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.emitReg(elemReg); + } else if (sigil.equals("%")) { + bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.emitReg(elemReg); + } + } + } + } + bytecodeCompiler.lastResultReg = valueReg; + bytecodeCompiler.currentCallContext = savedContext; + return; + } + + bytecodeCompiler.lastResultReg = targetReg; + } else if (leftOp.operator.equals("*") && leftOp.operand instanceof IdentifierNode) { + // Typeglob assignment: *foo = value + String varName = ((IdentifierNode) leftOp.operand).name; + String globalName = NameNormalizer.normalizeVariableName(varName, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(globalName); + + // Load the glob + int globReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_GLOB); + bytecodeCompiler.emitReg(globReg); + bytecodeCompiler.emit(nameIdx); + + // Store value to glob + bytecodeCompiler.emit(Opcodes.STORE_GLOB); + bytecodeCompiler.emitReg(globReg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = globReg; + } else if (leftOp.operator.equals("pos")) { + // pos($var) = value - lvalue assignment to regex position + // pos() returns a PosLvalueScalar that can be assigned to + node.left.accept(bytecodeCompiler); + int lvalueReg = bytecodeCompiler.lastResultReg; + + // Use SET_SCALAR to assign through the lvalue + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(lvalueReg); + bytecodeCompiler.emitReg(valueReg); + + bytecodeCompiler.lastResultReg = valueReg; + } else { + bytecodeCompiler.throwCompilerException("Assignment to unsupported operator: " + leftOp.operator); + } + } else if (node.left instanceof IdentifierNode) { + String varName = ((IdentifierNode) node.left).name; + + if (bytecodeCompiler.hasVariable(varName)) { + // Lexical variable - copy to its register + int targetReg = bytecodeCompiler.getVariableRegister(varName); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emitReg(valueReg); + bytecodeCompiler.lastResultReg = targetReg; + } else { + // Global variable (varName has no sigil here) + // Check strict vars - add sigil for checking + String varNameWithSigil = "$" + varName; + if (bytecodeCompiler.shouldBlockGlobalUnderStrictVars(varNameWithSigil)) { + bytecodeCompiler.throwCompilerException("Global symbol \"" + varNameWithSigil + "\" requires explicit package name"); + } + + String normalizedName = NameNormalizer.normalizeVariableName(varName, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(normalizedName); + bytecodeCompiler.emit(Opcodes.STORE_GLOBAL_SCALAR); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.emitReg(valueReg); + bytecodeCompiler.lastResultReg = valueReg; + } + } else if (node.left instanceof BinaryOperatorNode) { + BinaryOperatorNode leftBin = (BinaryOperatorNode) node.left; + + // Handle array slice assignment: @array[1, 3, 5] = (20, 30, 40) + if (leftBin.operator.equals("[") && leftBin.left instanceof OperatorNode) { + OperatorNode arrayOp = (OperatorNode) leftBin.left; + + // Must be @array (not $array) + if (arrayOp.operator.equals("@") && arrayOp.operand instanceof IdentifierNode) { + String varName = "@" + ((IdentifierNode) arrayOp.operand).name; + + // Get the array register + int arrayReg; + if (bytecodeCompiler.hasVariable(varName)) { + // Lexical array + arrayReg = bytecodeCompiler.getVariableRegister(varName); + } else { + // Global array - load it + arrayReg = bytecodeCompiler.allocateRegister(); + String globalArrayName = NameNormalizer.normalizeVariableName( + ((IdentifierNode) arrayOp.operand).name, + bytecodeCompiler.getCurrentPackage() + ); + int nameIdx = bytecodeCompiler.addToStringPool(globalArrayName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + } + + // Compile indices (right side of []) + // ArrayLiteralNode contains the indices + if (!(leftBin.right instanceof ArrayLiteralNode)) { + bytecodeCompiler.throwCompilerException("Array slice assignment requires index list"); + } + + ArrayLiteralNode indicesNode = (ArrayLiteralNode) leftBin.right; + List indexRegs = new ArrayList<>(); + for (Node indexNode : indicesNode.elements) { + indexNode.accept(bytecodeCompiler); + indexRegs.add(bytecodeCompiler.lastResultReg); + } + + // Create indices list + int indicesReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + bytecodeCompiler.emitReg(indicesReg); + bytecodeCompiler.emit(indexRegs.size()); + for (int indexReg : indexRegs) { + bytecodeCompiler.emitReg(indexReg); + } + + // Compile values (RHS of assignment) + node.right.accept(bytecodeCompiler); + int valuesReg = bytecodeCompiler.lastResultReg; + + // Emit direct opcode ARRAY_SLICE_SET + bytecodeCompiler.emit(Opcodes.ARRAY_SLICE_SET); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(indicesReg); + bytecodeCompiler.emitReg(valuesReg); + + bytecodeCompiler.lastResultReg = arrayReg; + bytecodeCompiler.currentCallContext = savedContext; + return; + } + } + + // Handle single element array assignment + // For: $array[index] = value or $matrix[3][0] = value + if (leftBin.operator.equals("[")) { + int arrayReg; + + // Check if left side is a variable or multidimensional access + if (leftBin.left instanceof OperatorNode) { + OperatorNode arrayOp = (OperatorNode) leftBin.left; + + // Single element assignment: $array[index] = value + if (arrayOp.operator.equals("$") && arrayOp.operand instanceof IdentifierNode) { + String varName = ((IdentifierNode) arrayOp.operand).name; + String arrayVarName = "@" + varName; + + // Get the array register + if (bytecodeCompiler.hasVariable(arrayVarName)) { + // Lexical array + arrayReg = bytecodeCompiler.getVariableRegister(arrayVarName); + } else { + // Global array - load it + arrayReg = bytecodeCompiler.allocateRegister(); + String globalArrayName = NameNormalizer.normalizeVariableName( + varName, + bytecodeCompiler.getCurrentPackage() + ); + int nameIdx = bytecodeCompiler.addToStringPool(globalArrayName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + } + } else { + bytecodeCompiler.throwCompilerException("Assignment requires scalar dereference: $var[index]"); + return; + } + } else if (leftBin.left instanceof BinaryOperatorNode) { + // Multidimensional case: $matrix[3][0] = value + // Compile left side (which returns a scalar containing an array reference) + leftBin.left.accept(bytecodeCompiler); + int scalarReg = bytecodeCompiler.lastResultReg; + + // Dereference the array reference to get the actual array + arrayReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(scalarReg); + } else { + bytecodeCompiler.throwCompilerException("Array assignment requires variable or expression on left side"); + return; + } + + // Compile index expression + if (!(leftBin.right instanceof ArrayLiteralNode)) { + bytecodeCompiler.throwCompilerException("Array assignment requires ArrayLiteralNode on right side"); + } + ArrayLiteralNode indexNode = (ArrayLiteralNode) leftBin.right; + if (indexNode.elements.isEmpty()) { + bytecodeCompiler.throwCompilerException("Array assignment requires index expression"); + } + + indexNode.elements.get(0).accept(bytecodeCompiler); + int indexReg = bytecodeCompiler.lastResultReg; + + // Compile RHS value + node.right.accept(bytecodeCompiler); + int assignValueReg = bytecodeCompiler.lastResultReg; + + // Emit ARRAY_SET + bytecodeCompiler.emit(Opcodes.ARRAY_SET); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(indexReg); + bytecodeCompiler.emitReg(assignValueReg); + + bytecodeCompiler.lastResultReg = assignValueReg; + bytecodeCompiler.currentCallContext = savedContext; + return; + } else if (leftBin.operator.equals("{")) { + // Hash element/slice assignment + // $hash{key} = value (scalar element) + // @hash{keys} = values (slice) + + // 1. Get hash variable (leftBin.left) + int hashReg; + if (leftBin.left instanceof OperatorNode) { + OperatorNode hashOp = (OperatorNode) leftBin.left; + + // Check for hash slice assignment: @hash{keys} = values + if (hashOp.operator.equals("@")) { + // Hash slice assignment + if (!(hashOp.operand instanceof IdentifierNode)) { + bytecodeCompiler.throwCompilerException("Hash slice assignment requires identifier"); + return; + } + String varName = ((IdentifierNode) hashOp.operand).name; + String hashVarName = "%" + varName; + + // Get the hash - check lexical first, then global + if (bytecodeCompiler.hasVariable(hashVarName)) { + // Lexical hash + hashReg = bytecodeCompiler.getVariableRegister(hashVarName); + } else { + // Global hash - load it + hashReg = bytecodeCompiler.allocateRegister(); + String globalHashName = NameNormalizer.normalizeVariableName( + varName, + bytecodeCompiler.getCurrentPackage() + ); + int nameIdx = bytecodeCompiler.addToStringPool(globalHashName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_HASH); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emit(nameIdx); + } + + // Get the keys from HashLiteralNode + if (!(leftBin.right instanceof HashLiteralNode)) { + bytecodeCompiler.throwCompilerException("Hash slice assignment requires HashLiteralNode"); + return; + } + HashLiteralNode keysNode = (HashLiteralNode) leftBin.right; + if (keysNode.elements.isEmpty()) { + bytecodeCompiler.throwCompilerException("Hash slice assignment requires at least one key"); + return; + } + + // Compile all keys into a list + List keyRegs = new ArrayList<>(); + for (Node keyElement : keysNode.elements) { + if (keyElement instanceof IdentifierNode) { + // Bareword key - autoquote + String keyString = ((IdentifierNode) keyElement).name; + int keyReg = bytecodeCompiler.allocateRegister(); + int keyIdx = bytecodeCompiler.addToStringPool(keyString); + bytecodeCompiler.emit(Opcodes.LOAD_STRING); + bytecodeCompiler.emitReg(keyReg); + bytecodeCompiler.emit(keyIdx); + keyRegs.add(keyReg); + } else { + // Expression key + keyElement.accept(bytecodeCompiler); + keyRegs.add(bytecodeCompiler.lastResultReg); + } + } + + // Create a RuntimeList from key registers + int keysListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + bytecodeCompiler.emitReg(keysListReg); + bytecodeCompiler.emit(keyRegs.size()); + for (int keyReg : keyRegs) { + bytecodeCompiler.emitReg(keyReg); + } + + // Compile RHS values + node.right.accept(bytecodeCompiler); + int valuesReg = bytecodeCompiler.lastResultReg; + + // Emit direct opcode HASH_SLICE_SET + bytecodeCompiler.emit(Opcodes.HASH_SLICE_SET); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(keysListReg); + bytecodeCompiler.emitReg(valuesReg); + + bytecodeCompiler.lastResultReg = valuesReg; + bytecodeCompiler.currentCallContext = savedContext; + return; + } else if (hashOp.operator.equals("$")) { + // $hash{key} - dereference to get hash + if (!(hashOp.operand instanceof IdentifierNode)) { + bytecodeCompiler.throwCompilerException("Hash assignment requires identifier"); + return; + } + String varName = ((IdentifierNode) hashOp.operand).name; + String hashVarName = "%" + varName; + + if (bytecodeCompiler.hasVariable(hashVarName)) { + // Lexical hash + hashReg = bytecodeCompiler.getVariableRegister(hashVarName); + } else { + // Global hash - load it + hashReg = bytecodeCompiler.allocateRegister(); + String globalHashName = NameNormalizer.normalizeVariableName( + varName, + bytecodeCompiler.getCurrentPackage() + ); + int nameIdx = bytecodeCompiler.addToStringPool(globalHashName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_HASH); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emit(nameIdx); + } + } else { + bytecodeCompiler.throwCompilerException("Hash assignment requires scalar dereference: $var{key}"); + return; + } + } else if (leftBin.left instanceof BinaryOperatorNode) { + // Nested: $hash{outer}{inner} = value + // Compile left side (returns scalar containing hash reference or autovivifies) + leftBin.left.accept(bytecodeCompiler); + int scalarReg = bytecodeCompiler.lastResultReg; + + // Dereference to get the hash (with autovivification) + hashReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(scalarReg); + } else { + bytecodeCompiler.throwCompilerException("Hash assignment requires variable or expression on left side"); + return; + } + + // 2. Compile key expression + if (!(leftBin.right instanceof HashLiteralNode)) { + bytecodeCompiler.throwCompilerException("Hash assignment requires HashLiteralNode on right side"); + return; + } + HashLiteralNode keyNode = (HashLiteralNode) leftBin.right; + if (keyNode.elements.isEmpty()) { + bytecodeCompiler.throwCompilerException("Hash key required for assignment"); + return; + } + + // Compile the key + // Special case: IdentifierNode in hash access is autoquoted (bareword key) + int keyReg; + Node keyElement = keyNode.elements.get(0); + if (keyElement instanceof IdentifierNode) { + // Bareword key: $hash{key} -> key is autoquoted to "key" + String keyString = ((IdentifierNode) keyElement).name; + keyReg = bytecodeCompiler.allocateRegister(); + int keyIdx = bytecodeCompiler.addToStringPool(keyString); + bytecodeCompiler.emit(Opcodes.LOAD_STRING); + bytecodeCompiler.emitReg(keyReg); + bytecodeCompiler.emit(keyIdx); + } else { + // Expression key: $hash{$var} or $hash{func()} + keyElement.accept(bytecodeCompiler); + keyReg = bytecodeCompiler.lastResultReg; + } + + // 3. Compile RHS value + node.right.accept(bytecodeCompiler); + int hashValueReg = bytecodeCompiler.lastResultReg; + + // 4. Emit HASH_SET + bytecodeCompiler.emit(Opcodes.HASH_SET); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(keyReg); + bytecodeCompiler.emitReg(hashValueReg); + + bytecodeCompiler.lastResultReg = hashValueReg; + bytecodeCompiler.currentCallContext = savedContext; + return; + } + + // Handle lvalue subroutine: f() = value + // When a function is called in lvalue context, it returns a RuntimeBaseProxy + // that wraps a mutable reference. We can assign to it using SET_SCALAR. + if (leftBin.operator.equals("(")) { + // Call the function (which returns a RuntimeBaseProxy in lvalue context) + node.left.accept(bytecodeCompiler); + int lvalueReg = bytecodeCompiler.lastResultReg; + + // Compile RHS + node.right.accept(bytecodeCompiler); + int rhsReg = bytecodeCompiler.lastResultReg; + + // Assign to the lvalue using SET_SCALAR + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(lvalueReg); + bytecodeCompiler.emitReg(rhsReg); + + bytecodeCompiler.lastResultReg = rhsReg; + bytecodeCompiler.currentCallContext = savedContext; + return; + } + + bytecodeCompiler.throwCompilerException("Assignment to non-identifier not yet supported: " + node.left.getClass().getSimpleName()); + } else if (node.left instanceof ListNode) { + // List assignment: ($a, $b) = ... or () = ... + // In scalar context, returns the number of elements on RHS + // In list context, returns the RHS list + ListNode listNode = (ListNode) node.left; + + // Compile RHS in LIST context to get all elements + int savedRhsContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.LIST; + node.right.accept(bytecodeCompiler); + int rhsReg = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedRhsContext; + + // Convert RHS to RuntimeList if needed + int rhsListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.SCALAR_TO_LIST); + bytecodeCompiler.emitReg(rhsListReg); + bytecodeCompiler.emitReg(rhsReg); + + // If the list is not empty, perform the assignment + if (!listNode.elements.isEmpty()) { + // Assign each RHS element to corresponding LHS variable + for (int i = 0; i < listNode.elements.size(); i++) { + Node lhsElement = listNode.elements.get(i); + + // Get the i-th element from RHS list + int indexReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_INT); + bytecodeCompiler.emitReg(indexReg); + bytecodeCompiler.emitInt(i); + + int elementReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_GET); + bytecodeCompiler.emitReg(elementReg); + bytecodeCompiler.emitReg(rhsListReg); + bytecodeCompiler.emitReg(indexReg); + + // Assign to LHS element + if (lhsElement instanceof OperatorNode) { + OperatorNode lhsOp = (OperatorNode) lhsElement; + if (lhsOp.operator.equals("$") && lhsOp.operand instanceof IdentifierNode) { + String varName = "$" + ((IdentifierNode) lhsOp.operand).name; + + if (bytecodeCompiler.hasVariable(varName)) { + int targetReg = bytecodeCompiler.getVariableRegister(varName); + if (bytecodeCompiler.capturedVarIndices != null && bytecodeCompiler.capturedVarIndices.containsKey(varName)) { + bytecodeCompiler.emit(Opcodes.SET_SCALAR); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emitReg(elementReg); + } else { + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emitReg(elementReg); + } + } else { + // Normalize global variable name (remove sigil, add package) + // Check strict vars before list assignment + if (bytecodeCompiler.shouldBlockGlobalUnderStrictVars(varName)) { + bytecodeCompiler.throwCompilerException("Global symbol \"" + varName + "\" requires explicit package name"); + } + + String bareVarName = varName.substring(1); // Remove "$" + String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(normalizedName); + bytecodeCompiler.emit(Opcodes.STORE_GLOBAL_SCALAR); + bytecodeCompiler.emit(nameIdx); + bytecodeCompiler.emitReg(elementReg); + } + } else if (lhsOp.operator.equals("@") && lhsOp.operand instanceof IdentifierNode) { + // Array slurp: ($a, @rest) = ... + // Collect remaining elements into a RuntimeList + String varName = "@" + ((IdentifierNode) lhsOp.operand).name; + + int arrayReg; + if (bytecodeCompiler.hasVariable(varName)) { + arrayReg = bytecodeCompiler.getVariableRegister(varName); + } else { + arrayReg = bytecodeCompiler.allocateRegister(); + String globalArrayName = NameNormalizer.normalizeVariableName( + ((IdentifierNode) lhsOp.operand).name, + bytecodeCompiler.getCurrentPackage() + ); + int nameIdx = bytecodeCompiler.addToStringPool(globalArrayName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + } + + // Create a list of remaining indices + // Use SLOWOP_LIST_SLICE_FROM to get list[i..] + int remainingListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LIST_SLICE_FROM); + bytecodeCompiler.emitReg(remainingListReg); + bytecodeCompiler.emitReg(rhsListReg); + bytecodeCompiler.emitInt(i); // Start index + + // Populate array from remaining elements + bytecodeCompiler.emit(Opcodes.ARRAY_SET_FROM_LIST); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(remainingListReg); + + // Array slurp consumes all remaining elements + break; + } else if (lhsOp.operator.equals("%") && lhsOp.operand instanceof IdentifierNode) { + // Hash slurp: ($a, %rest) = ... + String varName = "%" + ((IdentifierNode) lhsOp.operand).name; + + int hashReg; + if (bytecodeCompiler.hasVariable(varName)) { + hashReg = bytecodeCompiler.getVariableRegister(varName); + } else { + hashReg = bytecodeCompiler.allocateRegister(); + String globalHashName = NameNormalizer.normalizeVariableName( + ((IdentifierNode) lhsOp.operand).name, + bytecodeCompiler.getCurrentPackage() + ); + int nameIdx = bytecodeCompiler.addToStringPool(globalHashName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_HASH); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emit(nameIdx); + } + + // Get remaining elements from list + int remainingListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LIST_SLICE_FROM); + bytecodeCompiler.emitReg(remainingListReg); + bytecodeCompiler.emitReg(rhsListReg); + bytecodeCompiler.emitInt(i); // Start index + + // Populate hash from remaining elements + bytecodeCompiler.emit(Opcodes.HASH_SET_FROM_LIST); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(remainingListReg); + + // Hash slurp consumes all remaining elements + break; + } + } + } + } + + // Return value depends on savedContext (the context this assignment was called in) + if (savedContext == RuntimeContextType.SCALAR) { + // In scalar context, list assignment returns the count of RHS elements + int countReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_SIZE); + bytecodeCompiler.emitReg(countReg); + bytecodeCompiler.emitReg(rhsListReg); + bytecodeCompiler.lastResultReg = countReg; + } else { + // In list context, return the RHS value + bytecodeCompiler.lastResultReg = rhsListReg; + } + + bytecodeCompiler.currentCallContext = savedContext; + return; + } else { + bytecodeCompiler.throwCompilerException("Assignment to non-identifier not yet supported: " + node.left.getClass().getSimpleName()); + } + + } finally { + // Always restore the calling context + bytecodeCompiler.currentCallContext = savedContext; + } + } +} diff --git a/src/main/java/org/perlonjava/interpreter/CompileBinaryOperator.java b/src/main/java/org/perlonjava/interpreter/CompileBinaryOperator.java new file mode 100644 index 000000000..10fa9a1a7 --- /dev/null +++ b/src/main/java/org/perlonjava/interpreter/CompileBinaryOperator.java @@ -0,0 +1,501 @@ +package org.perlonjava.interpreter; + +import org.perlonjava.astnode.*; +import org.perlonjava.runtime.RuntimeContextType; + +public class CompileBinaryOperator { + static void visitBinaryOperator(BytecodeCompiler bytecodeCompiler, BinaryOperatorNode node) { + // Track token index for error reporting + bytecodeCompiler.currentTokenIndex = node.getIndex(); + + // Handle print/say early (special handling for filehandle) + if (node.operator.equals("print") || node.operator.equals("say")) { + // print/say FILEHANDLE LIST + // left = filehandle reference (\*STDERR) + // right = list to print + + // Compile the filehandle (left operand) + node.left.accept(bytecodeCompiler); + int filehandleReg = bytecodeCompiler.lastResultReg; + + // Compile the content (right operand) + node.right.accept(bytecodeCompiler); + int contentReg = bytecodeCompiler.lastResultReg; + + // Emit PRINT or SAY with both registers + bytecodeCompiler.emit(node.operator.equals("say") ? Opcodes.SAY : Opcodes.PRINT); + bytecodeCompiler.emitReg(contentReg); + bytecodeCompiler.emitReg(filehandleReg); + + // print/say return 1 on success + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_INT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitInt(1); + + bytecodeCompiler.lastResultReg = rd; + return; + } + + // Handle compound assignment operators (+=, -=, *=, /=, %=, .=, &=, |=, ^=, &.=, |.=, ^.=, binary&=, binary|=, binary^=, x=, **=, <<=, >>=, &&=, ||=) + if (node.operator.equals("+=") || node.operator.equals("-=") || + node.operator.equals("*=") || node.operator.equals("/=") || + node.operator.equals("%=") || node.operator.equals(".=") || + node.operator.equals("&=") || node.operator.equals("|=") || node.operator.equals("^=") || + node.operator.equals("&.=") || node.operator.equals("|.=") || node.operator.equals("^.=") || + node.operator.equals("x=") || node.operator.equals("**=") || + node.operator.equals("<<=") || node.operator.equals(">>=") || + node.operator.equals("&&=") || node.operator.equals("||=") || + node.operator.startsWith("binary")) { // Handle binary&=, binary|=, binary^= + bytecodeCompiler.handleCompoundAssignment(node); + return; + } + + // Handle assignment separately (doesn't follow standard left-right-op pattern) + if (node.operator.equals("=")) { + CompileAssignment.compileAssignmentOperator(bytecodeCompiler, node); + return; + } + + + // Handle -> operator specially for hashref/arrayref dereference + if (node.operator.equals("->")) { + bytecodeCompiler.currentTokenIndex = node.getIndex(); // Track token for error reporting + + if (node.right instanceof HashLiteralNode) { + // Hashref dereference: $ref->{key} + // left: scalar containing hash reference + // right: HashLiteralNode containing key + + // Compile the reference (left side) + node.left.accept(bytecodeCompiler); + int scalarRefReg = bytecodeCompiler.lastResultReg; + + // Dereference the scalar to get the actual hash + int hashReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(scalarRefReg); + + // Get the key + HashLiteralNode keyNode = (HashLiteralNode) node.right; + if (keyNode.elements.isEmpty()) { + bytecodeCompiler.throwCompilerException("Hash dereference requires key"); + } + + // Compile the key - handle bareword autoquoting + int keyReg; + Node keyElement = keyNode.elements.get(0); + if (keyElement instanceof IdentifierNode) { + // Bareword key: $ref->{key} -> key is autoquoted + String keyString = ((IdentifierNode) keyElement).name; + keyReg = bytecodeCompiler.allocateRegister(); + int keyIdx = bytecodeCompiler.addToStringPool(keyString); + bytecodeCompiler.emit(Opcodes.LOAD_STRING); + bytecodeCompiler.emitReg(keyReg); + bytecodeCompiler.emit(keyIdx); + } else { + // Expression key: $ref->{$var} + keyElement.accept(bytecodeCompiler); + keyReg = bytecodeCompiler.lastResultReg; + } + + // Access hash element + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.HASH_GET); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(keyReg); + + bytecodeCompiler.lastResultReg = rd; + return; + } else if (node.right instanceof ArrayLiteralNode) { + // Arrayref dereference: $ref->[index] + // left: scalar containing array reference + // right: ArrayLiteralNode containing index + + // Compile the reference (left side) + node.left.accept(bytecodeCompiler); + int scalarRefReg = bytecodeCompiler.lastResultReg; + + // Dereference the scalar to get the actual array + int arrayReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(scalarRefReg); + + // Get the index + ArrayLiteralNode indexNode = (ArrayLiteralNode) node.right; + if (indexNode.elements.isEmpty()) { + bytecodeCompiler.throwCompilerException("Array dereference requires index"); + } + + // Compile the index expression + indexNode.elements.get(0).accept(bytecodeCompiler); + int indexReg = bytecodeCompiler.lastResultReg; + + // Access array element + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_GET); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(indexReg); + + bytecodeCompiler.lastResultReg = rd; + return; + } + // Code reference call: $code->() or $code->(@args) + // right is ListNode with arguments + else if (node.right instanceof ListNode) { + // This is a code reference call: $coderef->(args) + // Compile the code reference in scalar context + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int coderefReg = bytecodeCompiler.lastResultReg; + + // Compile arguments in list context + bytecodeCompiler.currentCallContext = RuntimeContextType.LIST; + node.right.accept(bytecodeCompiler); + int argsReg = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit CALL_SUB opcode + bytecodeCompiler.emit(Opcodes.CALL_SUB); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(coderefReg); + bytecodeCompiler.emitReg(argsReg); + bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); + + bytecodeCompiler.lastResultReg = rd; + return; + } + // Method call: ->method() or ->$method() + // right is BinaryOperatorNode with operator "(" + else if (node.right instanceof BinaryOperatorNode) { + BinaryOperatorNode rightCall = (BinaryOperatorNode) node.right; + if (rightCall.operator.equals("(")) { + // object.call(method, arguments, context) + Node invocantNode = node.left; + Node methodNode = rightCall.left; + Node argsNode = rightCall.right; + + // Convert class name to string if needed: Class->method() + if (invocantNode instanceof IdentifierNode) { + String className = ((IdentifierNode) invocantNode).name; + invocantNode = new StringNode(className, ((IdentifierNode) invocantNode).getIndex()); + } + + // Convert method name to string if needed + if (methodNode instanceof OperatorNode) { + OperatorNode methodOp = (OperatorNode) methodNode; + // &method is introduced by parser if method is predeclared + if (methodOp.operator.equals("&")) { + methodNode = methodOp.operand; + } + } + if (methodNode instanceof IdentifierNode) { + String methodName = ((IdentifierNode) methodNode).name; + methodNode = new StringNode(methodName, ((IdentifierNode) methodNode).getIndex()); + } + + // Compile invocant in scalar context + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + invocantNode.accept(bytecodeCompiler); + int invocantReg = bytecodeCompiler.lastResultReg; + + // Compile method name in scalar context + methodNode.accept(bytecodeCompiler); + int methodReg = bytecodeCompiler.lastResultReg; + + // Get currentSub (__SUB__ for SUPER:: resolution) + int currentSubReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_CODE); + bytecodeCompiler.emitReg(currentSubReg); + int subIdx = bytecodeCompiler.addToStringPool("__SUB__"); + bytecodeCompiler.emit(subIdx); + + // Compile arguments in list context + bytecodeCompiler.currentCallContext = RuntimeContextType.LIST; + argsNode.accept(bytecodeCompiler); + int argsReg = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit CALL_METHOD + bytecodeCompiler.emit(Opcodes.CALL_METHOD); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(invocantReg); + bytecodeCompiler.emitReg(methodReg); + bytecodeCompiler.emitReg(currentSubReg); + bytecodeCompiler.emitReg(argsReg); + bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); + + bytecodeCompiler.lastResultReg = rd; + return; + } + } + // Otherwise, fall through to normal -> handling (method call) + } + + // Handle [] operator for array access + // Must be before automatic operand compilation to handle array slices + if (node.operator.equals("[")) { + bytecodeCompiler.currentTokenIndex = node.getIndex(); + + // Check if this is an array slice: @array[indices] + if (node.left instanceof OperatorNode) { + OperatorNode leftOp = (OperatorNode) node.left; + if (leftOp.operator.equals("@")) { + // This is an array slice - handle it specially + bytecodeCompiler.handleArraySlice(node, leftOp); + return; + } + + // Handle normal array element access: $array[index] + if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode) { + bytecodeCompiler.handleArrayElementAccess(node, leftOp); + return; + } + } + + // Handle general case: expr[index] + // This covers cases like $matrix[1][0] where $matrix[1] is an expression + bytecodeCompiler.handleGeneralArrayAccess(node); + return; + } + + // Handle {} operator specially for hash slice operations + // Must be before automatic operand compilation to avoid compiling @ operator + if (node.operator.equals("{")) { + bytecodeCompiler.currentTokenIndex = node.getIndex(); + + // Check if this is a hash slice: @hash{keys} or @$hashref{keys} + if (node.left instanceof OperatorNode) { + OperatorNode leftOp = (OperatorNode) node.left; + if (leftOp.operator.equals("@")) { + // This is a hash slice - handle it specially + bytecodeCompiler.handleHashSlice(node, leftOp); + return; + } + + // Handle normal hash element access: $hash{key} + if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode) { + bytecodeCompiler.handleHashElementAccess(node, leftOp); + return; + } + } + + // Handle general case: expr{key} + // This covers cases like $hash{outer}{inner} where $hash{outer} is an expression + bytecodeCompiler.handleGeneralHashAccess(node); + return; + } + + // Handle push/unshift operators + if (node.operator.equals("push") || node.operator.equals("unshift")) { + bytecodeCompiler.handlePushUnshift(node); + return; + } + + // Handle "join" operator specially to ensure proper context + // Left operand (separator) needs SCALAR context, right operand (list) needs LIST context + if (node.operator.equals("join")) { + // Save and set context for left operand (separator) + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + + // Set context for right operand (array/list) + bytecodeCompiler.currentCallContext = RuntimeContextType.LIST; + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; + + // Emit JOIN opcode + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.JOIN); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + + bytecodeCompiler.lastResultReg = rd; + return; + } + + // Handle function call operators specially to ensure arguments are in LIST context + if (node.operator.equals("(") || node.operator.equals("()")) { + // Function call: subname(args) or $coderef->(args) + // Save and set context for left operand (code reference) + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + + // Arguments must ALWAYS be evaluated in LIST context + // Even if the call itself is in SCALAR context (e.g., scalar(func())) + bytecodeCompiler.currentCallContext = RuntimeContextType.LIST; + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; + + // Emit CALL_SUB opcode + int rd = CompileBinaryOperatorHelper.compileBinaryOperatorSwitch(bytecodeCompiler, node.operator, rs1, rs2, node.getIndex()); + bytecodeCompiler.lastResultReg = rd; + return; + } + + // Handle short-circuit operators specially - don't compile right operand yet! + if (node.operator.equals("&&") || node.operator.equals("and")) { + // Logical AND with short-circuit evaluation + // Only evaluate right side if left side is true + + // Compile left operand in scalar context (need boolean value) + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; + + // Allocate result register and move left value to it + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + + // Mark position for forward jump + int skipRightPos = bytecodeCompiler.bytecode.size(); + + // Emit conditional jump: if (!rd) skip right evaluation + bytecodeCompiler.emit(Opcodes.GOTO_IF_FALSE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitInt(0); // Placeholder for offset (will be patched) + + // NOW compile right operand (only executed if left was true) + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; + + // Move right result to rd (overwriting left value) + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); + + // Patch the forward jump offset + int skipRightTarget = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + + bytecodeCompiler.lastResultReg = rd; + return; + } + + if (node.operator.equals("||") || node.operator.equals("or")) { + // Logical OR with short-circuit evaluation + // Only evaluate right side if left side is false + + // Compile left operand in scalar context (need boolean value) + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; + + // Allocate result register and move left value to it + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + + // Mark position for forward jump + int skipRightPos = bytecodeCompiler.bytecode.size(); + + // Emit conditional jump: if (rd) skip right evaluation + bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitInt(0); // Placeholder for offset (will be patched) + + // NOW compile right operand (only executed if left was false) + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; + + // Move right result to rd (overwriting left value) + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); + + // Patch the forward jump offset + int skipRightTarget = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + + bytecodeCompiler.lastResultReg = rd; + return; + } + + if (node.operator.equals("//")) { + // Defined-OR with short-circuit evaluation + // Only evaluate right side if left side is undefined + + // Compile left operand in scalar context (need to test definedness) + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; + + // Allocate result register and move left value to it + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + + // Check if left is defined + int definedReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.DEFINED); + bytecodeCompiler.emitReg(definedReg); + bytecodeCompiler.emitReg(rd); + + // Mark position for forward jump + int skipRightPos = bytecodeCompiler.bytecode.size(); + + // Emit conditional jump: if (defined) skip right evaluation + bytecodeCompiler.emit(Opcodes.GOTO_IF_TRUE); + bytecodeCompiler.emitReg(definedReg); + bytecodeCompiler.emitInt(0); // Placeholder for offset (will be patched) + + // NOW compile right operand (only executed if left was undefined) + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; + + // Move right result to rd (overwriting left value) + bytecodeCompiler.emit(Opcodes.MOVE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); + + // Patch the forward jump offset + int skipRightTarget = bytecodeCompiler.bytecode.size(); + bytecodeCompiler.patchIntOffset(skipRightPos + 2, skipRightTarget); + + bytecodeCompiler.lastResultReg = rd; + return; + } + + // Compile left and right operands (for non-short-circuit operators) + node.left.accept(bytecodeCompiler); + int rs1 = bytecodeCompiler.lastResultReg; + + node.right.accept(bytecodeCompiler); + int rs2 = bytecodeCompiler.lastResultReg; + + // Emit opcode based on operator (delegated to helper method) + int rd = CompileBinaryOperatorHelper.compileBinaryOperatorSwitch(bytecodeCompiler, node.operator, rs1, rs2, node.getIndex()); + + + bytecodeCompiler.lastResultReg = rd; + } +} diff --git a/src/main/java/org/perlonjava/interpreter/CompileBinaryOperatorHelper.java b/src/main/java/org/perlonjava/interpreter/CompileBinaryOperatorHelper.java new file mode 100644 index 000000000..a146e6f5c --- /dev/null +++ b/src/main/java/org/perlonjava/interpreter/CompileBinaryOperatorHelper.java @@ -0,0 +1,427 @@ +package org.perlonjava.interpreter; + +import org.perlonjava.runtime.RuntimeContextType; + +public class CompileBinaryOperatorHelper { + /** + * Helper method to compile binary operator switch statement. + * Extracted from visit(BinaryOperatorNode) to reduce method size. + * Handles the giant switch statement for all binary operators. + * + * @param bytecodeCompiler + * @param operator The binary operator string + * @param rs1 Left operand register + * @param rs2 Right operand register + * @param tokenIndex Token index for error reporting + * @return Result register containing the operation result + */ + public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, String operator, int rs1, int rs2, int tokenIndex) { + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit opcode based on operator + switch (operator) { + case "+" -> { + bytecodeCompiler.emit(Opcodes.ADD_SCALAR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "-" -> { + bytecodeCompiler.emit(Opcodes.SUB_SCALAR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "*" -> { + bytecodeCompiler.emit(Opcodes.MUL_SCALAR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "%" -> { + bytecodeCompiler.emit(Opcodes.MOD_SCALAR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "/" -> { + bytecodeCompiler.emit(Opcodes.DIV_SCALAR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "**" -> { + bytecodeCompiler.emit(Opcodes.POW_SCALAR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "." -> { + bytecodeCompiler.emit(Opcodes.CONCAT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "x" -> { + bytecodeCompiler.emit(Opcodes.REPEAT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "<=>" -> { + bytecodeCompiler.emit(Opcodes.COMPARE_NUM); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "cmp" -> { + bytecodeCompiler.emit(Opcodes.COMPARE_STR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "bless" -> { + // bless $ref, "Package" or bless $ref (defaults to current package) + // rs1 = reference to bless + // rs2 = package name (or undef for current package) + bytecodeCompiler.emit(Opcodes.BLESS); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "isa" -> { + // $obj isa "Package" - check if object is instance of package + // rs1 = object/reference + // rs2 = package name + bytecodeCompiler.emit(Opcodes.ISA); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "==" -> { + bytecodeCompiler.emit(Opcodes.EQ_NUM); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "<" -> { + bytecodeCompiler.emit(Opcodes.LT_NUM); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case ">" -> { + bytecodeCompiler.emit(Opcodes.GT_NUM); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "<=" -> { + bytecodeCompiler.emit(Opcodes.LE_NUM); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case ">=" -> { + bytecodeCompiler.emit(Opcodes.GE_NUM); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "!=" -> { + bytecodeCompiler.emit(Opcodes.NE_NUM); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "eq" -> { + // String equality: $a eq $b + bytecodeCompiler.emit(Opcodes.EQ_STR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "ne" -> { + // String inequality: $a ne $b + bytecodeCompiler.emit(Opcodes.NE_STR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "lt", "gt", "le", "ge" -> { + // String comparisons using COMPARE_STR (like cmp) + // cmp returns: -1 if $a lt $b, 0 if equal, 1 if $a gt $b + int cmpReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.COMPARE_STR); + bytecodeCompiler.emitReg(cmpReg); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + + // Compare result to 0 + int zeroReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_INT); + bytecodeCompiler.emitReg(zeroReg); + bytecodeCompiler.emitInt(0); + + // Emit appropriate comparison + switch (operator) { + case "lt" -> bytecodeCompiler.emit(Opcodes.LT_NUM); // cmp < 0 + case "gt" -> bytecodeCompiler.emit(Opcodes.GT_NUM); // cmp > 0 + case "le" -> { + // le: cmp <= 0, which is !(cmp > 0) + int gtReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.GT_NUM); + bytecodeCompiler.emitReg(gtReg); + bytecodeCompiler.emitReg(cmpReg); + bytecodeCompiler.emitReg(zeroReg); + bytecodeCompiler.emit(Opcodes.NOT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(gtReg); + return rd; + } + case "ge" -> { + // ge: cmp >= 0, which is !(cmp < 0) + int ltReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LT_NUM); + bytecodeCompiler.emitReg(ltReg); + bytecodeCompiler.emitReg(cmpReg); + bytecodeCompiler.emitReg(zeroReg); + bytecodeCompiler.emit(Opcodes.NOT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(ltReg); + return rd; + } + } + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(cmpReg); + bytecodeCompiler.emitReg(zeroReg); + } + case "(", "()" -> { + // Apply operator: $coderef->(args) or &subname(args) or foo(args) + // left (rs1) = code reference (RuntimeScalar containing RuntimeCode or SubroutineNode) + // right (rs2) = arguments (should be RuntimeList from ListNode) + + // Note: rs2 should contain a RuntimeList (from visiting the ListNode) + // We need to convert it to RuntimeArray for the CALL_SUB opcode + + // For now, rs2 is a RuntimeList - we'll pass it directly and let + // BytecodeInterpreter convert it to RuntimeArray + + // Emit CALL_SUB: rd = coderef.apply(args, context) + bytecodeCompiler.emit(Opcodes.CALL_SUB); + bytecodeCompiler.emitReg(rd); // Result register + bytecodeCompiler.emitReg(rs1); // Code reference register + bytecodeCompiler.emitReg(rs2); // Arguments register (RuntimeList to be converted to RuntimeArray) + bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); // Use current calling context + + // Note: CALL_SUB may return RuntimeControlFlowList + // The interpreter will handle control flow propagation + } + case ".." -> { + // Range operator: start..end + // Create a PerlRange object which can be iterated or converted to a list + + // Optimization: if both operands are constant numbers, create range at compile time + // (This optimization would need access to the original nodes, which we don't have here) + // So we always use runtime range creation + + // Runtime range creation using RANGE opcode + // rs1 and rs2 already contain the start and end values + bytecodeCompiler.emit(Opcodes.RANGE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "map" -> { + // Map operator: map { block } list + // rs1 = closure (SubroutineNode compiled to code reference) + // rs2 = list expression + + // Emit MAP opcode + bytecodeCompiler.emit(Opcodes.MAP); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); // List register + bytecodeCompiler.emitReg(rs1); // Closure register + bytecodeCompiler.emit(RuntimeContextType.LIST); // Map always uses list context + } + case "grep" -> { + // Grep operator: grep { block } list + // rs1 = closure (SubroutineNode compiled to code reference) + // rs2 = list expression + + // Emit GREP opcode + bytecodeCompiler.emit(Opcodes.GREP); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); // List register + bytecodeCompiler.emitReg(rs1); // Closure register + bytecodeCompiler.emit(RuntimeContextType.LIST); // Grep uses list context + } + case "sort" -> { + // Sort operator: sort { block } list + // rs1 = closure (SubroutineNode compiled to code reference) + // rs2 = list expression + + // Emit SORT opcode + bytecodeCompiler.emit(Opcodes.SORT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs2); // List register + bytecodeCompiler.emitReg(rs1); // Closure register + bytecodeCompiler.emitInt(bytecodeCompiler.addToStringPool(bytecodeCompiler.getCurrentPackage())); // Package name for sort + } + case "split" -> { + // Split operator: split pattern, string + // rs1 = pattern (string or regex) + // rs2 = list containing string to split (and optional limit) + + // Emit direct opcode SPLIT + bytecodeCompiler.emit(Opcodes.SPLIT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); // Pattern register + bytecodeCompiler.emitReg(rs2); // Args register + bytecodeCompiler.emit(RuntimeContextType.LIST); // Split uses list context + } + case "[" -> { + // Array element access: $a[10] means get element 10 from array @a + // Array slice: @a[1,2,3] or @a[1..3] means get multiple elements + // Also handles multidimensional: $a[0][1] means $a[0]->[1] + // This case should NOT be reached because array access is handled specially before this switch + bytecodeCompiler.throwCompilerException("Array access [ should be handled before switch", tokenIndex); + } + case "{" -> { + // Hash element access: $h{key} means get element 'key' from hash %h + // Hash slice access: @h{keys} returns multiple values as array + // This case should NOT be reached because hash access is handled specially before this switch + bytecodeCompiler.throwCompilerException("Hash access { should be handled before switch", tokenIndex); + } + case "push" -> { + // This should NOT be reached because push is handled specially before this switch + bytecodeCompiler.throwCompilerException("push should be handled before switch", tokenIndex); + } + case "unshift" -> { + // This should NOT be reached because unshift is handled specially before this switch + bytecodeCompiler.throwCompilerException("unshift should be handled before switch", tokenIndex); + } + case "+=" -> { + // This should NOT be reached because += is handled specially before this switch + bytecodeCompiler.throwCompilerException("+= should be handled before switch", tokenIndex); + } + case "-=", "*=", "/=", "%=", ".=" -> { + // This should NOT be reached because compound assignments are handled specially before this switch + bytecodeCompiler.throwCompilerException(operator + " should be handled before switch", tokenIndex); + } + case "readline" -> { + // <$fh> - read line from filehandle + // rs1 = filehandle (or undef for ARGV) + // rs2 = unused (ListNode) + bytecodeCompiler.emit(Opcodes.READLINE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); + } + case "=~" -> { + // $string =~ /pattern/ - regex match + // rs1 = string to match against + // rs2 = compiled regex pattern + bytecodeCompiler.emit(Opcodes.MATCH_REGEX); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); + } + case "!~" -> { + // $string !~ /pattern/ - negated regex match + // rs1 = string to match against + // rs2 = compiled regex pattern + bytecodeCompiler.emit(Opcodes.MATCH_REGEX_NOT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); + } + case "&" -> { + // Numeric bitwise AND (default): rs1 & rs2 + bytecodeCompiler.emit(Opcodes.BITWISE_AND_BINARY); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "binary&" -> { + // Numeric bitwise AND (use integer): rs1 binary& rs2 + // Same as & but explicitly numeric + bytecodeCompiler.emit(Opcodes.BITWISE_AND_BINARY); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "|" -> { + // Numeric bitwise OR (default): rs1 | rs2 + bytecodeCompiler.emit(Opcodes.BITWISE_OR_BINARY); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "binary|" -> { + // Numeric bitwise OR (use integer): rs1 binary| rs2 + // Same as | but explicitly numeric + bytecodeCompiler.emit(Opcodes.BITWISE_OR_BINARY); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "^" -> { + // Numeric bitwise XOR (default): rs1 ^ rs2 + bytecodeCompiler.emit(Opcodes.BITWISE_XOR_BINARY); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "binary^" -> { + // Numeric bitwise XOR (use integer): rs1 binary^ rs2 + // Same as ^ but explicitly numeric + bytecodeCompiler.emit(Opcodes.BITWISE_XOR_BINARY); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "&." -> { + // String bitwise AND: rs1 &. rs2 + bytecodeCompiler.emit(Opcodes.STRING_BITWISE_AND); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "|." -> { + // String bitwise OR: rs1 |. rs2 + bytecodeCompiler.emit(Opcodes.STRING_BITWISE_OR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "^." -> { + // String bitwise XOR: rs1 ^. rs2 + bytecodeCompiler.emit(Opcodes.STRING_BITWISE_XOR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case "<<" -> { + // Left shift: rs1 << rs2 + bytecodeCompiler.emit(Opcodes.LEFT_SHIFT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + case ">>" -> { + // Right shift: rs1 >> rs2 + bytecodeCompiler.emit(Opcodes.RIGHT_SHIFT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs1); + bytecodeCompiler.emitReg(rs2); + } + default -> bytecodeCompiler.throwCompilerException("Unsupported operator: " + operator, tokenIndex); + } + + return rd; + } +} diff --git a/src/main/java/org/perlonjava/interpreter/CompileOperator.java b/src/main/java/org/perlonjava/interpreter/CompileOperator.java new file mode 100644 index 000000000..ae7626c8c --- /dev/null +++ b/src/main/java/org/perlonjava/interpreter/CompileOperator.java @@ -0,0 +1,2390 @@ +package org.perlonjava.interpreter; + +import org.perlonjava.astnode.*; +import org.perlonjava.runtime.NameNormalizer; +import org.perlonjava.runtime.RuntimeContextType; + +import java.util.ArrayList; +import java.util.List; + +public class CompileOperator { + public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode node) { + // Track token index for error reporting + bytecodeCompiler.currentTokenIndex = node.getIndex(); + + String op = node.operator; + + // Group 1: Variable declarations (my, our, local, state) + if (op.equals("my") || op.equals("our") || op.equals("local") || op.equals("state")) { + bytecodeCompiler.compileVariableDeclaration(node, op); + return; + } + + // Group 2: Variable reference operators ($, @, %, *, &, \) + if (op.equals("$") || op.equals("@") || op.equals("%") || op.equals("*") || op.equals("&") || op.equals("\\")) { + bytecodeCompiler.compileVariableReference(node, op); + return; + } + + // Handle remaining operators + if (op.equals("scalar")) { + // Force scalar context: scalar(expr) + // Evaluates the operand and converts the result to scalar + if (node.operand != null) { + // Evaluate operand in scalar context + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + try { + node.operand.accept(bytecodeCompiler); + int operandReg = bytecodeCompiler.lastResultReg; + + // Emit ARRAY_SIZE to convert to scalar + // This handles arrays/hashes (converts to size) and passes through scalars + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_SIZE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(operandReg); + + bytecodeCompiler.lastResultReg = rd; + } finally { + bytecodeCompiler.currentCallContext = savedContext; + } + } else { + bytecodeCompiler.throwCompilerException("scalar operator requires an operand"); + } + return; + } else if (op.equals("package") || op.equals("class")) { + // Package/Class declaration: package Foo; or class Foo; + // This updates the current package context for subsequent variable declarations + if (node.operand instanceof IdentifierNode) { + String packageName = ((IdentifierNode) node.operand).name; + + // Check if this is a class declaration (either "class" operator or isClass annotation) + Boolean isClassAnnotation = (Boolean) node.getAnnotation("isClass"); + boolean isClass = op.equals("class") || (isClassAnnotation != null && isClassAnnotation); + + // Update the current package/class in symbol table + // This tracks package name, isClass flag, and version + bytecodeCompiler.symbolTable.setCurrentPackage(packageName, isClass); + + // Register as Perl 5.38+ class for proper stringification if needed + if (isClass) { + org.perlonjava.runtime.ClassRegistry.registerClass(packageName); + } + + bytecodeCompiler.lastResultReg = -1; // No runtime value + } else { + bytecodeCompiler.throwCompilerException(op + " operator requires an identifier"); + } + } else if (op.equals("say") || op.equals("print")) { + // say/print $x + if (node.operand != null) { + node.operand.accept(bytecodeCompiler); + int rs = bytecodeCompiler.lastResultReg; + + bytecodeCompiler.emit(op.equals("say") ? Opcodes.SAY : Opcodes.PRINT); + bytecodeCompiler.emitReg(rs); + } + } else if (op.equals("not") || op.equals("!")) { + // Logical NOT operator: not $x or !$x + // Evaluate operand in scalar context (need boolean value) + if (node.operand != null) { + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + node.operand.accept(bytecodeCompiler); + int rs = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit NOT opcode + bytecodeCompiler.emit(Opcodes.NOT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs); + + bytecodeCompiler.lastResultReg = rd; + } else { + bytecodeCompiler.throwCompilerException("NOT operator requires operand"); + } + } else if (op.equals("~") || op.equals("binary~")) { + // Bitwise NOT operator: ~$x or binary~$x + // Evaluate operand and emit BITWISE_NOT_BINARY opcode + if (node.operand != null) { + node.operand.accept(bytecodeCompiler); + int rs = bytecodeCompiler.lastResultReg; + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit BITWISE_NOT_BINARY opcode + bytecodeCompiler.emit(Opcodes.BITWISE_NOT_BINARY); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs); + + bytecodeCompiler.lastResultReg = rd; + } else { + bytecodeCompiler.throwCompilerException("Bitwise NOT operator requires operand"); + } + } else if (op.equals("~.")) { + // String bitwise NOT operator: ~.$x + // Evaluate operand and emit BITWISE_NOT_STRING opcode + if (node.operand != null) { + node.operand.accept(bytecodeCompiler); + int rs = bytecodeCompiler.lastResultReg; + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit BITWISE_NOT_STRING opcode + bytecodeCompiler.emit(Opcodes.BITWISE_NOT_STRING); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs); + + bytecodeCompiler.lastResultReg = rd; + } else { + bytecodeCompiler.throwCompilerException("String bitwise NOT operator requires operand"); + } + } else if (op.equals("defined")) { + // Defined operator: defined($x) + // Check if value is defined (not undef) + if (node.operand != null) { + node.operand.accept(bytecodeCompiler); + int rs = bytecodeCompiler.lastResultReg; + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit DEFINED opcode + bytecodeCompiler.emit(Opcodes.DEFINED); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(rs); + + bytecodeCompiler.lastResultReg = rd; + } else { + bytecodeCompiler.throwCompilerException("defined operator requires operand"); + } + } else if (op.equals("ref")) { + // Ref operator: ref($x) + // Get reference type (blessed class name or base type) + if (node.operand == null) { + bytecodeCompiler.throwCompilerException("ref requires an argument"); + } + + // Compile the operand + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (list.elements.isEmpty()) { + bytecodeCompiler.throwCompilerException("ref requires an argument"); + } + // Get first element + list.elements.get(0).accept(bytecodeCompiler); + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit REF opcode + bytecodeCompiler.emit(Opcodes.REF); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("prototype")) { + // Prototype operator: prototype(\&func) or prototype("func_name") + // Returns the prototype string for a subroutine + if (node.operand == null) { + bytecodeCompiler.throwCompilerException("prototype requires an argument"); + } + + // Compile the operand (code reference or function name) + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (list.elements.isEmpty()) { + bytecodeCompiler.throwCompilerException("prototype requires an argument"); + } + // Get first element + list.elements.get(0).accept(bytecodeCompiler); + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Add current package to string pool + int packageIdx = bytecodeCompiler.addToStringPool(bytecodeCompiler.getCurrentPackage()); + + // Emit PROTOTYPE opcode + bytecodeCompiler.emit(Opcodes.PROTOTYPE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.emitInt(packageIdx); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("quoteRegex")) { + // Quote regex operator: qr{pattern}flags + // operand is a ListNode with [pattern, flags] + if (node.operand == null || !(node.operand instanceof ListNode)) { + bytecodeCompiler.throwCompilerException("quoteRegex requires pattern and flags"); + } + + ListNode operand = (ListNode) node.operand; + if (operand.elements.size() < 2) { + bytecodeCompiler.throwCompilerException("quoteRegex requires pattern and flags"); + } + + // Compile pattern and flags + operand.elements.get(0).accept(bytecodeCompiler); // Pattern + int patternReg = bytecodeCompiler.lastResultReg; + + operand.elements.get(1).accept(bytecodeCompiler); // Flags + int flagsReg = bytecodeCompiler.lastResultReg; + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit QUOTE_REGEX opcode + bytecodeCompiler.emit(Opcodes.QUOTE_REGEX); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(patternReg); + bytecodeCompiler.emitReg(flagsReg); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("++") || op.equals("--") || op.equals("++postfix") || op.equals("--postfix")) { + // Pre/post increment/decrement + boolean isPostfix = op.endsWith("postfix"); + boolean isIncrement = op.startsWith("++"); + + if (node.operand instanceof IdentifierNode) { + String varName = ((IdentifierNode) node.operand).name; + + if (bytecodeCompiler.hasVariable(varName)) { + int varReg = bytecodeCompiler.getVariableRegister(varName); + + // Use optimized autoincrement/decrement opcodes + if (isPostfix) { + // Postfix: returns old value before modifying + // Need TWO registers: one for result (old value), one for variable + int resultReg = bytecodeCompiler.allocateRegister(); + if (isIncrement) { + bytecodeCompiler.emit(Opcodes.POST_AUTOINCREMENT); + } else { + bytecodeCompiler.emit(Opcodes.POST_AUTODECREMENT); + } + bytecodeCompiler.emitReg(resultReg); // Destination for old value + bytecodeCompiler.emitReg(varReg); // Variable to modify in-place + bytecodeCompiler.lastResultReg = resultReg; + } else { + // Prefix: returns new value after modifying + if (isIncrement) { + bytecodeCompiler.emit(Opcodes.PRE_AUTOINCREMENT); + } else { + bytecodeCompiler.emit(Opcodes.PRE_AUTODECREMENT); + } + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.lastResultReg = varReg; + } + } else { + bytecodeCompiler.throwCompilerException("Increment/decrement of non-lexical variable not yet supported"); + } + } else if (node.operand instanceof OperatorNode) { + // Handle $x++ + OperatorNode innerOp = (OperatorNode) node.operand; + if (innerOp.operator.equals("$") && innerOp.operand instanceof IdentifierNode) { + String varName = "$" + ((IdentifierNode) innerOp.operand).name; + + if (bytecodeCompiler.hasVariable(varName)) { + int varReg = bytecodeCompiler.getVariableRegister(varName); + + // Use optimized autoincrement/decrement opcodes + if (isPostfix) { + // Postfix: returns old value before modifying + // Need TWO registers: one for result (old value), one for variable + int resultReg = bytecodeCompiler.allocateRegister(); + if (isIncrement) { + bytecodeCompiler.emit(Opcodes.POST_AUTOINCREMENT); + } else { + bytecodeCompiler.emit(Opcodes.POST_AUTODECREMENT); + } + bytecodeCompiler.emitReg(resultReg); // Destination for old value + bytecodeCompiler.emitReg(varReg); // Variable to modify in-place + bytecodeCompiler.lastResultReg = resultReg; + } else { + if (isIncrement) { + bytecodeCompiler.emit(Opcodes.PRE_AUTOINCREMENT); + } else { + bytecodeCompiler.emit(Opcodes.PRE_AUTODECREMENT); + } + bytecodeCompiler.emitReg(varReg); + bytecodeCompiler.lastResultReg = varReg; + } + } else { + // Global variable increment/decrement + // Normalize global variable name (remove sigil, add package) + String bareVarName = varName.substring(1); // Remove "$" + String normalizedName = NameNormalizer.normalizeVariableName(bareVarName, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(normalizedName); + + // Load global variable + int globalReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_SCALAR); + bytecodeCompiler.emitReg(globalReg); + bytecodeCompiler.emit(nameIdx); + + // Apply increment/decrement + if (isPostfix) { + // Postfix: returns old value before modifying + // Need TWO registers: one for result (old value), one for variable + int resultReg = bytecodeCompiler.allocateRegister(); + if (isIncrement) { + bytecodeCompiler.emit(Opcodes.POST_AUTOINCREMENT); + } else { + bytecodeCompiler.emit(Opcodes.POST_AUTODECREMENT); + } + bytecodeCompiler.emitReg(resultReg); // Destination for old value + bytecodeCompiler.emitReg(globalReg); // Variable to modify in-place + bytecodeCompiler.lastResultReg = resultReg; + } else { + if (isIncrement) { + bytecodeCompiler.emit(Opcodes.PRE_AUTOINCREMENT); + } else { + bytecodeCompiler.emit(Opcodes.PRE_AUTODECREMENT); + } + bytecodeCompiler.emitReg(globalReg); + bytecodeCompiler.lastResultReg = globalReg; + } + + // NOTE: Do NOT store back to global variable! + // The POST/PRE_AUTO* opcodes modify the global variable directly + // and return the appropriate value (old for postfix, new for prefix). + // Storing back would overwrite the modification with the return value. + } + } else { + bytecodeCompiler.throwCompilerException("Invalid operand for increment/decrement operator"); + } + } else { + bytecodeCompiler.throwCompilerException("Increment/decrement operator requires operand"); + } + } else if (op.equals("return")) { + // return $expr; + // Also handles 'goto &NAME' tail calls (parsed as 'return (coderef(@_))') + + // Check if this is a 'goto &NAME' or 'goto EXPR' tail call + // Pattern: return with ListNode containing single BinaryOperatorNode("(") + // where left is OperatorNode("&") and right is @_ + if (node.operand instanceof ListNode list && list.elements.size() == 1) { + Node firstElement = list.elements.getFirst(); + if (firstElement instanceof BinaryOperatorNode callNode && callNode.operator.equals("(")) { + Node callTarget = callNode.left; + + // Handle &sub syntax (goto &foo) + if (callTarget instanceof OperatorNode opNode && opNode.operator.equals("&")) { + // This is a tail call: goto &sub + // Evaluate the code reference in scalar context + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + callTarget.accept(bytecodeCompiler); + int codeRefReg = bytecodeCompiler.lastResultReg; + + // Evaluate the arguments in list context (usually @_) + bytecodeCompiler.currentCallContext = RuntimeContextType.LIST; + callNode.right.accept(bytecodeCompiler); + int argsReg = bytecodeCompiler.lastResultReg; + bytecodeCompiler.currentCallContext = savedContext; + + // Allocate register for call result + int rd = bytecodeCompiler.allocateRegister(); + + // Emit CALL_SUB to invoke the code reference with proper context + bytecodeCompiler.emit(Opcodes.CALL_SUB); + bytecodeCompiler.emitReg(rd); // Result register + bytecodeCompiler.emitReg(codeRefReg); // Code reference register + bytecodeCompiler.emitReg(argsReg); // Arguments register + bytecodeCompiler.emit(savedContext); // Use saved calling context for the tail call + + // Then return the result + bytecodeCompiler.emitWithToken(Opcodes.RETURN, node.getIndex()); + bytecodeCompiler.emitReg(rd); + + bytecodeCompiler.lastResultReg = -1; + return; + } + } + } + + if (node.operand != null) { + // Regular return with expression + node.operand.accept(bytecodeCompiler); + int exprReg = bytecodeCompiler.lastResultReg; + + // Emit RETURN with expression register + bytecodeCompiler.emitWithToken(Opcodes.RETURN, node.getIndex()); + bytecodeCompiler.emitReg(exprReg); + } else { + // return; (no value - return empty list/undef) + int undefReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(undefReg); + + bytecodeCompiler.emitWithToken(Opcodes.RETURN, node.getIndex()); + bytecodeCompiler.emitReg(undefReg); + } + bytecodeCompiler.lastResultReg = -1; // No result after return + } else if (op.equals("last") || op.equals("next") || op.equals("redo")) { + // Loop control operators: last/next/redo [LABEL] + bytecodeCompiler.handleLoopControlOperator(node, op); + bytecodeCompiler.lastResultReg = -1; // No result after control flow + } else if (op.equals("rand")) { + // rand() or rand($max) + // Calls Random.rand(max) where max defaults to 1 + int rd = bytecodeCompiler.allocateRegister(); + + if (node.operand != null) { + // rand($max) - evaluate operand + node.operand.accept(bytecodeCompiler); + int maxReg = bytecodeCompiler.lastResultReg; + + // Emit RAND opcode + bytecodeCompiler.emit(Opcodes.RAND); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(maxReg); + } else { + // rand() with no argument - defaults to 1 + int oneReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_INT); + bytecodeCompiler.emitReg(oneReg); + bytecodeCompiler.emitInt(1); + + // Emit RAND opcode + bytecodeCompiler.emit(Opcodes.RAND); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(oneReg); + } + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("sleep")) { + // sleep $seconds + // Calls Time.sleep(seconds) + int rd = bytecodeCompiler.allocateRegister(); + + if (node.operand != null) { + // sleep($seconds) - evaluate operand + node.operand.accept(bytecodeCompiler); + int secondsReg = bytecodeCompiler.lastResultReg; + + // Emit direct opcode SLEEP_OP + bytecodeCompiler.emit(Opcodes.SLEEP_OP); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(secondsReg); + } else { + // sleep with no argument - defaults to infinity (but we'll use a large number) + int maxReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_INT); + bytecodeCompiler.emitReg(maxReg); + bytecodeCompiler.emitInt(Integer.MAX_VALUE); + + bytecodeCompiler.emit(Opcodes.SLEEP_OP); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(maxReg); + } + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("study")) { + // study $var + // In modern Perl, study is a no-op that always returns true + // We evaluate the operand for side effects, then return 1 + + if (node.operand != null) { + // Evaluate operand for side effects (though typically there are none) + node.operand.accept(bytecodeCompiler); + } + + // Return 1 (true) + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_INT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitInt(1); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("require")) { + // require MODULE_NAME or require VERSION + // Evaluate operand in scalar context + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + try { + node.operand.accept(bytecodeCompiler); + int operandReg = bytecodeCompiler.lastResultReg; + + // Call ModuleOperators.require() + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.REQUIRE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(operandReg); + + bytecodeCompiler.lastResultReg = rd; + } finally { + bytecodeCompiler.currentCallContext = savedContext; + } + } else if (op.equals("pos")) { + // pos($var) - get or set regex match position + // Returns an lvalue that can be assigned to + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + try { + node.operand.accept(bytecodeCompiler); + int operandReg = bytecodeCompiler.lastResultReg; + + // Call RuntimeScalar.pos() + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.POS); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(operandReg); + + bytecodeCompiler.lastResultReg = rd; + } finally { + bytecodeCompiler.currentCallContext = savedContext; + } + } else if (op.equals("index") || op.equals("rindex")) { + // index(str, substr, pos?) or rindex(str, substr, pos?) + if (node.operand instanceof ListNode) { + ListNode args = (ListNode) node.operand; + + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + try { + // Evaluate first arg (string) + if (args.elements.isEmpty()) { + bytecodeCompiler.throwCompilerException("Not enough arguments for " + op); + } + args.elements.get(0).accept(bytecodeCompiler); + int strReg = bytecodeCompiler.lastResultReg; + + // Evaluate second arg (substring) + if (args.elements.size() < 2) { + bytecodeCompiler.throwCompilerException("Not enough arguments for " + op); + } + args.elements.get(1).accept(bytecodeCompiler); + int substrReg = bytecodeCompiler.lastResultReg; + + // Evaluate third arg (position) - optional, defaults to undef + int posReg; + if (args.elements.size() >= 3) { + args.elements.get(2).accept(bytecodeCompiler); + posReg = bytecodeCompiler.lastResultReg; + } else { + posReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(posReg); + } + + // Call index or rindex + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(op.equals("index") ? Opcodes.INDEX : Opcodes.RINDEX); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(strReg); + bytecodeCompiler.emitReg(substrReg); + bytecodeCompiler.emitReg(posReg); + + bytecodeCompiler.lastResultReg = rd; + } finally { + bytecodeCompiler.currentCallContext = savedContext; + } + } else { + bytecodeCompiler.throwCompilerException(op + " requires a list of arguments"); + } + } else if (op.equals("stat") || op.equals("lstat")) { + // stat FILE or lstat FILE + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + try { + node.operand.accept(bytecodeCompiler); + int operandReg = bytecodeCompiler.lastResultReg; + + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(op.equals("stat") ? Opcodes.STAT : Opcodes.LSTAT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(operandReg); + bytecodeCompiler.emit(savedContext); // Pass calling context + + bytecodeCompiler.lastResultReg = rd; + } finally { + bytecodeCompiler.currentCallContext = savedContext; + } + } else if (op.startsWith("-") && op.length() == 2) { + // File test operators: -r, -w, -x, etc. + + // Check if operand is the special filehandle "_" + boolean isUnderscoreOperand = (node.operand instanceof IdentifierNode) + && ((IdentifierNode) node.operand).name.equals("_"); + + if (isUnderscoreOperand) { + // Special case: -r _ uses cached file handle + // Call FileTestOperator.fileTestLastHandle(String) + int rd = bytecodeCompiler.allocateRegister(); + int operatorStrIndex = bytecodeCompiler.addToStringPool(op); + + // Emit FILETEST_LASTHANDLE opcode + bytecodeCompiler.emit(Opcodes.FILETEST_LASTHANDLE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emit(operatorStrIndex); + + bytecodeCompiler.lastResultReg = rd; + } else { + // Normal case: evaluate operand and test it + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + try { + node.operand.accept(bytecodeCompiler); + int operandReg = bytecodeCompiler.lastResultReg; + + int rd = bytecodeCompiler.allocateRegister(); + + // Map operator to opcode + char testChar = op.charAt(1); + short opcode; + switch (testChar) { + case 'r': + opcode = Opcodes.FILETEST_R; + break; + case 'w': + opcode = Opcodes.FILETEST_W; + break; + case 'x': + opcode = Opcodes.FILETEST_X; + break; + case 'o': + opcode = Opcodes.FILETEST_O; + break; + case 'R': + opcode = Opcodes.FILETEST_R_REAL; + break; + case 'W': + opcode = Opcodes.FILETEST_W_REAL; + break; + case 'X': + opcode = Opcodes.FILETEST_X_REAL; + break; + case 'O': + opcode = Opcodes.FILETEST_O_REAL; + break; + case 'e': + opcode = Opcodes.FILETEST_E; + break; + case 'z': + opcode = Opcodes.FILETEST_Z; + break; + case 's': + opcode = Opcodes.FILETEST_S; + break; + case 'f': + opcode = Opcodes.FILETEST_F; + break; + case 'd': + opcode = Opcodes.FILETEST_D; + break; + case 'l': + opcode = Opcodes.FILETEST_L; + break; + case 'p': + opcode = Opcodes.FILETEST_P; + break; + case 'S': + opcode = Opcodes.FILETEST_S_UPPER; + break; + case 'b': + opcode = Opcodes.FILETEST_B; + break; + case 'c': + opcode = Opcodes.FILETEST_C; + break; + case 't': + opcode = Opcodes.FILETEST_T; + break; + case 'u': + opcode = Opcodes.FILETEST_U; + break; + case 'g': + opcode = Opcodes.FILETEST_G; + break; + case 'k': + opcode = Opcodes.FILETEST_K; + break; + case 'T': + opcode = Opcodes.FILETEST_T_UPPER; + break; + case 'B': + opcode = Opcodes.FILETEST_B_UPPER; + break; + case 'M': + opcode = Opcodes.FILETEST_M; + break; + case 'A': + opcode = Opcodes.FILETEST_A; + break; + case 'C': + opcode = Opcodes.FILETEST_C_UPPER; + break; + default: + bytecodeCompiler.throwCompilerException("Unsupported file test operator: " + op); + return; + } + + bytecodeCompiler.emit(opcode); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(operandReg); + + bytecodeCompiler.lastResultReg = rd; + } finally { + bytecodeCompiler.currentCallContext = savedContext; + } + } + } else if (op.equals("die")) { + // die $message; + if (node.operand != null) { + // Evaluate die message + node.operand.accept(bytecodeCompiler); + int msgReg = bytecodeCompiler.lastResultReg; + + // Precompute location message at compile time (zero overhead!) + String locationMsg; + // Use annotation from AST node which has the correct line number + Object lineObj = node.getAnnotation("line"); + Object fileObj = node.getAnnotation("file"); + if (lineObj != null && fileObj != null) { + String fileName = fileObj.toString(); + int lineNumber = Integer.parseInt(lineObj.toString()); + locationMsg = " at " + fileName + " line " + lineNumber; + } else if (bytecodeCompiler.errorUtil != null) { + // Fallback to errorUtil if annotations not available + String fileName = bytecodeCompiler.errorUtil.getFileName(); + int lineNumber = bytecodeCompiler.errorUtil.getLineNumberAccurate(node.getIndex()); + locationMsg = " at " + fileName + " line " + lineNumber; + } else { + // Final fallback if neither available + locationMsg = " at " + bytecodeCompiler.sourceName + " line " + bytecodeCompiler.sourceLine; + } + + int locationReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_STRING); + bytecodeCompiler.emitReg(locationReg); + bytecodeCompiler.emit(bytecodeCompiler.addToStringPool(locationMsg)); + + // Emit DIE with both message and precomputed location + bytecodeCompiler.emitWithToken(Opcodes.DIE, node.getIndex()); + bytecodeCompiler.emitReg(msgReg); + bytecodeCompiler.emitReg(locationReg); + } else { + // die; (no message - use $@) + // For now, emit with undef register + int undefReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(undefReg); + + // Precompute location message for bare die + String locationMsg; + if (bytecodeCompiler.errorUtil != null) { + String fileName = bytecodeCompiler.errorUtil.getFileName(); + int lineNumber = bytecodeCompiler.errorUtil.getLineNumber(node.getIndex()); + locationMsg = " at " + fileName + " line " + lineNumber; + } else { + locationMsg = " at " + bytecodeCompiler.sourceName + " line " + bytecodeCompiler.sourceLine; + } + + int locationReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_STRING); + bytecodeCompiler.emitReg(locationReg); + bytecodeCompiler.emitInt(bytecodeCompiler.addToStringPool(locationMsg)); + + bytecodeCompiler.emitWithToken(Opcodes.DIE, node.getIndex()); + bytecodeCompiler.emitReg(undefReg); + bytecodeCompiler.emitReg(locationReg); + } + bytecodeCompiler.lastResultReg = -1; // No result after die + } else if (op.equals("warn")) { + // warn $message; + if (node.operand != null) { + // Evaluate warn message + node.operand.accept(bytecodeCompiler); + int msgReg = bytecodeCompiler.lastResultReg; + + // Precompute location message at compile time + String locationMsg; + // Use annotation from AST node which has the correct line number + Object lineObj = node.getAnnotation("line"); + Object fileObj = node.getAnnotation("file"); + if (lineObj != null && fileObj != null) { + String fileName = fileObj.toString(); + int lineNumber = Integer.parseInt(lineObj.toString()); + locationMsg = " at " + fileName + " line " + lineNumber; + } else if (bytecodeCompiler.errorUtil != null) { + // Fallback to errorUtil if annotations not available + String fileName = bytecodeCompiler.errorUtil.getFileName(); + int lineNumber = bytecodeCompiler.errorUtil.getLineNumberAccurate(node.getIndex()); + locationMsg = " at " + fileName + " line " + lineNumber; + } else { + // Final fallback if neither available + locationMsg = " at " + bytecodeCompiler.sourceName + " line " + bytecodeCompiler.sourceLine; + } + + int locationReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_STRING); + bytecodeCompiler.emitReg(locationReg); + bytecodeCompiler.emit(bytecodeCompiler.addToStringPool(locationMsg)); + + // Emit WARN with both message and precomputed location + bytecodeCompiler.emitWithToken(Opcodes.WARN, node.getIndex()); + bytecodeCompiler.emitReg(msgReg); + bytecodeCompiler.emitReg(locationReg); + } else { + // warn; (no message - use $@) + int undefReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(undefReg); + + // Precompute location message for bare warn + String locationMsg; + if (bytecodeCompiler.errorUtil != null) { + String fileName = bytecodeCompiler.errorUtil.getFileName(); + int lineNumber = bytecodeCompiler.errorUtil.getLineNumber(node.getIndex()); + locationMsg = " at " + fileName + " line " + lineNumber; + } else { + locationMsg = " at " + bytecodeCompiler.sourceName + " line " + bytecodeCompiler.sourceLine; + } + + int locationReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_STRING); + bytecodeCompiler.emitReg(locationReg); + bytecodeCompiler.emitInt(bytecodeCompiler.addToStringPool(locationMsg)); + + bytecodeCompiler.emitWithToken(Opcodes.WARN, node.getIndex()); + bytecodeCompiler.emitReg(undefReg); + bytecodeCompiler.emitReg(locationReg); + } + // warn returns 1 (true) in Perl + int resultReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_INT); + bytecodeCompiler.emitReg(resultReg); + bytecodeCompiler.emitInt(1); + bytecodeCompiler.lastResultReg = resultReg; + } else if (op.equals("eval")) { + // eval $string; + if (node.operand != null) { + // Evaluate eval operand (the code string) + node.operand.accept(bytecodeCompiler); + int stringReg = bytecodeCompiler.lastResultReg; + + // Allocate register for result + int rd = bytecodeCompiler.allocateRegister(); + + // Emit direct opcode EVAL_STRING + bytecodeCompiler.emitWithToken(Opcodes.EVAL_STRING, node.getIndex()); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(stringReg); + + bytecodeCompiler.lastResultReg = rd; + } else { + // eval; (no operand - return undef) + int undefReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(undefReg); + bytecodeCompiler.lastResultReg = undefReg; + } + } else if (op.equals("select")) { + // select FILEHANDLE or select() + // SELECT is a fast opcode (used in every print statement) + // Format: [SELECT] [rd] [rs_list] + // Effect: rd = IOOperator.select(registers[rs_list], SCALAR) + + int rd = bytecodeCompiler.allocateRegister(); + + if (node.operand != null && node.operand instanceof ListNode) { + // select FILEHANDLE or select() with arguments + // Compile the operand (ListNode containing filehandle ref) + node.operand.accept(bytecodeCompiler); + int listReg = bytecodeCompiler.lastResultReg; + + // Emit SELECT opcode + bytecodeCompiler.emitWithToken(Opcodes.SELECT, node.getIndex()); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(listReg); + } else { + // select() with no arguments - returns current filehandle + // Create empty list + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + int listReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitReg(listReg); + bytecodeCompiler.emit(0); // count = 0 + + // Emit SELECT opcode + bytecodeCompiler.emitWithToken(Opcodes.SELECT, node.getIndex()); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(listReg); + } + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("undef")) { + // undef operator - returns undefined value + // Can be used standalone: undef + // Or with an operand to undef a variable: undef $x (not implemented yet) + int undefReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_UNDEF); + bytecodeCompiler.emitReg(undefReg); + bytecodeCompiler.lastResultReg = undefReg; + } else if (op.equals("unaryMinus")) { + // Unary minus: -$x + // Compile operand + node.operand.accept(bytecodeCompiler); + int operandReg = bytecodeCompiler.lastResultReg; + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit NEG_SCALAR + bytecodeCompiler.emit(Opcodes.NEG_SCALAR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(operandReg); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("pop")) { + // Array pop: $x = pop @array or $x = pop @$ref + // operand: ListNode containing OperatorNode("@", IdentifierNode or OperatorNode) + if (node.operand == null || !(node.operand instanceof ListNode)) { + bytecodeCompiler.throwCompilerException("pop requires array argument"); + } + + ListNode list = (ListNode) node.operand; + if (list.elements.isEmpty() || !(list.elements.get(0) instanceof OperatorNode)) { + bytecodeCompiler.throwCompilerException("pop requires array variable"); + } + + OperatorNode arrayOp = (OperatorNode) list.elements.get(0); + if (!arrayOp.operator.equals("@")) { + bytecodeCompiler.throwCompilerException("pop requires array variable: pop @array"); + } + + int arrayReg = -1; // Will be assigned in if/else blocks + + if (arrayOp.operand instanceof IdentifierNode) { + // pop @array + String varName = "@" + ((IdentifierNode) arrayOp.operand).name; + + // Get the array - check lexical first, then global + if (bytecodeCompiler.hasVariable(varName)) { + // Lexical array + arrayReg = bytecodeCompiler.getVariableRegister(varName); + } else { + // Global array - load it + arrayReg = bytecodeCompiler.allocateRegister(); + String globalArrayName = NameNormalizer.normalizeVariableName(((IdentifierNode) arrayOp.operand).name, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(globalArrayName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + } + } else if (arrayOp.operand instanceof OperatorNode) { + // pop @$ref - dereference first + arrayOp.operand.accept(bytecodeCompiler); + int refReg = bytecodeCompiler.lastResultReg; + + // Dereference to get the array + arrayReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(refReg); + } else { + bytecodeCompiler.throwCompilerException("pop requires array variable or dereferenced array: pop @array or pop @$ref"); + } + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit ARRAY_POP + bytecodeCompiler.emit(Opcodes.ARRAY_POP); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(arrayReg); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("shift")) { + // Array shift: $x = shift @array or $x = shift @$ref + // operand: ListNode containing OperatorNode("@", IdentifierNode or OperatorNode) + if (node.operand == null || !(node.operand instanceof ListNode)) { + bytecodeCompiler.throwCompilerException("shift requires array argument"); + } + + ListNode list = (ListNode) node.operand; + if (list.elements.isEmpty() || !(list.elements.get(0) instanceof OperatorNode)) { + bytecodeCompiler.throwCompilerException("shift requires array variable"); + } + + OperatorNode arrayOp = (OperatorNode) list.elements.get(0); + if (!arrayOp.operator.equals("@")) { + bytecodeCompiler.throwCompilerException("shift requires array variable: shift @array"); + } + + int arrayReg = -1; // Will be assigned in if/else blocks + + if (arrayOp.operand instanceof IdentifierNode) { + // shift @array + String varName = "@" + ((IdentifierNode) arrayOp.operand).name; + + // Get the array - check lexical first, then global + if (bytecodeCompiler.hasVariable(varName)) { + // Lexical array + arrayReg = bytecodeCompiler.getVariableRegister(varName); + } else { + // Global array - load it + arrayReg = bytecodeCompiler.allocateRegister(); + String globalArrayName = NameNormalizer.normalizeVariableName(((IdentifierNode) arrayOp.operand).name, bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(globalArrayName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + } + } else if (arrayOp.operand instanceof OperatorNode) { + // shift @$ref - dereference first + arrayOp.operand.accept(bytecodeCompiler); + int refReg = bytecodeCompiler.lastResultReg; + + // Dereference to get the array + arrayReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(refReg); + } else { + bytecodeCompiler.throwCompilerException("shift requires array variable or dereferenced array: shift @array or shift @$ref"); + } + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit ARRAY_SHIFT + bytecodeCompiler.emit(Opcodes.ARRAY_SHIFT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(arrayReg); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("splice")) { + // Array splice: splice @array, offset, length, @list + // operand: ListNode containing [@array, offset, length, replacement_list] + if (node.operand == null || !(node.operand instanceof ListNode)) { + bytecodeCompiler.throwCompilerException("splice requires array and arguments"); + } + + ListNode list = (ListNode) node.operand; + if (list.elements.isEmpty() || !(list.elements.get(0) instanceof OperatorNode)) { + bytecodeCompiler.throwCompilerException("splice requires array variable"); + } + + // First element is the array + OperatorNode arrayOp = (OperatorNode) list.elements.get(0); + if (!arrayOp.operator.equals("@")) { + bytecodeCompiler.throwCompilerException("splice requires array variable: splice @array, ..."); + } + + int arrayReg = -1; // Will be assigned in if/else blocks + + if (arrayOp.operand instanceof IdentifierNode) { + // splice @array + String varName = "@" + ((IdentifierNode) arrayOp.operand).name; + + // Get the array - check lexical first, then global + if (bytecodeCompiler.hasVariable(varName)) { + // Lexical array + arrayReg = bytecodeCompiler.getVariableRegister(varName); + } else { + // Global array - load it + arrayReg = bytecodeCompiler.allocateRegister(); + String globalArrayName = NameNormalizer.normalizeVariableName( + ((IdentifierNode) arrayOp.operand).name, + bytecodeCompiler.getCurrentPackage() + ); + int nameIdx = bytecodeCompiler.addToStringPool(globalArrayName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + } + } else if (arrayOp.operand instanceof OperatorNode) { + // splice @$ref - dereference first + arrayOp.operand.accept(bytecodeCompiler); + int refReg = bytecodeCompiler.lastResultReg; + + // Dereference to get the array + arrayReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(refReg); + } else { + bytecodeCompiler.throwCompilerException("splice requires array variable or dereferenced array: splice @array or splice @$ref"); + } + + // Create a list with the remaining arguments (offset, length, replacement values) + // Compile each remaining argument and collect them into a RuntimeList + List argRegs = new ArrayList<>(); + for (int i = 1; i < list.elements.size(); i++) { + list.elements.get(i).accept(bytecodeCompiler); + argRegs.add(bytecodeCompiler.lastResultReg); + } + + // Create a RuntimeList from these registers + int argsListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + bytecodeCompiler.emitReg(argsListReg); + bytecodeCompiler.emit(argRegs.size()); + for (int argReg : argRegs) { + bytecodeCompiler.emitReg(argReg); + } + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit direct opcode SPLICE + bytecodeCompiler.emit(Opcodes.SPLICE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(argsListReg); + bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); // Pass context for scalar/list conversion + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("reverse")) { + // Array/string reverse: reverse @array or reverse $string + // operand: ListNode containing arguments + if (node.operand == null || !(node.operand instanceof ListNode)) { + bytecodeCompiler.throwCompilerException("reverse requires arguments"); + } + + ListNode list = (ListNode) node.operand; + + // Compile all arguments into registers + List argRegs = new ArrayList<>(); + for (Node arg : list.elements) { + arg.accept(bytecodeCompiler); + argRegs.add(bytecodeCompiler.lastResultReg); + } + + // Create a RuntimeList from these registers + int argsListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + bytecodeCompiler.emitReg(argsListReg); + bytecodeCompiler.emit(argRegs.size()); + for (int argReg : argRegs) { + bytecodeCompiler.emitReg(argReg); + } + + // Allocate result register + int rd = bytecodeCompiler.allocateRegister(); + + // Emit direct opcode REVERSE + bytecodeCompiler.emit(Opcodes.REVERSE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argsListReg); + bytecodeCompiler.emit(RuntimeContextType.LIST); // Context + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("exists")) { + // exists $hash{key} or exists $array[index] + // operand: ListNode containing the hash/array access + if (node.operand == null || !(node.operand instanceof ListNode)) { + bytecodeCompiler.throwCompilerException("exists requires an argument"); + } + + ListNode list = (ListNode) node.operand; + if (list.elements.isEmpty()) { + bytecodeCompiler.throwCompilerException("exists requires an argument"); + } + + Node arg = list.elements.get(0); + + // Handle hash access: $hash{key} + if (arg instanceof BinaryOperatorNode && ((BinaryOperatorNode) arg).operator.equals("{")) { + BinaryOperatorNode hashAccess = (BinaryOperatorNode) arg; + + // Get hash register (need to handle $hash{key} -> %hash) + int hashReg; + if (hashAccess.left instanceof OperatorNode) { + OperatorNode leftOp = (OperatorNode) hashAccess.left; + if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode) { + // Simple: exists $hash{key} -> get %hash + String varName = ((IdentifierNode) leftOp.operand).name; + String hashVarName = "%" + varName; + + if (bytecodeCompiler.hasVariable(hashVarName)) { + // Lexical hash + hashReg = bytecodeCompiler.getVariableRegister(hashVarName); + } else { + // Global hash - load it + hashReg = bytecodeCompiler.allocateRegister(); + String globalHashName = NameNormalizer.normalizeVariableName( + varName, + bytecodeCompiler.getCurrentPackage() + ); + int nameIdx = bytecodeCompiler.addToStringPool(globalHashName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_HASH); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emit(nameIdx); + } + } else { + // Complex: dereference needed + leftOp.operand.accept(bytecodeCompiler); + int scalarReg = bytecodeCompiler.lastResultReg; + + hashReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(scalarReg); + } + } else if (hashAccess.left instanceof BinaryOperatorNode) { + // Nested: exists $hash{outer}{inner} + hashAccess.left.accept(bytecodeCompiler); + int scalarReg = bytecodeCompiler.lastResultReg; + + hashReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(scalarReg); + } else { + bytecodeCompiler.throwCompilerException("Hash access requires variable or expression on left side"); + return; + } + + // Compile key (right side contains HashLiteralNode) + int keyReg; + if (hashAccess.right instanceof HashLiteralNode) { + HashLiteralNode keyNode = (HashLiteralNode) hashAccess.right; + if (!keyNode.elements.isEmpty()) { + Node keyElement = keyNode.elements.get(0); + if (keyElement instanceof IdentifierNode) { + // Bareword key - autoquote + String keyString = ((IdentifierNode) keyElement).name; + keyReg = bytecodeCompiler.allocateRegister(); + int keyIdx = bytecodeCompiler.addToStringPool(keyString); + bytecodeCompiler.emit(Opcodes.LOAD_STRING); + bytecodeCompiler.emitReg(keyReg); + bytecodeCompiler.emit(keyIdx); + } else { + // Expression key + keyElement.accept(bytecodeCompiler); + keyReg = bytecodeCompiler.lastResultReg; + } + } else { + bytecodeCompiler.throwCompilerException("Hash key required for exists"); + return; + } + } else { + hashAccess.right.accept(bytecodeCompiler); + keyReg = bytecodeCompiler.lastResultReg; + } + + // Emit HASH_EXISTS + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.HASH_EXISTS); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(keyReg); + + bytecodeCompiler.lastResultReg = rd; + } else { + // For now, use SLOW_OP for other cases (array exists, etc.) + arg.accept(bytecodeCompiler); + int argReg = bytecodeCompiler.lastResultReg; + + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.EXISTS); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + + bytecodeCompiler.lastResultReg = rd; + } + } else if (op.equals("delete")) { + // delete $hash{key} or delete @hash{@keys} + // operand: ListNode containing the hash/array access + if (node.operand == null || !(node.operand instanceof ListNode)) { + bytecodeCompiler.throwCompilerException("delete requires an argument"); + } + + ListNode list = (ListNode) node.operand; + if (list.elements.isEmpty()) { + bytecodeCompiler.throwCompilerException("delete requires an argument"); + } + + Node arg = list.elements.get(0); + + // Handle hash access: $hash{key} or hash slice delete: delete @hash{keys} + if (arg instanceof BinaryOperatorNode && ((BinaryOperatorNode) arg).operator.equals("{")) { + BinaryOperatorNode hashAccess = (BinaryOperatorNode) arg; + + // Check if it's a hash slice delete: delete @hash{keys} + if (hashAccess.left instanceof OperatorNode) { + OperatorNode leftOp = (OperatorNode) hashAccess.left; + if (leftOp.operator.equals("@")) { + // Hash slice delete: delete @hash{'key1', 'key2'} + // Use SLOW_OP for slice delete + int hashReg; + + if (leftOp.operand instanceof IdentifierNode) { + String varName = ((IdentifierNode) leftOp.operand).name; + String hashVarName = "%" + varName; + + if (bytecodeCompiler.hasVariable(hashVarName)) { + hashReg = bytecodeCompiler.getVariableRegister(hashVarName); + } else { + hashReg = bytecodeCompiler.allocateRegister(); + String globalHashName = NameNormalizer.normalizeVariableName( + varName, + bytecodeCompiler.getCurrentPackage() + ); + int nameIdx = bytecodeCompiler.addToStringPool(globalHashName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_HASH); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emit(nameIdx); + } + } else { + bytecodeCompiler.throwCompilerException("Hash slice delete requires identifier"); + return; + } + + // Get keys from HashLiteralNode + if (!(hashAccess.right instanceof HashLiteralNode)) { + bytecodeCompiler.throwCompilerException("Hash slice delete requires HashLiteralNode"); + return; + } + HashLiteralNode keysNode = (HashLiteralNode) hashAccess.right; + + // Compile all keys + List keyRegs = new ArrayList<>(); + for (Node keyElement : keysNode.elements) { + if (keyElement instanceof IdentifierNode) { + String keyString = ((IdentifierNode) keyElement).name; + int keyReg = bytecodeCompiler.allocateRegister(); + int keyIdx = bytecodeCompiler.addToStringPool(keyString); + bytecodeCompiler.emit(Opcodes.LOAD_STRING); + bytecodeCompiler.emitReg(keyReg); + bytecodeCompiler.emit(keyIdx); + keyRegs.add(keyReg); + } else { + keyElement.accept(bytecodeCompiler); + keyRegs.add(bytecodeCompiler.lastResultReg); + } + } + + // Create RuntimeList from keys + int keysListReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CREATE_LIST); + bytecodeCompiler.emitReg(keysListReg); + bytecodeCompiler.emit(keyRegs.size()); + for (int keyReg : keyRegs) { + bytecodeCompiler.emitReg(keyReg); + } + + // Use SLOW_OP for hash slice delete + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.HASH_SLICE_DELETE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(keysListReg); + + bytecodeCompiler.lastResultReg = rd; + return; + } + } + + // Single key delete: delete $hash{key} + // Get hash register (need to handle $hash{key} -> %hash) + int hashReg; + if (hashAccess.left instanceof OperatorNode) { + OperatorNode leftOp = (OperatorNode) hashAccess.left; + if (leftOp.operator.equals("$") && leftOp.operand instanceof IdentifierNode) { + // Simple: delete $hash{key} -> get %hash + String varName = ((IdentifierNode) leftOp.operand).name; + String hashVarName = "%" + varName; + + if (bytecodeCompiler.hasVariable(hashVarName)) { + // Lexical hash + hashReg = bytecodeCompiler.getVariableRegister(hashVarName); + } else { + // Global hash - load it + hashReg = bytecodeCompiler.allocateRegister(); + String globalHashName = NameNormalizer.normalizeVariableName( + varName, + bytecodeCompiler.getCurrentPackage() + ); + int nameIdx = bytecodeCompiler.addToStringPool(globalHashName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_HASH); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emit(nameIdx); + } + } else { + // Complex: dereference needed + leftOp.operand.accept(bytecodeCompiler); + int scalarReg = bytecodeCompiler.lastResultReg; + + hashReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(scalarReg); + } + } else if (hashAccess.left instanceof BinaryOperatorNode) { + // Nested: delete $hash{outer}{inner} + hashAccess.left.accept(bytecodeCompiler); + int scalarReg = bytecodeCompiler.lastResultReg; + + hashReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_HASH, node.getIndex()); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(scalarReg); + } else { + bytecodeCompiler.throwCompilerException("Hash access requires variable or expression on left side"); + return; + } + + // Compile key (right side contains HashLiteralNode) + int keyReg; + if (hashAccess.right instanceof HashLiteralNode) { + HashLiteralNode keyNode = (HashLiteralNode) hashAccess.right; + if (!keyNode.elements.isEmpty()) { + Node keyElement = keyNode.elements.get(0); + if (keyElement instanceof IdentifierNode) { + // Bareword key - autoquote + String keyString = ((IdentifierNode) keyElement).name; + keyReg = bytecodeCompiler.allocateRegister(); + int keyIdx = bytecodeCompiler.addToStringPool(keyString); + bytecodeCompiler.emit(Opcodes.LOAD_STRING); + bytecodeCompiler.emitReg(keyReg); + bytecodeCompiler.emit(keyIdx); + } else { + // Expression key + keyElement.accept(bytecodeCompiler); + keyReg = bytecodeCompiler.lastResultReg; + } + } else { + bytecodeCompiler.throwCompilerException("Hash key required for delete"); + return; + } + } else { + hashAccess.right.accept(bytecodeCompiler); + keyReg = bytecodeCompiler.lastResultReg; + } + + // Emit HASH_DELETE + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.HASH_DELETE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(hashReg); + bytecodeCompiler.emitReg(keyReg); + + bytecodeCompiler.lastResultReg = rd; + } else { + // For now, use SLOW_OP for other cases (hash slice delete, array delete, etc.) + arg.accept(bytecodeCompiler); + int argReg = bytecodeCompiler.lastResultReg; + + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.DELETE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + + bytecodeCompiler.lastResultReg = rd; + } + } else if (op.equals("keys")) { + // keys %hash + // operand: hash variable (OperatorNode("%" ...) or other expression) + if (node.operand == null) { + bytecodeCompiler.throwCompilerException("keys requires a hash argument"); + } + + // Compile the hash operand + node.operand.accept(bytecodeCompiler); + int hashReg = bytecodeCompiler.lastResultReg; + + // Emit HASH_KEYS + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.HASH_KEYS); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(hashReg); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("values")) { + // values %hash + // operand: hash variable (OperatorNode("%" ...) or other expression) + if (node.operand == null) { + bytecodeCompiler.throwCompilerException("values requires a hash argument"); + } + + // Compile the hash operand + node.operand.accept(bytecodeCompiler); + int hashReg = bytecodeCompiler.lastResultReg; + + // Emit HASH_VALUES + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.HASH_VALUES); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(hashReg); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("$#")) { + // $#array - get last index of array (size - 1) + // operand: array variable (OperatorNode("@" ...) or IdentifierNode) + if (node.operand == null) { + bytecodeCompiler.throwCompilerException("$# requires an array argument"); + } + + int arrayReg = -1; + + // Handle different operand types + if (node.operand instanceof OperatorNode) { + OperatorNode operandOp = (OperatorNode) node.operand; + + if (operandOp.operator.equals("@") && operandOp.operand instanceof IdentifierNode) { + // $#@array or $#array (both work) + String varName = "@" + ((IdentifierNode) operandOp.operand).name; + + if (bytecodeCompiler.hasVariable(varName)) { + arrayReg = bytecodeCompiler.getVariableRegister(varName); + } else { + arrayReg = bytecodeCompiler.allocateRegister(); + String globalArrayName = NameNormalizer.normalizeVariableName( + ((IdentifierNode) operandOp.operand).name, + bytecodeCompiler.getCurrentPackage() + ); + int nameIdx = bytecodeCompiler.addToStringPool(globalArrayName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + } + } else if (operandOp.operator.equals("$")) { + // $#$ref - dereference first + operandOp.accept(bytecodeCompiler); + int refReg = bytecodeCompiler.lastResultReg; + + arrayReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emitWithToken(Opcodes.DEREF_ARRAY, node.getIndex()); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emitReg(refReg); + } else { + bytecodeCompiler.throwCompilerException("$# requires array variable or dereferenced array"); + } + } else if (node.operand instanceof IdentifierNode) { + // $#array (without @) + String varName = "@" + ((IdentifierNode) node.operand).name; + + if (bytecodeCompiler.hasVariable(varName)) { + arrayReg = bytecodeCompiler.getVariableRegister(varName); + } else { + arrayReg = bytecodeCompiler.allocateRegister(); + String globalArrayName = NameNormalizer.normalizeVariableName( + ((IdentifierNode) node.operand).name, + bytecodeCompiler.getCurrentPackage() + ); + int nameIdx = bytecodeCompiler.addToStringPool(globalArrayName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_ARRAY); + bytecodeCompiler.emitReg(arrayReg); + bytecodeCompiler.emit(nameIdx); + } + } else { + bytecodeCompiler.throwCompilerException("$# requires array variable"); + } + + // Get array size + int sizeReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_SIZE); + bytecodeCompiler.emitReg(sizeReg); + bytecodeCompiler.emitReg(arrayReg); + + // Subtract 1 to get last index + int oneReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOAD_INT); + bytecodeCompiler.emitReg(oneReg); + bytecodeCompiler.emitInt(1); + + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.SUB_SCALAR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(sizeReg); + bytecodeCompiler.emitReg(oneReg); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("length")) { + // length($string) - get string length + // operand: ListNode containing the string argument + if (node.operand == null) { + bytecodeCompiler.throwCompilerException("length requires an argument"); + } + + // Compile the operand + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (list.elements.isEmpty()) { + bytecodeCompiler.throwCompilerException("length requires an argument"); + } + // Get first element + list.elements.get(0).accept(bytecodeCompiler); + } else { + node.operand.accept(bytecodeCompiler); + } + int stringReg = bytecodeCompiler.lastResultReg; + + // Call length builtin using SLOW_OP + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LENGTH_OP); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(stringReg); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("open")) { + // open(filehandle, mode, filename) or open(filehandle, expr) + if (node.operand == null || !(node.operand instanceof ListNode)) { + bytecodeCompiler.throwCompilerException("open requires arguments"); + } + + ListNode argsList = (ListNode) node.operand; + if (argsList.elements.isEmpty()) { + bytecodeCompiler.throwCompilerException("open requires arguments"); + } + + // Compile all arguments into a list + int argsReg = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.NEW_ARRAY); + bytecodeCompiler.emitReg(argsReg); + + for (Node arg : argsList.elements) { + arg.accept(bytecodeCompiler); + int elemReg = bytecodeCompiler.lastResultReg; + + bytecodeCompiler.emit(Opcodes.ARRAY_PUSH); + bytecodeCompiler.emitReg(argsReg); + bytecodeCompiler.emitReg(elemReg); + } + + // Call open with context and args + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.OPEN); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emit(bytecodeCompiler.currentCallContext); + bytecodeCompiler.emitReg(argsReg); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("matchRegex")) { + // m/pattern/flags - create a regex and return it (for use with =~) + // operand: ListNode containing pattern string and flags string + if (node.operand == null || !(node.operand instanceof ListNode)) { + bytecodeCompiler.throwCompilerException("matchRegex requires pattern and flags"); + } + + ListNode args = (ListNode) node.operand; + if (args.elements.size() < 2) { + bytecodeCompiler.throwCompilerException("matchRegex requires pattern and flags"); + } + + // Compile pattern + args.elements.get(0).accept(bytecodeCompiler); + int patternReg = bytecodeCompiler.lastResultReg; + + // Compile flags + args.elements.get(1).accept(bytecodeCompiler); + int flagsReg = bytecodeCompiler.lastResultReg; + + // Create quoted regex using QUOTE_REGEX opcode + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.QUOTE_REGEX); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(patternReg); + bytecodeCompiler.emitReg(flagsReg); + + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("chomp")) { + // chomp($x) or chomp - remove trailing newlines + if (node.operand == null) { + // chomp with no args - operates on $_ + String varName = "$_"; + int targetReg; + if (bytecodeCompiler.hasVariable(varName)) { + targetReg = bytecodeCompiler.getVariableRegister(varName); + } else { + targetReg = bytecodeCompiler.allocateRegister(); + int nameIdx = bytecodeCompiler.addToStringPool("main::_"); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_SCALAR); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emit(nameIdx); + } + + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CHOMP); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(targetReg); + + bytecodeCompiler.lastResultReg = rd; + } else { + // chomp with argument + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("chomp requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int targetReg = bytecodeCompiler.lastResultReg; + + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CHOMP); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(targetReg); + + bytecodeCompiler.lastResultReg = rd; + } + } else if (op.equals("+")) { + // Unary + operator: forces numeric context on its operand + // For arrays/hashes in scalar context, this returns the size + // For scalars, this ensures the value is numeric + if (node.operand != null) { + // Evaluate operand in scalar context + int savedContext = bytecodeCompiler.currentCallContext; + bytecodeCompiler.currentCallContext = RuntimeContextType.SCALAR; + try { + node.operand.accept(bytecodeCompiler); + int operandReg = bytecodeCompiler.lastResultReg; + + // Emit ARRAY_SIZE to convert to scalar + // This handles arrays/hashes (converts to size) and passes through scalars + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ARRAY_SIZE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(operandReg); + + bytecodeCompiler.lastResultReg = rd; + } finally { + bytecodeCompiler.currentCallContext = savedContext; + } + } else { + bytecodeCompiler.throwCompilerException("unary + operator requires an operand"); + } + } else if (op.equals("wantarray")) { + // wantarray operator: returns undef in VOID, false in SCALAR, true in LIST + // Read register 2 (wantarray context) and convert to Perl convention + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.WANTARRAY); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(2); // Register 2 contains the calling context + + bytecodeCompiler.lastResultReg = rd; + // GENERATED_OPERATORS_START + } else if (op.equals("int")) { + // int($x) - MathOperators.integer + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("int requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.INT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("log")) { + // log($x) - MathOperators.log + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("log requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LOG); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("sqrt")) { + // sqrt($x) - MathOperators.sqrt + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("sqrt requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.SQRT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("cos")) { + // cos($x) - MathOperators.cos + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("cos requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.COS); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("sin")) { + // sin($x) - MathOperators.sin + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("sin requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.SIN); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("exp")) { + // exp($x) - MathOperators.exp + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("exp requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.EXP); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("abs")) { + // abs($x) - MathOperators.abs + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("abs requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ABS); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("binary~")) { + // binary~($x) - BitwiseOperators.bitwiseNotBinary + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("binary~ requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.BINARY_NOT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("integerBitwiseNot")) { + // integerBitwiseNot($x) - BitwiseOperators.integerBitwiseNot + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("integerBitwiseNot requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.INTEGER_BITWISE_NOT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("ord")) { + // ord($x) - ScalarOperators.ord + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("ord requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ORD); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("ordBytes")) { + // ordBytes($x) - ScalarOperators.ordBytes + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("ordBytes requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.ORD_BYTES); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("oct")) { + // oct($x) - ScalarOperators.oct + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("oct requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.OCT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("hex")) { + // hex($x) - ScalarOperators.hex + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("hex requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.HEX); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("srand")) { + // srand($x) - Random.srand + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("srand requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.SRAND); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("chr")) { + // chr($x) - StringOperators.chr + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("chr requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CHR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("chrBytes")) { + // chrBytes($x) - StringOperators.chrBytes + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("chrBytes requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CHR_BYTES); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("lengthBytes")) { + // lengthBytes($x) - StringOperators.lengthBytes + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("lengthBytes requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LENGTH_BYTES); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("quotemeta")) { + // quotemeta($x) - StringOperators.quotemeta + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("quotemeta requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.QUOTEMETA); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("fc")) { + // fc($x) - StringOperators.fc + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("fc requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.FC); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("lc")) { + // lc($x) - StringOperators.lc + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("lc requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LC); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("lcfirst")) { + // lcfirst($x) - StringOperators.lcfirst + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("lcfirst requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.LCFIRST); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("uc")) { + // uc($x) - StringOperators.uc + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("uc requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.UC); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("ucfirst")) { + // ucfirst($x) - StringOperators.ucfirst + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("ucfirst requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.UCFIRST); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("sleep")) { + // sleep($x) - Time.sleep + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("sleep requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.SLEEP); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("tell")) { + // tell($x) - IOOperator.tell + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("tell requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.TELL); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("rmdir")) { + // rmdir($x) - Directory.rmdir + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("rmdir requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.RMDIR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("closedir")) { + // closedir($x) - Directory.closedir + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("closedir requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CLOSEDIR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("rewinddir")) { + // rewinddir($x) - Directory.rewinddir + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("rewinddir requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.REWINDDIR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("telldir")) { + // telldir($x) - Directory.telldir + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("telldir requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.TELLDIR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("chdir")) { + // chdir($x) - Directory.chdir + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("chdir requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.CHDIR); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.lastResultReg = rd; + } else if (op.equals("exit")) { + // exit($x) - WarnDie.exit + if (node.operand instanceof ListNode) { + ListNode list = (ListNode) node.operand; + if (!list.elements.isEmpty()) { + list.elements.get(0).accept(bytecodeCompiler); + } else { + bytecodeCompiler.throwCompilerException("exit requires an argument"); + } + } else { + node.operand.accept(bytecodeCompiler); + } + int argReg = bytecodeCompiler.lastResultReg; + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.EXIT); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(argReg); + bytecodeCompiler.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)) { + bytecodeCompiler.throwCompilerException("tr operator requires list operand"); + } + + ListNode list = (ListNode) node.operand; + if (list.elements.size() < 3) { + bytecodeCompiler.throwCompilerException("tr operator requires search, replace, and modifiers"); + } + + // Compile search pattern + list.elements.get(0).accept(bytecodeCompiler); + int searchReg = bytecodeCompiler.lastResultReg; + + // Compile replace pattern + list.elements.get(1).accept(bytecodeCompiler); + int replaceReg = bytecodeCompiler.lastResultReg; + + // Compile modifiers + list.elements.get(2).accept(bytecodeCompiler); + int modifiersReg = bytecodeCompiler.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(bytecodeCompiler); + targetReg = bytecodeCompiler.lastResultReg; + } else { + // Default to $_ - need to load it + targetReg = bytecodeCompiler.allocateRegister(); + String underscoreName = NameNormalizer.normalizeVariableName("_", bytecodeCompiler.getCurrentPackage()); + int nameIdx = bytecodeCompiler.addToStringPool(underscoreName); + bytecodeCompiler.emit(Opcodes.LOAD_GLOBAL_SCALAR); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emit(nameIdx); + } + + // Emit TR_TRANSLITERATE operation + int rd = bytecodeCompiler.allocateRegister(); + bytecodeCompiler.emit(Opcodes.TR_TRANSLITERATE); + bytecodeCompiler.emitReg(rd); + bytecodeCompiler.emitReg(searchReg); + bytecodeCompiler.emitReg(replaceReg); + bytecodeCompiler.emitReg(modifiersReg); + bytecodeCompiler.emitReg(targetReg); + bytecodeCompiler.emitInt(bytecodeCompiler.currentCallContext); + + bytecodeCompiler.lastResultReg = rd; + } else { + bytecodeCompiler.throwCompilerException("Unsupported operator: " + op); + } + } +}