From bbb076152c62dceef0e4293c2569c517b89bf0a3 Mon Sep 17 00:00:00 2001 From: "Flavio S. Glock" Date: Tue, 21 Apr 2026 11:25:26 +0200 Subject: [PATCH] fix(overload): honor `no overloading` for arithmetic ops; load XSLoader early MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Three related fixes uncovered while investigating `./jcpan -t Hash::Ordered`: 1. `no overloading` was only honored for string concatenation and `join`. Arithmetic ops (`+`, `-`, `*`, `/`, `%`, `**`, unary `-`) still dispatched through the overload machinery, so `sub _numify { no overloading; 0+$_[0] }` recursed into its own `0+` overload and blew the JVM stack. This caused Hash::Ordered's t/basic.t to die with `Deep recursion on subroutine "Hash::Ordered::_numify"` / `StackOverflowError`. Fix: mirror the existing `stringConcatNoOverload` / `joinNoOverload` pattern for arithmetic. - `RuntimeScalar.getNumberNoOverload()` — returns refaddr-style integer for blessed refs, bypassing `Overload.numify`. - `MathOperators.{add,subtract,multiply,divide,modulus,pow,unaryMinus}NoOverload`. - `OperatorHandler.getNoOverload(op)` + registered `_noOverload` variants for `+ - * / % ** unaryMinus`. - `EmitOperator.emitOperator` prefers the NoOverload variant when `HINT_NO_AMAGIC` is set in the current lexical scope (same mechanism already used for concat/join). 2. Bytecode-interpreter parity for the above. The interpreter had no equivalent of the JVM-backend's NoOverload dispatch for arithmetic. - New opcodes `ADD_NO_OVERLOAD`..`POW_NO_OVERLOAD`, `NEG_NO_OVERLOAD`. - `CompileBinaryOperatorHelper` and `CompileOperator` emit them when `isNoOverloadingEnabled()` is true. - `InlineOpcodeHandler` implements the runtime handlers; `BytecodeInterpreter` dispatches them; `Disassemble` prints them. 3. `XSLoader::load` was undefined at startup on the interpreter backend, which made *every* interpreter invocation die with `Undefined subroutine &XSLoader::load called at jar:PERL5LIB/strict.pm line 12`. Root cause: in `GlobalContext.initializeGlobals()`, `DynaLoader.initialize()` triggered `require(Exporter.pm)` → `use strict;` → `require(strict.pm)`, whose top-level calls `XSLoader::load(...)`. On the interpreter backend that top-level actually runs (the JVM backend caches the code ref differently and masks the issue). But `XSLoader.initialize()` was scheduled *after* DynaLoader, so the global codeRef was only a symbolic-reference stub with no method handle — `defined()` returned true while `apply()` threw `Undefined subroutine &XSLoader::load`. Fix: move `XSLoader.initialize()` ahead of `DynaLoader.initialize()` (and before `Builtin.initialize()`), with a comment explaining the ordering requirement. Verification: - `./jcpan -t Hash::Ordered` — before: 1/4 test programs failed with `StackOverflowError`; after: all 112 tests pass. - `./jperl --interpreter -e '...'` — now runs (previously died on startup). - `./jperl --interpreter /tmp/test_no_overloading.pl` — exercises the new `NEG_NO_OVERLOAD` / arithmetic NoOverload opcodes in the interpreter. - `make` — full unit test suite passes. Generated with [Devin](https://cli.devin.ai/docs) Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com> --- .../backend/bytecode/BytecodeInterpreter.java | 23 +++++ .../bytecode/CompileBinaryOperatorHelper.java | 17 ++-- .../backend/bytecode/CompileOperator.java | 2 +- .../backend/bytecode/Disassemble.java | 26 ++++++ .../backend/bytecode/InlineOpcodeHandler.java | 84 +++++++++++++++++++ .../perlonjava/backend/bytecode/Opcodes.java | 13 +++ .../perlonjava/backend/jvm/EmitOperator.java | 16 +++- .../org/perlonjava/core/Configuration.java | 4 +- .../runtime/operators/MathOperators.java | 60 +++++++++++++ .../runtime/operators/OperatorHandler.java | 22 +++++ .../runtime/runtimetypes/GlobalContext.java | 14 +++- .../runtime/runtimetypes/RuntimeScalar.java | 28 +++++++ 12 files changed, 293 insertions(+), 16 deletions(-) diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index 27c4c3925..1604956e6 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -624,6 +624,29 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c pc = InlineOpcodeHandler.executeNegScalar(bytecode, pc, registers); } + // Arithmetic without overload dispatch (no overloading pragma) + case Opcodes.ADD_NO_OVERLOAD -> { + pc = InlineOpcodeHandler.executeAddNoOverload(bytecode, pc, registers); + } + case Opcodes.SUB_NO_OVERLOAD -> { + pc = InlineOpcodeHandler.executeSubNoOverload(bytecode, pc, registers); + } + case Opcodes.MUL_NO_OVERLOAD -> { + pc = InlineOpcodeHandler.executeMulNoOverload(bytecode, pc, registers); + } + case Opcodes.DIV_NO_OVERLOAD -> { + pc = InlineOpcodeHandler.executeDivNoOverload(bytecode, pc, registers); + } + case Opcodes.MOD_NO_OVERLOAD -> { + pc = InlineOpcodeHandler.executeModNoOverload(bytecode, pc, registers); + } + case Opcodes.POW_NO_OVERLOAD -> { + pc = InlineOpcodeHandler.executePowNoOverload(bytecode, pc, registers); + } + case Opcodes.NEG_NO_OVERLOAD -> { + pc = InlineOpcodeHandler.executeNegNoOverload(bytecode, pc, registers); + } + // Specialized unboxed operations (rare optimizations) case Opcodes.ADD_SCALAR_INT -> { pc = InlineOpcodeHandler.executeAddScalarInt(bytecode, pc, registers); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java index 7389af20b..ca645fe0a 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileBinaryOperatorHelper.java @@ -24,45 +24,48 @@ public static int compileBinaryOperatorSwitch(BytecodeCompiler bytecodeCompiler, int rd = bytecodeCompiler.allocateOutputRegister(); // Emit opcode based on operator + boolean noOverload = bytecodeCompiler.isNoOverloadingEnabled(); switch (operator) { case "+" -> { - bytecodeCompiler.emit(Opcodes.ADD_SCALAR); + bytecodeCompiler.emit(noOverload ? Opcodes.ADD_NO_OVERLOAD : Opcodes.ADD_SCALAR); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); } case "-" -> { - bytecodeCompiler.emit(Opcodes.SUB_SCALAR); + bytecodeCompiler.emit(noOverload ? Opcodes.SUB_NO_OVERLOAD : Opcodes.SUB_SCALAR); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); } case "*" -> { - bytecodeCompiler.emit(Opcodes.MUL_SCALAR); + bytecodeCompiler.emit(noOverload ? Opcodes.MUL_NO_OVERLOAD : Opcodes.MUL_SCALAR); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); } case "%" -> { - bytecodeCompiler.emit(bytecodeCompiler.isIntegerEnabled() ? Opcodes.INTEGER_MOD : Opcodes.MOD_SCALAR); + bytecodeCompiler.emit(noOverload ? Opcodes.MOD_NO_OVERLOAD + : (bytecodeCompiler.isIntegerEnabled() ? Opcodes.INTEGER_MOD : Opcodes.MOD_SCALAR)); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); } case "/" -> { - bytecodeCompiler.emit(bytecodeCompiler.isIntegerEnabled() ? Opcodes.INTEGER_DIV : Opcodes.DIV_SCALAR); + bytecodeCompiler.emit(noOverload ? Opcodes.DIV_NO_OVERLOAD + : (bytecodeCompiler.isIntegerEnabled() ? Opcodes.INTEGER_DIV : Opcodes.DIV_SCALAR)); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); } case "**" -> { - bytecodeCompiler.emit(Opcodes.POW_SCALAR); + bytecodeCompiler.emit(noOverload ? Opcodes.POW_NO_OVERLOAD : Opcodes.POW_SCALAR); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); } case "." -> { - bytecodeCompiler.emit(bytecodeCompiler.isNoOverloadingEnabled() ? Opcodes.CONCAT_NO_OVERLOAD : Opcodes.CONCAT); + bytecodeCompiler.emit(noOverload ? Opcodes.CONCAT_NO_OVERLOAD : Opcodes.CONCAT); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(rs1); bytecodeCompiler.emitReg(rs2); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index c865e32b7..d208a6076 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -1156,7 +1156,7 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode bytecodeCompiler.compileNode(node.operand, -1, RuntimeContextType.SCALAR); int operandReg = bytecodeCompiler.lastResultReg; int rd = bytecodeCompiler.allocateOutputRegister(); - bytecodeCompiler.emit(Opcodes.NEG_SCALAR); + bytecodeCompiler.emit(bytecodeCompiler.isNoOverloadingEnabled() ? Opcodes.NEG_NO_OVERLOAD : Opcodes.NEG_SCALAR); bytecodeCompiler.emitReg(rd); bytecodeCompiler.emitReg(operandReg); bytecodeCompiler.lastResultReg = rd; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java index fbfd281aa..5673e65d8 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Disassemble.java @@ -302,6 +302,32 @@ public static String disassemble(InterpretedCode interpretedCode) { int rsNeg = interpretedCode.bytecode[pc++]; sb.append("NEG_SCALAR r").append(rd).append(" = -r").append(rsNeg).append("\n"); break; + case Opcodes.ADD_NO_OVERLOAD: + case Opcodes.SUB_NO_OVERLOAD: + case Opcodes.MUL_NO_OVERLOAD: + case Opcodes.DIV_NO_OVERLOAD: + case Opcodes.MOD_NO_OVERLOAD: + case Opcodes.POW_NO_OVERLOAD: { + rd = interpretedCode.bytecode[pc++]; + rs1 = interpretedCode.bytecode[pc++]; + rs2 = interpretedCode.bytecode[pc++]; + String op = switch (opcode) { + case Opcodes.ADD_NO_OVERLOAD -> "ADD_NO_OVERLOAD"; + case Opcodes.SUB_NO_OVERLOAD -> "SUB_NO_OVERLOAD"; + case Opcodes.MUL_NO_OVERLOAD -> "MUL_NO_OVERLOAD"; + case Opcodes.DIV_NO_OVERLOAD -> "DIV_NO_OVERLOAD"; + case Opcodes.MOD_NO_OVERLOAD -> "MOD_NO_OVERLOAD"; + default -> "POW_NO_OVERLOAD"; + }; + sb.append(op).append(" r").append(rd).append(" = r").append(rs1).append(", r").append(rs2).append("\n"); + break; + } + case Opcodes.NEG_NO_OVERLOAD: { + rd = interpretedCode.bytecode[pc++]; + int rsNegNo = interpretedCode.bytecode[pc++]; + sb.append("NEG_NO_OVERLOAD r").append(rd).append(" = -r").append(rsNegNo).append("\n"); + break; + } case Opcodes.ADD_SCALAR_INT: rd = interpretedCode.bytecode[pc++]; int rs = interpretedCode.bytecode[pc++]; diff --git a/src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java index 83e089e45..edf1ba260 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/InlineOpcodeHandler.java @@ -172,6 +172,90 @@ public static int executeNegScalar(int[] bytecode, int pc, RuntimeBase[] registe return pc; } + /** + * Arithmetic without overload dispatch. + * Used when {@code no overloading} is in effect at compile time. + * Format: OPCODE rd rs1 rs2 + */ + public static int executeAddNoOverload(int[] bytecode, int pc, RuntimeBase[] registers) { + int rd = bytecode[pc++]; + int rs1 = bytecode[pc++]; + int rs2 = bytecode[pc++]; + RuntimeBase v1 = registers[rs1]; + RuntimeBase v2 = registers[rs2]; + RuntimeScalar s1 = (v1 instanceof RuntimeScalar) ? (RuntimeScalar) v1 : v1.scalar(); + RuntimeScalar s2 = (v2 instanceof RuntimeScalar) ? (RuntimeScalar) v2 : v2.scalar(); + registers[rd] = MathOperators.addNoOverload(s1, s2); + return pc; + } + + public static int executeSubNoOverload(int[] bytecode, int pc, RuntimeBase[] registers) { + int rd = bytecode[pc++]; + int rs1 = bytecode[pc++]; + int rs2 = bytecode[pc++]; + RuntimeBase v1 = registers[rs1]; + RuntimeBase v2 = registers[rs2]; + RuntimeScalar s1 = (v1 instanceof RuntimeScalar) ? (RuntimeScalar) v1 : v1.scalar(); + RuntimeScalar s2 = (v2 instanceof RuntimeScalar) ? (RuntimeScalar) v2 : v2.scalar(); + registers[rd] = MathOperators.subtractNoOverload(s1, s2); + return pc; + } + + public static int executeMulNoOverload(int[] bytecode, int pc, RuntimeBase[] registers) { + int rd = bytecode[pc++]; + int rs1 = bytecode[pc++]; + int rs2 = bytecode[pc++]; + RuntimeBase v1 = registers[rs1]; + RuntimeBase v2 = registers[rs2]; + RuntimeScalar s1 = (v1 instanceof RuntimeScalar) ? (RuntimeScalar) v1 : v1.scalar(); + RuntimeScalar s2 = (v2 instanceof RuntimeScalar) ? (RuntimeScalar) v2 : v2.scalar(); + registers[rd] = MathOperators.multiplyNoOverload(s1, s2); + return pc; + } + + public static int executeDivNoOverload(int[] bytecode, int pc, RuntimeBase[] registers) { + int rd = bytecode[pc++]; + int rs1 = bytecode[pc++]; + int rs2 = bytecode[pc++]; + RuntimeBase v1 = registers[rs1]; + RuntimeBase v2 = registers[rs2]; + RuntimeScalar s1 = (v1 instanceof RuntimeScalar) ? (RuntimeScalar) v1 : v1.scalar(); + RuntimeScalar s2 = (v2 instanceof RuntimeScalar) ? (RuntimeScalar) v2 : v2.scalar(); + registers[rd] = MathOperators.divideNoOverload(s1, s2); + return pc; + } + + public static int executeModNoOverload(int[] bytecode, int pc, RuntimeBase[] registers) { + int rd = bytecode[pc++]; + int rs1 = bytecode[pc++]; + int rs2 = bytecode[pc++]; + RuntimeBase v1 = registers[rs1]; + RuntimeBase v2 = registers[rs2]; + RuntimeScalar s1 = (v1 instanceof RuntimeScalar) ? (RuntimeScalar) v1 : v1.scalar(); + RuntimeScalar s2 = (v2 instanceof RuntimeScalar) ? (RuntimeScalar) v2 : v2.scalar(); + registers[rd] = MathOperators.modulusNoOverload(s1, s2); + return pc; + } + + public static int executePowNoOverload(int[] bytecode, int pc, RuntimeBase[] registers) { + int rd = bytecode[pc++]; + int rs1 = bytecode[pc++]; + int rs2 = bytecode[pc++]; + RuntimeBase v1 = registers[rs1]; + RuntimeBase v2 = registers[rs2]; + RuntimeScalar s1 = (v1 instanceof RuntimeScalar) ? (RuntimeScalar) v1 : v1.scalar(); + RuntimeScalar s2 = (v2 instanceof RuntimeScalar) ? (RuntimeScalar) v2 : v2.scalar(); + registers[rd] = MathOperators.powNoOverload(s1, s2); + return pc; + } + + public static int executeNegNoOverload(int[] bytecode, int pc, RuntimeBase[] registers) { + int rd = bytecode[pc++]; + int rs = bytecode[pc++]; + registers[rd] = MathOperators.unaryMinusNoOverload((RuntimeScalar) registers[rs]); + return pc; + } + /** * Addition with immediate: rd = rs + immediate * Format: ADD_SCALAR_INT rd rs immediate diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index 300c677a8..d47a32f9b 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -2279,6 +2279,19 @@ public class Opcodes { */ public static final short LOAD_UNDEF_READONLY = 473; + /** + * Arithmetic operators without overload dispatch. + * Used when {@code no overloading} is in effect at compile time. + * Format: OPCODE rd rs1 rs2 (NEG_NO_OVERLOAD: rd rs) + */ + public static final short ADD_NO_OVERLOAD = 474; + public static final short SUB_NO_OVERLOAD = 475; + public static final short MUL_NO_OVERLOAD = 476; + public static final short DIV_NO_OVERLOAD = 477; + public static final short MOD_NO_OVERLOAD = 478; + public static final short POW_NO_OVERLOAD = 479; + public static final short NEG_NO_OVERLOAD = 480; + private Opcodes() { } // Utility class - no instantiation } diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index 330961f97..15b300a65 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -32,12 +32,20 @@ static void emitOperator(Node node, EmitterVisitor emitterVisitor) { throw new PerlCompilerException(node.getIndex(), "Node must be OperatorNode or BinaryOperatorNode", emitterVisitor.ctx.errorUtil); } + // Check if `no overloading` is active - prefer NoOverload variant when available + ScopedSymbolTable symbolTable = emitterVisitor.ctx.symbolTable; + boolean noOverloading = symbolTable != null && + symbolTable.isStrictOptionEnabled(Strict.HINT_NO_AMAGIC); + OperatorHandler operatorHandler = noOverloading ? OperatorHandler.getNoOverload(operator) : null; + // Check if uninitialized warnings are enabled at compile time // Use warn variant for zero-overhead when warnings disabled - boolean warnUninit = emitterVisitor.ctx.symbolTable.isWarningCategoryEnabled("uninitialized"); - OperatorHandler operatorHandler = warnUninit - ? OperatorHandler.getWarn(operator) - : OperatorHandler.get(operator); + if (operatorHandler == null) { + boolean warnUninit = emitterVisitor.ctx.symbolTable.isWarningCategoryEnabled("uninitialized"); + operatorHandler = warnUninit + ? OperatorHandler.getWarn(operator) + : OperatorHandler.get(operator); + } if (operatorHandler == null) { throw new PerlCompilerException(node.getIndex(), "Operator \"" + operator + "\" doesn't have a defined JVM descriptor", emitterVisitor.ctx.errorUtil); } diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index e707a84bc..3b20f5ed9 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,7 +33,7 @@ public final class Configuration { * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitId = "57b3c2940"; + public static final String gitCommitId = "f8e0a8fc1"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 21 2026 11:07:07"; + public static final String buildTimestamp = "Apr 21 2026 11:24:02"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/runtime/operators/MathOperators.java b/src/main/java/org/perlonjava/runtime/operators/MathOperators.java index a1f34bd6a..1571a84f1 100644 --- a/src/main/java/org/perlonjava/runtime/operators/MathOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/MathOperators.java @@ -1246,4 +1246,64 @@ public static RuntimeScalar not(RuntimeScalar runtimeScalar) { default -> getScalarBoolean(!runtimeScalar.getBoolean()); }; } + + // ===================================================================== + // NoOverload variants - used when 'no overloading' pragma is in effect. + // These skip overload dispatch entirely and treat blessed references + // as if unblessed (using refaddr-style numeric conversion). + // ===================================================================== + + private static RuntimeScalar arith(RuntimeScalar a, RuntimeScalar b, int op) { + a = a.getNumberNoOverload(); + b = b.getNumberNoOverload(); + if (a.type == DOUBLE || b.type == DOUBLE) { + double x = a.getDouble(); + double y = b.getDouble(); + return switch (op) { + case 0 -> new RuntimeScalar(x + y); + case 1 -> new RuntimeScalar(x - y); + case 2 -> new RuntimeScalar(x * y); + case 3 -> new RuntimeScalar(x / y); + case 4 -> new RuntimeScalar(x % y); + case 5 -> new RuntimeScalar(Math.pow(x, y)); + default -> throw new IllegalStateException(); + }; + } + long x = a.getLong(); + long y = b.getLong(); + try { + return switch (op) { + case 0 -> getScalarInt(Math.addExact(x, y)); + case 1 -> getScalarInt(Math.subtractExact(x, y)); + case 2 -> getScalarInt(Math.multiplyExact(x, y)); + case 3 -> y != 0 && x % y == 0 + ? getScalarInt(x / y) + : new RuntimeScalar((double) x / (double) y); + case 4 -> y != 0 ? getScalarInt(x % y) + : new RuntimeScalar((double) x % (double) y); + case 5 -> new RuntimeScalar(Math.pow(x, y)); + default -> throw new IllegalStateException(); + }; + } catch (ArithmeticException ignored) { + return new RuntimeScalar((double) x + (double) y); + } + } + + public static RuntimeScalar addNoOverload(RuntimeScalar a, RuntimeScalar b) { return arith(a, b, 0); } + public static RuntimeScalar subtractNoOverload(RuntimeScalar a, RuntimeScalar b) { return arith(a, b, 1); } + public static RuntimeScalar multiplyNoOverload(RuntimeScalar a, RuntimeScalar b) { return arith(a, b, 2); } + public static RuntimeScalar divideNoOverload(RuntimeScalar a, RuntimeScalar b) { return arith(a, b, 3); } + public static RuntimeScalar modulusNoOverload(RuntimeScalar a, RuntimeScalar b) { return arith(a, b, 4); } + public static RuntimeScalar powNoOverload(RuntimeScalar a, RuntimeScalar b) { return arith(a, b, 5); } + + public static RuntimeScalar unaryMinusNoOverload(RuntimeScalar a) { + RuntimeScalar n = a.getNumberNoOverload(); + if (n.type == DOUBLE) return new RuntimeScalar(-n.getDouble()); + long v = n.getLong(); + try { + return getScalarInt(Math.negateExact(v)); + } catch (ArithmeticException ignored) { + return new RuntimeScalar(-(double) v); + } + } } \ No newline at end of file diff --git a/src/main/java/org/perlonjava/runtime/operators/OperatorHandler.java b/src/main/java/org/perlonjava/runtime/operators/OperatorHandler.java index 0a072b155..296917bf1 100644 --- a/src/main/java/org/perlonjava/runtime/operators/OperatorHandler.java +++ b/src/main/java/org/perlonjava/runtime/operators/OperatorHandler.java @@ -39,6 +39,16 @@ public record OperatorHandler(String className, String methodName, int methodTyp put("**_warn", "powWarn", "org/perlonjava/runtime/operators/MathOperators", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); put("unaryMinus_warn", "unaryMinusWarn", "org/perlonjava/runtime/operators/MathOperators", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); + // NoOverload variants - used when 'no overloading' pragma is in effect + // These bypass overload dispatch entirely (blessed refs -> refaddr-like numify) + put("+_noOverload", "addNoOverload", "org/perlonjava/runtime/operators/MathOperators"); + put("-_noOverload", "subtractNoOverload", "org/perlonjava/runtime/operators/MathOperators"); + put("*_noOverload", "multiplyNoOverload", "org/perlonjava/runtime/operators/MathOperators"); + put("/_noOverload", "divideNoOverload", "org/perlonjava/runtime/operators/MathOperators"); + put("%_noOverload", "modulusNoOverload", "org/perlonjava/runtime/operators/MathOperators"); + put("**_noOverload", "powNoOverload", "org/perlonjava/runtime/operators/MathOperators", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); + put("unaryMinus_noOverload", "unaryMinusNoOverload", "org/perlonjava/runtime/operators/MathOperators", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); + put("^^", "xor", "org/perlonjava/runtime/operators/Operator", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); put("xor", "xor", "org/perlonjava/runtime/operators/Operator", "(Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;"); @@ -425,6 +435,18 @@ public static OperatorHandler getWarn(String operator) { return warnHandler != null ? warnHandler : operatorHandlers.get(operator); } + /** + * Retrieves the NoOverload variant of an OperatorHandler if available. + * For operators like + and -, returns the noOverload variant (addNoOverload, ...) + * that bypasses overload dispatch. Used when {@code no overloading} is in effect. + * + * @param operator The operator symbol. + * @return The NoOverload variant OperatorHandler, or null if no variant exists. + */ + public static OperatorHandler getNoOverload(String operator) { + return operatorHandlers.get(operator + "_noOverload"); + } + /** * Gets the class name containing the method associated with the operator. * diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java index 5217bb584..b935eca25 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java @@ -243,6 +243,18 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { Mro.initialize(); // mro functions available without 'use mro' Vars.initialize(); Subs.initialize(); + // Register XSLoader first: several modules below (and pragmas loaded during + // their .pm compilation such as 'use strict' inside Exporter.pm) call + // XSLoader::load at BEGIN time. Defining it up-front avoids an "Undefined + // subroutine &XSLoader::load" failure in the interpreter backend, where + // require() actually executes the required file's top-level (including + // the XSLoader::load call in strict.pm) before XSLoader.initialize() would + // otherwise have run. + // Note: XSLoader MUST be initialized before DynaLoader.initialize() because + // DynaLoader's initializeExporter() triggers require(Exporter.pm), which + // does `use strict;`, which (re)compiles strict.pm, which calls XSLoader::load. + XSLoader.initialize(); + DynaLoader.initialize(); Builtin.initialize(); Base.initialize(); Symbol.initialize(); @@ -275,8 +287,6 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { IOHandle.initialize(); // IO::Handle methods (_sync, _error, etc.) Version.initialize(); // Initialize version module for version objects Attributes.initialize(); // attributes:: XS-equivalent functions (used by attributes.pm) - DynaLoader.initialize(); - XSLoader.initialize(); // XSLoader will load other classes on-demand // Filter::Util::Call will be loaded via XSLoader when needed // Reset method cache after initializing UNIVERSAL diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 25e46d5fc..c66e7113a 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -390,6 +390,34 @@ public RuntimeScalar getNumberLarge() { }; } + /** + * Convert to number without triggering overload dispatch. + * Used by {@code no overloading} pragma to bypass the {@code 0+} overload. + * For blessed references, returns the identity hash code as an integer. + */ + public RuntimeScalar getNumberNoOverload() { + return switch (type) { + case INTEGER, DOUBLE -> this; + case STRING, BYTE_STRING -> NumberParser.parseNumber(this); + case UNDEF -> scalarZero; + case VSTRING -> NumberParser.parseNumber(this); + case BOOLEAN -> (boolean) value ? scalarOne : scalarZero; + case GLOB -> scalarOne; + case JAVAOBJECT -> value != null ? scalarOne : scalarZero; + case TIED_SCALAR -> this.tiedFetch().getNumberNoOverload(); + case READONLY_SCALAR -> ((RuntimeScalar) this.value).getNumberNoOverload(); + case DUALVAR -> ((DualVar) this.value).numericValue(); + default -> { + // For references (blessed or not), return the identity hash code + // of the referent, matching Scalar::Util::refaddr semantics. + if (value != null) { + yield new RuntimeScalar(System.identityHashCode(value)); + } + yield scalarZero; + } + }; + } + /** * Converts scalar to number with uninitialized value warning. * Called when 'use warnings "uninitialized"' is in effect.