diff --git a/dev/interpreter/OPCODES_ARCHITECTURE.md b/dev/interpreter/OPCODES_ARCHITECTURE.md new file mode 100644 index 000000000..d30b2d051 --- /dev/null +++ b/dev/interpreter/OPCODES_ARCHITECTURE.md @@ -0,0 +1,91 @@ +# Interpreter Opcodes Architecture + +## Two-Level Dispatch Design + +The interpreter uses a two-level dispatch strategy for optimal performance: + +``` +Fast Opcodes (0-92) → Main switch in BytecodeInterpreter +Slow Opcodes (SLOW_OP + id) → SlowOpcodeHandler.handleSlowOp() +``` + +### Why Two-Level Dispatch? + +**Decision Date**: 2026-02-13 + +With short[] bytecode, we have 65,536 opcodes available. We could unify all opcodes into a single flat range (0-127), but we deliberately chose to keep the two-level design. + +### Performance Rationale + +1. **CPU Instruction Cache (i-cache) Locality** + - Hot path (fast opcodes): ~1KB of code, stays in L1 i-cache + - Cold path (slow opcodes): Separate method, doesn't pollute hot path + - Better cache efficiency for frequently-executed operations + +2. **JVM Optimization** + - Both designs use tableswitch (O(1) jump table) + - Two-level overhead: ~1-2 cycles per slow op + - Negligible cost (<1% based on analysis) + +3. **Code Organization** + - Clear separation: hot vs cold operations + - Easy to identify performance-critical paths + - Proven pattern: Lua 5.x, Python 2.x, Ruby YARV + +### When to Use Each + +**Fast Opcodes (0-92)**: Frequently executed operations +- Arithmetic: ADD_SCALAR, SUB_SCALAR, MUL_SCALAR +- Data structures: ARRAY_GET, ARRAY_SET, HASH_GET, HASH_SET +- Control flow: GOTO, GOTO_IF_FALSE, RETURN +- Common operators: CONCAT, NOT, AND, OR + +**Slow Opcodes (SLOW_OP + id)**: Rarely used operations +- System calls: SLOWOP_SYSCALL, SLOWOP_FORK +- IPC: SLOWOP_SEMGET, SLOWOP_SHMREAD +- Complex operations: SLOWOP_EVAL_STRING, SLOWOP_SPLICE +- Specialized features: SLOWOP_LOAD_GLOB, SLOWOP_LOCAL_SCALAR + +### Promoting Slow to Fast + +If profiling shows a slow opcode is frequently executed, it can be promoted: + +1. Add new fast opcode in dense range (e.g., `public static final byte OP_NAME = 93;`) +2. Implement in main BytecodeInterpreter switch +3. Update BytecodeCompiler to emit fast opcode +4. Remove from SlowOpcodeHandler + +**Example**: If `SLOWOP_SPLICE` became hot, we could: +```java +// In Opcodes.java +public static final byte SPLICE = 93; // Promoted from SLOWOP_SPLICE + +// In BytecodeInterpreter.java +case Opcodes.SPLICE: { + // Implementation here (moved from SlowOpcodeHandler) +} +``` + +### Benchmarking Results + +**Theoretical analysis** (2026-02-13): +- Two-level dispatch: 2-3 cycles per slow op +- Single-level dispatch: 1-2 cycles per op +- Difference: ~1 cycle (negligible) +- Benefit: Better i-cache utilization for hot path + +**Empirical validation**: Not yet benchmarked (expected <1% difference) + +### Alternative Considered + +**Unified single-level dispatch**: All opcodes in range 0-127 +- **Pros**: Simpler architecture, one less branch +- **Cons**: Mixes hot/cold code, worse i-cache locality +- **Decision**: Rejected in favor of current design + +### References + +- Plan: `/Users/fglock/.claude/plans/glimmering-marinating-frost.md` (Part 1) +- Implementation: `src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java` +- Slow opcodes: `src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java` +- Opcode constants: `src/main/java/org/perlonjava/interpreter/Opcodes.java` diff --git a/docs/about/changelog.md b/docs/about/changelog.md index ea9e568db..cdeb546c9 100644 --- a/docs/about/changelog.md +++ b/docs/about/changelog.md @@ -14,8 +14,9 @@ Release history of PerlOnJava. See [Roadmap](roadmap.md) for future plans. - Optimization: faster type resolution in Perl scalars. - Optimization: `make` now runs tests in parallel. - Optimization: A workaround is implemented to Java 64k bytes segment limit. -- New command line option: `--interpreter` to run PerlOnJava as an interpreter instead of JVM compiler. - - `./jperl --interpreter --disassemble -e 'print "Hello, World!\n"'` +- New `interpreter` backend. + - New command line option: `--interpreter` to run PerlOnJava as an interpreter instead of JVM compiler. + - `./jperl --interpreter --disassemble -e 'print "Hello, World!\n"'` - The interpreter mode excels at dynamic eval STRING operations (46x faster than compilation for unique strings, matching Perl 5 performance). For general code, it runs only 15% slower than Perl 5. It is also useful for implementing debugging, handling "Method too large" errors, and enabling Android and GraalVM compatibility. - Planned release date: 2026-02-10. diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java index 78cf4db9e..45e059abc 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeCompiler.java @@ -520,6 +520,102 @@ public void visit(IdentifierNode node) { } } + /** + * Handle hash slice operations: @hash{keys} or @$hashref{keys} + * Must be called before automatic operand compilation to avoid compiling @ operator + */ + private void handleHashSlice(BinaryOperatorNode node, OperatorNode leftOp) { + int hashReg; + + // Hash slice: @hash{'key1', 'key2'} returns array of values + // Hashref slice: @$hashref{'key1', 'key2'} dereferences then slices + + if (leftOp.operand instanceof IdentifierNode) { + // Direct hash slice: @hash{keys} + String varName = ((IdentifierNode) leftOp.operand).name; + String hashVarName = "%" + varName; + + // 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); + emit(nameIdx); + } + } else if (leftOp.operand instanceof OperatorNode) { + // Hashref slice: @$hashref{keys} + // Compile the reference and dereference it + leftOp.operand.accept(this); + int scalarRefReg = lastResultReg; + + // Dereference to get the hash + hashReg = allocateRegister(); + emitWithToken(Opcodes.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_DEREF_HASH); + emitReg(hashReg); + emitReg(scalarRefReg); + } else { + throwCompilerException("Hash slice requires hash variable or reference"); + return; + } + + // Get the keys from HashLiteralNode + if (!(node.right instanceof HashLiteralNode)) { + throwCompilerException("Hash slice requires HashLiteralNode"); + } + HashLiteralNode keysNode = (HashLiteralNode) node.right; + if (keysNode.elements.isEmpty()) { + throwCompilerException("Hash slice requires at least one key"); + } + + // 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); + } + + // Emit SLOW_OP with SLOWOP_HASH_SLICE + int rdSlice = allocateRegister(); + emit(Opcodes.SLOW_OP); + emit(Opcodes.SLOWOP_HASH_SLICE); + emitReg(rdSlice); + emitReg(hashReg); + emitReg(keysListReg); + + lastResultReg = rdSlice; + } + @Override public void visit(BinaryOperatorNode node) { // Track token index for error reporting @@ -1098,6 +1194,183 @@ public void visit(BinaryOperatorNode node) { 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; + + // 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); + 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 SLOW_OP with SLOWOP_HASH_SLICE_SET + emit(Opcodes.SLOW_OP); + emit(Opcodes.SLOWOP_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; + } + 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); + emit(Opcodes.LOAD_GLOBAL_HASH); + emitReg(hashReg); + emit(nameIdx); + } + } else { + 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(this); + int scalarReg = lastResultReg; + + // Dereference to get the hash (with autovivification) + hashReg = allocateRegister(); + emitWithToken(Opcodes.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_DEREF_HASH); + 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; + } + + // 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; + } + + // 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; } throwCompilerException("Assignment to non-identifier not yet supported: " + node.left.getClass().getSimpleName()); @@ -1112,6 +1385,114 @@ public void visit(BinaryOperatorNode node) { } } + // Handle -> operator specially for hashref/arrayref dereference + if (node.operator.equals("->")) { + 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(this); + int scalarRefReg = lastResultReg; + + // Dereference the scalar to get the actual hash + int hashReg = allocateRegister(); + emitWithToken(Opcodes.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_DEREF_HASH); + emitReg(hashReg); + emitReg(scalarRefReg); + + // Get the key + HashLiteralNode keyNode = (HashLiteralNode) node.right; + if (keyNode.elements.isEmpty()) { + 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 = allocateRegister(); + int keyIdx = addToStringPool(keyString); + emit(Opcodes.LOAD_STRING); + emitReg(keyReg); + emit(keyIdx); + } else { + // Expression key: $ref->{$var} + keyElement.accept(this); + keyReg = lastResultReg; + } + + // Access hash element + int rd = allocateRegister(); + emit(Opcodes.HASH_GET); + emitReg(rd); + emitReg(hashReg); + emitReg(keyReg); + + 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(this); + int scalarRefReg = lastResultReg; + + // Dereference the scalar to get the actual array + int arrayReg = allocateRegister(); + emitWithToken(Opcodes.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_DEREF_ARRAY); + emitReg(arrayReg); + emitReg(scalarRefReg); + + // Get the index + ArrayLiteralNode indexNode = (ArrayLiteralNode) node.right; + if (indexNode.elements.isEmpty()) { + throwCompilerException("Array dereference requires index"); + } + + // Compile the index expression + indexNode.elements.get(0).accept(this); + int indexReg = lastResultReg; + + // Access array element + int rd = allocateRegister(); + emit(Opcodes.ARRAY_GET); + emitReg(rd); + emitReg(arrayReg); + emitReg(indexReg); + + lastResultReg = rd; + return; + } + // Otherwise, fall through to normal -> handling (method call) + } + + // 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; + } + } + // Otherwise, fall through to normal {} handling after operand compilation + } + // Compile left and right operands node.left.accept(this); int rs1 = lastResultReg; @@ -1518,68 +1899,200 @@ public void visit(BinaryOperatorNode node) { } case "{" -> { // Hash element access: $h{key} means get element 'key' from hash %h - // left: OperatorNode("$", IdentifierNode("h")) + // Hash slice access: @h{keys} returns multiple values as array + // left: OperatorNode("$", IdentifierNode("h")) or OperatorNode("@", ...) // right: HashLiteralNode(key_expression) - if (!(node.left instanceof OperatorNode)) { - throw new RuntimeException("Hash access requires variable on left side"); - } - OperatorNode leftOp = (OperatorNode) node.left; - if (!leftOp.operator.equals("$") || !(leftOp.operand instanceof IdentifierNode)) { - throw new RuntimeException("Hash access requires scalar dereference: $var{key}"); - } - - String varName = ((IdentifierNode) leftOp.operand).name; - String hashVarName = "%" + varName; + currentTokenIndex = node.getIndex(); // Track token for error reporting - // Get the hash - check lexical first, then global int hashReg; - if (hasVariable(hashVarName)) { - // Lexical hash - hashReg = getVariableRegister(hashVarName); - } else { - // Global hash - load it - hashReg = allocateRegister(); - String globalHashName = "main::" + varName; - int nameIdx = addToStringPool(globalHashName); - emit(Opcodes.LOAD_GLOBAL_HASH); + + // Determine if this is a simple hash access or nested/ref access + if (node.left instanceof OperatorNode) { + OperatorNode leftOp = (OperatorNode) node.left; + + // Check for hash slice: @hash{keys} or hashref slice: @$hashref{keys} + if (leftOp.operator.equals("@")) { + // Hash slice: @hash{'key1', 'key2'} returns array of values + // Hashref slice: @$hashref{'key1', 'key2'} dereferences then slices + + if (leftOp.operand instanceof IdentifierNode) { + // Direct hash slice: @hash{keys} + String varName = ((IdentifierNode) leftOp.operand).name; + String hashVarName = "%" + varName; + + // 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); + emit(nameIdx); + } + } else if (leftOp.operand instanceof OperatorNode) { + // Hashref slice: @$hashref{keys} + // DEBUG: Check if we reach this code + System.err.println("DEBUG: Compiling hashref slice @$ref{keys}"); + + // Compile the reference and dereference it + leftOp.operand.accept(this); + int scalarRefReg = lastResultReg; + + // Dereference to get the hash + hashReg = allocateRegister(); + emitWithToken(Opcodes.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_DEREF_HASH); + emitReg(hashReg); + emitReg(scalarRefReg); + } else { + throwCompilerException("Hash slice requires hash variable or reference"); + return; + } + + // Get the keys from HashLiteralNode + if (!(node.right instanceof HashLiteralNode)) { + throwCompilerException("Hash slice requires HashLiteralNode"); + } + HashLiteralNode keysNode = (HashLiteralNode) node.right; + if (keysNode.elements.isEmpty()) { + throwCompilerException("Hash slice requires at least one key"); + } + + // 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); + } + + // Emit SLOW_OP with SLOWOP_HASH_SLICE + int rdSlice = allocateRegister(); + emit(Opcodes.SLOW_OP); + emit(Opcodes.SLOWOP_HASH_SLICE); + emitReg(rdSlice); emitReg(hashReg); - emit(nameIdx); - } + emitReg(keysListReg); - // Evaluate key expression - // For HashLiteralNode, get the first element (should be the key) - if (!(node.right instanceof HashLiteralNode)) { - throw new RuntimeException("Hash access requires HashLiteralNode on right side"); + lastResultReg = rdSlice; + return; } - HashLiteralNode keyNode = (HashLiteralNode) node.right; - if (keyNode.elements.isEmpty()) { - throw new RuntimeException("Hash access requires key expression"); + + // Handle scalar hash access: $hash{key} or $hash{outer}{inner} + if (!leftOp.operator.equals("$")) { + throwCompilerException("Hash access requires $ or @ sigil"); } - // Compile the key expression - // Special case: bareword identifiers should be treated as string literals - int keyReg; - Node keyElement = keyNode.elements.get(0); - if (keyElement instanceof IdentifierNode) { - // Bareword key - treat as string literal - String keyStr = ((IdentifierNode) keyElement).name; - keyReg = allocateRegister(); - int strIdx = addToStringPool(keyStr); - emit(Opcodes.LOAD_STRING); - emitReg(keyReg); - emit(strIdx); + // Check if it's simple ($var) or nested ($expr{key}) + if (leftOp.operand instanceof IdentifierNode) { + // Simple: $hash{key} + String varName = ((IdentifierNode) leftOp.operand).name; + String hashVarName = "%" + varName; + + // 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 = "main::" + varName; + int nameIdx = addToStringPool(globalHashName); + emit(Opcodes.LOAD_GLOBAL_HASH); + emitReg(hashReg); + emit(nameIdx); + } } else { - // Expression key - evaluate normally - keyElement.accept(this); - keyReg = lastResultReg; + // Nested or complex: $hash{outer}{inner} or $func()->{key} + // Compile the left side (returns a scalar containing hash reference) + leftOp.operand.accept(this); + int scalarReg = lastResultReg; + + // Dereference to get the hash + hashReg = allocateRegister(); + emitWithToken(Opcodes.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_DEREF_HASH); + emitReg(hashReg); + emitReg(scalarReg); } + } else if (node.left instanceof BinaryOperatorNode) { + // Nested access where left is already a hash access + // e.g., $hash{outer}{inner} - the outer "{" is evaluated + node.left.accept(this); + int scalarReg = lastResultReg; - // Emit HASH_GET - emit(Opcodes.HASH_GET); - emitReg(rd); + // Dereference to get the hash + hashReg = allocateRegister(); + emitWithToken(Opcodes.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_DEREF_HASH); emitReg(hashReg); + emitReg(scalarReg); + } else { + throwCompilerException("Hash access requires variable or expression on left side"); + return; + } + + // Common code for all scalar hash access paths + // Evaluate key expression + if (!(node.right instanceof HashLiteralNode)) { + throwCompilerException("Hash access requires HashLiteralNode on right side"); + } + HashLiteralNode keyNode = (HashLiteralNode) node.right; + if (keyNode.elements.isEmpty()) { + throwCompilerException("Hash access requires key expression"); + } + + // Compile the key expression + // Special case: bareword identifiers should be treated as string literals + int keyReg; + Node keyElement = keyNode.elements.get(0); + if (keyElement instanceof IdentifierNode) { + // Bareword key - treat as string literal + String keyStr = ((IdentifierNode) keyElement).name; + keyReg = allocateRegister(); + int strIdx = addToStringPool(keyStr); + emit(Opcodes.LOAD_STRING); emitReg(keyReg); + emit(strIdx); + } else { + // Expression key - evaluate normally + keyElement.accept(this); + keyReg = lastResultReg; + } + + // Emit HASH_GET + emit(Opcodes.HASH_GET); + emitReg(rd); + emitReg(hashReg); + emitReg(keyReg); } case "push" -> { // Array push: push(@array, values...) @@ -2038,6 +2551,23 @@ public void visit(OperatorNode node) { } else { throwCompilerException("scalar operator requires an operand"); } + } else if (op.equals("!")) { + // Logical NOT: !expr + if (node.operand == null) { + throwCompilerException("! operator requires an operand"); + } + + // Compile the operand + node.operand.accept(this); + int operandReg = lastResultReg; + + // Emit NOT opcode + int rd = allocateRegister(); + emit(Opcodes.NOT); + emitReg(rd); + emitReg(operandReg); + + lastResultReg = rd; } else if (op.equals("%")) { // Hash variable dereference: %x if (node.operand instanceof IdentifierNode) { @@ -2641,6 +3171,353 @@ public void visit(OperatorNode node) { 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.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_DEREF_HASH); + 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.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_DEREF_HASH); + 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.SLOW_OP); + emit(Opcodes.SLOWOP_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.SLOW_OP); + emit(Opcodes.SLOWOP_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.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_DEREF_HASH); + 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.SLOW_OP, node.getIndex()); + emit(Opcodes.SLOWOP_DEREF_HASH); + 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.SLOW_OP); + emit(Opcodes.SLOWOP_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 { throwCompilerException("Unsupported operator: " + op); diff --git a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java index eb2100764..6a4d3bc01 100644 --- a/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/interpreter/BytecodeInterpreter.java @@ -608,6 +608,46 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c break; } + case Opcodes.HASH_EXISTS: { + // Check if hash key exists: rd = exists $hash{key} + int rd = bytecode[pc++]; + int hashReg = bytecode[pc++]; + int keyReg = bytecode[pc++]; + RuntimeHash hash = (RuntimeHash) registers[hashReg]; + RuntimeScalar key = (RuntimeScalar) registers[keyReg]; + registers[rd] = hash.exists(key); + break; + } + + case Opcodes.HASH_DELETE: { + // Delete hash key: rd = delete $hash{key} + int rd = bytecode[pc++]; + int hashReg = bytecode[pc++]; + int keyReg = bytecode[pc++]; + RuntimeHash hash = (RuntimeHash) registers[hashReg]; + RuntimeScalar key = (RuntimeScalar) registers[keyReg]; + registers[rd] = hash.delete(key); + break; + } + + case Opcodes.HASH_KEYS: { + // Get hash keys: rd = keys %hash + int rd = bytecode[pc++]; + int hashReg = bytecode[pc++]; + RuntimeHash hash = (RuntimeHash) registers[hashReg]; + registers[rd] = hash.keys(); + break; + } + + case Opcodes.HASH_VALUES: { + // Get hash values: rd = values %hash + int rd = bytecode[pc++]; + int hashReg = bytecode[pc++]; + RuntimeHash hash = (RuntimeHash) registers[hashReg]; + registers[rd] = hash.values(); + break; + } + // ================================================================= // SUBROUTINE CALLS // ================================================================= diff --git a/src/main/java/org/perlonjava/interpreter/Opcodes.java b/src/main/java/org/perlonjava/interpreter/Opcodes.java index 8a1b48459..07436f4ac 100644 --- a/src/main/java/org/perlonjava/interpreter/Opcodes.java +++ b/src/main/java/org/perlonjava/interpreter/Opcodes.java @@ -548,6 +548,24 @@ public class Opcodes { /** Slow op ID: rd = Operator.split(pattern, args, ctx) - split string into array */ public static final int SLOWOP_SPLIT = 32; + /** Slow opcode for exists operator (fallback) */ + public static final int SLOWOP_EXISTS = 33; + + /** Slow opcode for delete operator (fallback) */ + public static final int SLOWOP_DELETE = 34; + + /** Slow op ID: rd = deref_hash(scalar_ref) - dereference hash reference for hashref access */ + public static final int SLOWOP_DEREF_HASH = 35; + + /** Slow op ID: rd = hash.getSlice(keys_list) - hash slice operation @hash{keys} */ + public static final int SLOWOP_HASH_SLICE = 36; + + /** Slow op ID: rd = hash.deleteSlice(keys_list) - hash slice delete operation delete @hash{keys} */ + public static final int SLOWOP_HASH_SLICE_DELETE = 37; + + /** Slow op ID: hash.setSlice(keys_list, values_list) - hash slice assignment @hash{keys} = values */ + public static final int SLOWOP_HASH_SLICE_SET = 38; + // ================================================================= // OPCODES 93-255: RESERVED FOR FUTURE FAST OPERATIONS // ================================================================= diff --git a/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java b/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java index d0ba71a90..a4d81b63a 100644 --- a/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java +++ b/src/main/java/org/perlonjava/interpreter/SlowOpcodeHandler.java @@ -178,6 +178,24 @@ public static int execute( case Opcodes.SLOWOP_SPLIT: return executeSplit(bytecode, pc, registers); + case Opcodes.SLOWOP_EXISTS: + return executeExists(bytecode, pc, registers); + + case Opcodes.SLOWOP_DELETE: + return executeDelete(bytecode, pc, registers); + + case Opcodes.SLOWOP_DEREF_HASH: + return executeDerefHash(bytecode, pc, registers); + + case Opcodes.SLOWOP_HASH_SLICE: + return executeHashSlice(bytecode, pc, registers); + + case Opcodes.SLOWOP_HASH_SLICE_DELETE: + return executeHashSliceDelete(bytecode, pc, registers); + + case Opcodes.SLOWOP_HASH_SLICE_SET: + return executeHashSliceSet(bytecode, pc, registers); + default: throw new RuntimeException( "Unknown slow operation ID: " + slowOpId + @@ -225,6 +243,11 @@ public static String getSlowOpName(int slowOpId) { case Opcodes.SLOWOP_REVERSE -> "reverse"; case Opcodes.SLOWOP_ARRAY_SLICE_SET -> "array_slice_set"; case Opcodes.SLOWOP_SPLIT -> "split"; + case Opcodes.SLOWOP_EXISTS -> "exists"; + case Opcodes.SLOWOP_DELETE -> "delete"; + case Opcodes.SLOWOP_DEREF_HASH -> "deref_hash"; + case Opcodes.SLOWOP_HASH_SLICE -> "hash_slice"; + case Opcodes.SLOWOP_HASH_SLICE_DELETE -> "hash_slice_delete"; default -> "slowop_" + slowOpId; }; } @@ -878,6 +901,181 @@ private static int executeSplit( return pc; } + /** + * SLOW_EXISTS: rd = exists operand + * Format: [SLOW_EXISTS] [rd] [operandReg] + * Effect: rd = exists operand (fallback for non-simple cases) + */ + private static int executeExists( + short[] bytecode, + int pc, + RuntimeBase[] registers) { + + int rd = bytecode[pc++]; + int operandReg = bytecode[pc++]; + + // For now, throw unsupported - basic exists should use fast path + throw new UnsupportedOperationException( + "exists() slow path not yet implemented in interpreter. " + + "Use simple hash access: exists $hash{key}" + ); + } + + /** + * SLOW_DELETE: rd = delete operand + * Format: [SLOW_DELETE] [rd] [operandReg] + * Effect: rd = delete operand (fallback for non-simple cases) + */ + private static int executeDelete( + short[] bytecode, + int pc, + RuntimeBase[] registers) { + + int rd = bytecode[pc++]; + int operandReg = bytecode[pc++]; + + // For now, throw unsupported - basic delete should use fast path + throw new UnsupportedOperationException( + "delete() slow path not yet implemented in interpreter. " + + "Use simple hash access: delete $hash{key}" + ); + } + + /** + * Dereference hash reference for hashref access. + * Handles: $hashref->{key} where $hashref contains a hash reference + * + * @param bytecode The bytecode array + * @param pc Program counter (points after slowOpId) + * @param registers Register array + * @return Updated program counter + */ + private static int executeDerefHash( + short[] bytecode, + int pc, + RuntimeBase[] registers) { + + int rd = bytecode[pc++]; + int scalarReg = bytecode[pc++]; + + RuntimeBase scalarBase = registers[scalarReg]; + + // If it's already a hash, use it directly + if (scalarBase instanceof RuntimeHash) { + registers[rd] = scalarBase; + return pc; + } + + // Otherwise, dereference as hash reference + RuntimeScalar scalar = scalarBase.scalar(); + + // Get the dereferenced hash using Perl's hash dereference semantics + RuntimeHash hash = scalar.hashDeref(); + + registers[rd] = hash; + return pc; + } + + /** + * SLOW_HASH_SLICE: rd = hash.getSlice(keys_list) + * Format: [SLOW_HASH_SLICE] [rd] [hashReg] [keysListReg] + * Effect: rd = RuntimeArray of values for the given keys + */ + private static int executeHashSlice( + short[] bytecode, + int pc, + RuntimeBase[] registers) { + + int rd = bytecode[pc++]; + int hashReg = bytecode[pc++]; + int keysListReg = bytecode[pc++]; + + RuntimeHash hash = (RuntimeHash) registers[hashReg]; + RuntimeList keysList = (RuntimeList) registers[keysListReg]; + + // Get values for all keys + RuntimeList valuesList = hash.getSlice(keysList); + + // Convert to RuntimeArray for array assignment + RuntimeArray result = new RuntimeArray(); + for (RuntimeBase elem : valuesList.elements) { + result.elements.add(elem.scalar()); + } + + registers[rd] = result; + return pc; + } + + /** + * SLOW_HASH_SLICE_DELETE: rd = hash.deleteSlice(keys_list) + * Format: [SLOW_HASH_SLICE_DELETE] [rd] [hashReg] [keysListReg] + * Effect: rd = RuntimeList of deleted values + */ + private static int executeHashSliceDelete( + short[] bytecode, + int pc, + RuntimeBase[] registers) { + + int rd = bytecode[pc++]; + int hashReg = bytecode[pc++]; + int keysListReg = bytecode[pc++]; + + RuntimeHash hash = (RuntimeHash) registers[hashReg]; + RuntimeList keysList = (RuntimeList) registers[keysListReg]; + + // Delete values for all keys and return them + RuntimeList deletedValuesList = hash.deleteSlice(keysList); + + // Convert to RuntimeArray for array assignment + RuntimeArray result = new RuntimeArray(); + for (RuntimeBase elem : deletedValuesList.elements) { + result.elements.add(elem.scalar()); + } + + registers[rd] = result; + return pc; + } + + /** + * SLOW_HASH_SLICE_SET: hash.setSlice(keys_list, values_list) + * Format: [SLOW_HASH_SLICE_SET] [hashReg] [keysListReg] [valuesListReg] + * Effect: Assign values to multiple hash keys (slice assignment) + */ + private static int executeHashSliceSet( + short[] bytecode, + int pc, + RuntimeBase[] registers) { + + int hashReg = bytecode[pc++]; + int keysListReg = bytecode[pc++]; + int valuesListReg = bytecode[pc++]; + + RuntimeHash hash = (RuntimeHash) registers[hashReg]; + RuntimeList keysList = (RuntimeList) registers[keysListReg]; + RuntimeBase valuesBase = registers[valuesListReg]; + + // Convert values to RuntimeList if needed + RuntimeList valuesList; + if (valuesBase instanceof RuntimeList) { + valuesList = (RuntimeList) valuesBase; + } else if (valuesBase instanceof RuntimeArray) { + // Convert RuntimeArray to RuntimeList + valuesList = new RuntimeList(); + for (RuntimeScalar elem : (RuntimeArray) valuesBase) { + valuesList.elements.add(elem); + } + } else { + // Single value - wrap in list + valuesList = new RuntimeList(); + valuesList.elements.add(valuesBase.scalar()); + } + + // Set all key-value pairs + hash.setSlice(keysList, valuesList); + + return pc; + } + private SlowOpcodeHandler() { // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/runtime/RuntimeHash.java b/src/main/java/org/perlonjava/runtime/RuntimeHash.java index 83538bdf5..2297914e5 100644 --- a/src/main/java/org/perlonjava/runtime/RuntimeHash.java +++ b/src/main/java/org/perlonjava/runtime/RuntimeHash.java @@ -529,6 +529,35 @@ public RuntimeList deleteSlice(RuntimeList value) { return result; } + /** + * Set multiple hash elements from key and value lists (slice assignment). + * + * @param keys The RuntimeList containing the keys. + * @param values The RuntimeList containing the values. + */ + public void setSlice(RuntimeList keys, RuntimeList values) { + if (this.type == AUTOVIVIFY_HASH) { + AutovivificationHash.vivify(this); + } + + // Iterate through keys and values in parallel + Iterator keyIter = keys.iterator(); + Iterator valueIter = values.iterator(); + + while (keyIter.hasNext()) { + RuntimeScalar keyScalar = keyIter.next(); + String key = keyScalar.toString(); // Convert key to string + RuntimeScalar value; + if (valueIter.hasNext()) { + value = valueIter.next(); + } else { + // If we run out of values, use undef + value = new RuntimeScalar(); + } + this.put(key, value); + } + } + /** * The keys() operator for hashes. *