Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
23 commits
Select commit Hold shift + click to select a range
cf1a8c9
fix: short-circuit //=, ||=, &&= in bytecode interpreter and other DB…
fglock Apr 1, 2026
3527d84
docs: update DBIx::Class design doc with steps 5.35-5.37
fglock Apr 2, 2026
c2b893d
docs: comprehensive update of DBIx::Class design doc with 314-file te…
fglock Apr 2, 2026
ff6b7e1
fix: glob deref, lvalue vivification, and DBI prepare_cached for DBIx…
fglock Apr 2, 2026
d5f18ba
fix: Storable binary serializer and DBI HandleError for DBIx::Class
fglock Apr 2, 2026
9c8841e
fix: autovivification hash/array reuse for multi-element list assignment
fglock Apr 2, 2026
4ef1e69
fix: ref() returns REF for nested references (ref-of-ref)
fglock Apr 2, 2026
d040d49
feat: caller() returns compile-time $^H and %^H hints (elements 8 and…
fglock Apr 2, 2026
1b1d35c
fix: mro::get_isarev dynamic scan and mro::get_pkg_gen auto-increment
fglock Apr 2, 2026
045dbed
fix: BytecodeCompiler sub-compiler inherits pragma flags (strict/warn…
fglock Apr 2, 2026
6e0b615
fix: warn() returns 1, overload fallback semantics and autogeneration
fglock Apr 2, 2026
3169b53
fix: B.pm SV flags rewrite and large integer literals stored as DOUBLE
fglock Apr 2, 2026
f90b06e
fix: caller() in eval STRING, list slice in interpreter, sub naming
fglock Apr 2, 2026
36cb83f
docs: update design doc with steps 5.52-5.53 and Sub-Quote progress
fglock Apr 2, 2026
6ea9951
fix: interpreter LIST_SLICE scalar context conversion
fglock Apr 2, 2026
2a5f856
fix: Storable nfreeze/thaw call STORABLE_freeze/thaw hooks on blessed…
fglock Apr 2, 2026
7035549
fix: DBI sth Active flag lifecycle matches real DBI behavior
fglock Apr 2, 2026
003b368
docs: update design doc with DBI Active flag fix results (step 5.56)
fglock Apr 2, 2026
3cc2ff1
fix: resolve post-rebase test regressions in multiple test files
fglock Apr 2, 2026
c9545d6
docs: update DBIx::Class plan with step 5.57 (post-rebase regression …
fglock Apr 2, 2026
6d90b0a
fix: pack/unpack 32-bit consistency — j/J use ivsize=4, disable q/Q
fglock Apr 2, 2026
e226130
fix: reject quad sprintf formats (%lld, %Ld, etc.) on 32-bit
fglock Apr 2, 2026
3564e00
fix: handle Inf/NaN values with invalid quad sprintf formats
fglock Apr 2, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -87,6 +87,7 @@ Image-ExifTool-*

# Ignore xxx/ directory (temporary module staging area)
xxx/
cpan_build_dir/
*.jfr
report.txt
exiftool_results.json
512 changes: 445 additions & 67 deletions dev/modules/dbix_class.md

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import org.perlonjava.runtime.perlmodule.FilterUtilCall;
import org.perlonjava.runtime.perlmodule.Strict;
import org.perlonjava.runtime.runtimetypes.*;
import org.perlonjava.runtime.WarningBitsRegistry;

import java.lang.invoke.MethodHandle;
import java.lang.reflect.Constructor;
Expand Down Expand Up @@ -305,6 +306,9 @@ public static RuntimeList executePerlAST(Node ast,
// code in the same lexical block sees the updated hints
if (savedCurrentScope != null) {
savedCurrentScope.setStrictOptions(ctx.symbolTable.getStrictOptions());
// Also update per-call-site hints so caller()[8] and caller()[10] are correct
WarningBitsRegistry.setCallSiteHints(ctx.symbolTable.getStrictOptions());
WarningBitsRegistry.snapshotCurrentHintHash();
SpecialBlockParser.setCurrentScope(savedCurrentScope);
}
}
Expand Down
251 changes: 179 additions & 72 deletions src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java

Large diffs are not rendered by default.

Original file line number Diff line number Diff line change
Expand Up @@ -1960,6 +1960,27 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c
pc = SlowOpcodeHandler.executeDispatchVarAttrs(bytecode, pc, registers, code.constants);
}

case Opcodes.VIVIFY_LVALUE -> {
// Vivify an lvalue proxy so the entry exists in the parent container.
// For plain scalars this is a no-op.
int reg = bytecode[pc++];
RuntimeBase val = registers[reg];
if (val instanceof RuntimeScalar rs) {
rs.vivifyLvalue();
}
}

case Opcodes.LIST_SLICE -> {
// List slice: rd = list.getSlice(indices)
// Used for (list)[indices] syntax
int rd = bytecode[pc++];
int listReg = bytecode[pc++];
int indicesReg = bytecode[pc++];
RuntimeList list = registers[listReg].getList();
RuntimeList indices = registers[indicesReg].getList();
registers[rd] = list.getSlice(indices);
}

default -> {
int opcodeInt = opcode;
throw new RuntimeException(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -291,14 +291,36 @@ else if (node.right instanceof BinaryOperatorNode rightCall) {
}
}

// Handle ListNode case: (expr)[index] like (caller)[0]
// Transform to [expr]->[index] like JVM does
// Handle ListNode case: (expr)[indices] like (caller(0))[0] or (1,2,3,4)[1,2]
// Use proper list slice semantics: evaluate list, then slice by indices
if (node.left instanceof ListNode listNode) {
// Create: ArrayLiteralNode containing the list elements
// Then: BinaryOperatorNode("->", arrayLiteral, node.right)
ArrayLiteralNode arrayLiteral = new ArrayLiteralNode(listNode.elements, listNode.getIndex());
BinaryOperatorNode arrowNode = new BinaryOperatorNode("->", arrayLiteral, node.right, node.getIndex());
arrowNode.accept(bytecodeCompiler);
// Compile the list in LIST context
bytecodeCompiler.compileNode(listNode, -1, RuntimeContextType.LIST);
int listReg = bytecodeCompiler.lastResultReg;

// Compile the indices in LIST context
ListNode indices = ((ArrayLiteralNode) node.right).asListNode();
bytecodeCompiler.compileNode(indices, -1, RuntimeContextType.LIST);
int indicesReg = bytecodeCompiler.lastResultReg;

// Emit LIST_SLICE opcode: rd = list.getSlice(indices)
int sliceReg = bytecodeCompiler.allocateOutputRegister();
bytecodeCompiler.emit(Opcodes.LIST_SLICE);
bytecodeCompiler.emitReg(sliceReg);
bytecodeCompiler.emitReg(listReg);
bytecodeCompiler.emitReg(indicesReg);

// Handle context conversion: LIST_SLICE returns a RuntimeList,
// but in scalar context we need to extract the scalar value
if (bytecodeCompiler.currentCallContext == RuntimeContextType.SCALAR) {
int scalarReg = bytecodeCompiler.allocateOutputRegister();
bytecodeCompiler.emit(Opcodes.LIST_TO_SCALAR);
bytecodeCompiler.emitReg(scalarReg);
bytecodeCompiler.emitReg(sliceReg);
bytecodeCompiler.lastResultReg = scalarReg;
} else {
bytecodeCompiler.lastResultReg = sliceReg;
}
return;
}

Expand Down
13 changes: 13 additions & 0 deletions src/main/java/org/perlonjava/backend/bytecode/Disassemble.java
Original file line number Diff line number Diff line change
Expand Up @@ -880,6 +880,19 @@ public static String disassemble(InterpretedCode interpretedCode) {
.append("[r").append(asdlIndicesReg).append("]\n");
break;
}
case Opcodes.VIVIFY_LVALUE: {
int vivReg = interpretedCode.bytecode[pc++];
sb.append("VIVIFY_LVALUE r").append(vivReg).append("\n");
break;
}
case Opcodes.LIST_SLICE: {
rd = interpretedCode.bytecode[pc++];
int lsListReg = interpretedCode.bytecode[pc++];
int lsIndicesReg = interpretedCode.bytecode[pc++];
sb.append("LIST_SLICE r").append(rd).append(" = r").append(lsListReg)
.append(".getSlice(r").append(lsIndicesReg).append(")\n");
break;
}
case Opcodes.HASH_KEYS:
rd = interpretedCode.bytecode[pc++];
int hashKeysReg = interpretedCode.bytecode[pc++];
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -358,7 +358,8 @@ public static int executeIntegerDivAssign(int[] bytecode, int pc, RuntimeBase[]
int rd = bytecode[pc++];
int rs = bytecode[pc++];
RuntimeScalar s1 = (RuntimeScalar) registers[rd];
s1.set(MathOperators.integerDivide(s1, (RuntimeScalar) registers[rs]));
RuntimeScalar s2 = (registers[rs] instanceof RuntimeScalar) ? (RuntimeScalar) registers[rs] : registers[rs].scalar();
registers[rd] = MathOperators.integerDivideAssignWarn(s1, s2);
return pc;
}

Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/perlonjava/backend/bytecode/Opcodes.java
Original file line number Diff line number Diff line change
Expand Up @@ -2132,6 +2132,21 @@ public class Opcodes {
*/
public static final short DISPATCH_VAR_ATTRS = 451;

/**
* Vivify an lvalue proxy (hash/array element) so the entry exists in the parent container.
* For plain scalars this is a no-op. Used by ||=/&&=//= to match Perl 5's lvalue semantics
* where hash element access creates the entry before the condition check.
* Format: VIVIFY_LVALUE reg
*/
public static final short VIVIFY_LVALUE = 452;

/**
* List slice: rd = list.getSlice(indices)
* Used for (list)[indices] syntax in the interpreter.
* Format: LIST_SLICE rd list_reg indices_reg
*/
public static final short LIST_SLICE = 453;

private Opcodes() {
} // Utility class - no instantiation
}
13 changes: 10 additions & 3 deletions src/main/java/org/perlonjava/backend/jvm/EmitBinaryOperator.java
Original file line number Diff line number Diff line change
Expand Up @@ -214,9 +214,16 @@ static void handleCompoundAssignment(EmitterVisitor emitterVisitor, BinaryOperat
};

// Check if we have an operator handler for this compound operator
OperatorHandler operatorHandler = shouldUseWarnVariant
? OperatorHandler.getWarn(node.operator)
: OperatorHandler.get(node.operator);
// Under "use integer", use the integer warn variant for /=
boolean isInteger = emitterVisitor.ctx.symbolTable.isStrictOptionEnabled(Strict.HINT_INTEGER);
OperatorHandler operatorHandler;
if (shouldUseWarnVariant && isInteger && node.operator.equals("/=")) {
operatorHandler = OperatorHandler.get("/=_int_warn");
} else {
operatorHandler = shouldUseWarnVariant
? OperatorHandler.getWarn(node.operator)
: OperatorHandler.get(node.operator);
}

if (operatorHandler != null) {
// Use the new *Assign methods which check for compound overloads first
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/org/perlonjava/backend/jvm/EmitCompilerFlag.java
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,21 @@ public static void emitCompilerFlag(EmitterContext ctx, CompilerFlagNode node) {
"setCallSiteBits",
"(Ljava/lang/String;)V", false);

// Emit runtime code to update per-call-site $^H (hints).
// This allows caller()[8] to return accurate hints for the current statement.
int hints = node.getStrictOptions();
mv.visitLdcInsn(hints);
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/WarningBitsRegistry",
"setCallSiteHints",
"(I)V", false);

// Emit runtime code to snapshot %^H for caller()[10].
mv.visitMethodInsn(Opcodes.INVOKESTATIC,
"org/perlonjava/runtime/WarningBitsRegistry",
"snapshotCurrentHintHash",
"()V", false);

// Emit runtime code for warning scope if needed
int warningScopeId = node.getWarningScopeId();
if (warningScopeId > 0) {
Expand Down
36 changes: 27 additions & 9 deletions src/main/java/org/perlonjava/backend/jvm/EmitLiteral.java
Original file line number Diff line number Diff line change
Expand Up @@ -443,15 +443,33 @@ public static void emitNumber(EmitterContext ctx, NumberNode node) {
"getScalarInt",
"(I)Lorg/perlonjava/runtime/runtimetypes/RuntimeScalar;", false);
} else if (isLargeInteger) {
// Store large integers as strings to preserve precision
// This emulates 32-bit Perl behavior
if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("visit(NumberNode) emit large integer as string");
// Store large integers with precision preservation.
// Try long first (exact for values up to 2^63-1).
// RuntimeScalar(long) uses initializeWithLong() which stores values
// within 2^53 as DOUBLE and larger ones as STRING for full precision.
// Fall back to double for values that overflow long (e.g., unsigned 64-bit).
if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("visit(NumberNode) emit large integer");
boolean fitsInLong = true;
long longVal = 0;
try {
longVal = Long.parseLong(value);
} catch (NumberFormatException e) {
fitsInLong = false;
}
mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/runtimetypes/RuntimeScalar");
mv.visitInsn(Opcodes.DUP);
mv.visitLdcInsn(value);
mv.visitMethodInsn(
Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/runtimetypes/RuntimeScalar",
"<init>", "(Ljava/lang/String;)V", false);
if (fitsInLong) {
mv.visitLdcInsn(longVal);
mv.visitMethodInsn(
Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/runtimetypes/RuntimeScalar",
"<init>", "(J)V", false);
} else {
// Value exceeds long range — store as double (Perl NV promotion)
mv.visitLdcInsn(Double.valueOf(value));
mv.visitMethodInsn(
Opcodes.INVOKESPECIAL, "org/perlonjava/runtime/runtimetypes/RuntimeScalar",
"<init>", "(D)V", false);
}
} else {
// Create new RuntimeScalar for floating-point values
mv.visitTypeInsn(Opcodes.NEW, "org/perlonjava/runtime/runtimetypes/RuntimeScalar");
Expand All @@ -466,8 +484,8 @@ public static void emitNumber(EmitterContext ctx, NumberNode node) {
if (isInteger) {
mv.visitLdcInsn(Integer.parseInt(value));
} else if (isLargeInteger) {
// For large integers in unboxed context, we have to convert to double
// but this will lose precision - same as 32-bit Perl
// For unboxed context, convert to double (only option for primitive numeric)
// This may lose precision for values beyond 2^53
mv.visitLdcInsn(Double.parseDouble(value));
} else {
mv.visitLdcInsn(Double.parseDouble(value));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,13 @@ static void emitLogicalAssign(EmitterVisitor emitterVisitor, BinaryOperatorNode
// and jump away during evaluation.
node.left.accept(emitterVisitor.with(RuntimeContextType.SCALAR)); // target - left parameter

// Vivify the LHS proxy so hash/array entries exist before the condition check.
// This matches Perl 5's behavior where $h{key} ||= expr creates the hash entry
// (with undef value) during lvalue resolution, before evaluating the boolean condition.
mv.visitInsn(Opcodes.DUP);
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeScalar",
"vivifyLvalue", "()V", false);

int leftSlot = emitterVisitor.ctx.javaClassInfo.acquireSpillSlot();
boolean pooledLeft = leftSlot >= 0;
if (!pooledLeft) {
Expand Down
22 changes: 15 additions & 7 deletions src/main/java/org/perlonjava/backend/jvm/EmitStatement.java
Original file line number Diff line number Diff line change
Expand Up @@ -427,14 +427,22 @@ static void emitDoWhile(EmitterVisitor emitterVisitor, For3Node node) {
// Continue label (for next iteration)
mv.visitLabel(continueLabel);

// Visit the condition node in scalar context
node.condition.accept(emitterVisitor.with(RuntimeContextType.SCALAR));

// Convert the result to a boolean
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeBase", "getBoolean", "()Z", false);
// Check if condition is a compile-time constant (e.g., "do {} until TRUE_CONST")
String currentPackage = emitterVisitor.ctx.symbolTable.getCurrentPackage();
Boolean constantCondition = ConstantFoldingVisitor.getConstantConditionValue(node.condition, currentPackage);

// If condition is true, jump back to start
mv.visitJumpInsn(Opcodes.IFNE, startLabel);
if (constantCondition != null) {
if (constantCondition) {
// Condition is constant true — infinite loop, jump back unconditionally
mv.visitJumpInsn(Opcodes.GOTO, startLabel);
}
// else: condition is constant false — don't jump back, body runs exactly once
} else {
// Non-constant condition — emit normal runtime evaluation
node.condition.accept(emitterVisitor.with(RuntimeContextType.SCALAR));
mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "org/perlonjava/runtime/runtimetypes/RuntimeBase", "getBoolean", "()Z", false);
mv.visitJumpInsn(Opcodes.IFNE, startLabel);
}

// End of loop
mv.visitLabel(endLabel);
Expand Down
2 changes: 1 addition & 1 deletion src/main/java/org/perlonjava/core/Configuration.java
Original file line number Diff line number Diff line change
Expand Up @@ -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 = "9ac685c8e";
public static final String gitCommitId = "e22613062";

/**
* Git commit date of the build (ISO format: YYYY-MM-DD).
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -123,6 +123,14 @@ public static Boolean getConstantConditionValue(Node condition, String currentPa
}
}

// Handle not/! operators (used for `until` conditions and explicit negation)
if (condition instanceof OperatorNode opNode && opNode.operand != null) {
if ("not".equals(opNode.operator) || "!".equals(opNode.operator)) {
Boolean innerValue = getConstantConditionValue(opNode.operand, currentPackage);
if (innerValue != null) return !innerValue;
}
}

return null;
}

Expand All @@ -143,6 +151,11 @@ private static Boolean resolveConstantSubBoolean(String name, String currentPack
}
RuntimeBase firstElement = constList.elements.getFirst();
if (firstElement instanceof RuntimeScalar scalar) {
// References are always truthy in Perl — don't call getBoolean()
// which could trigger overloaded bool at compile time
if (RuntimeScalarType.isReference(scalar)) {
return true;
}
return scalar.getBoolean();
}
}
Expand Down
3 changes: 3 additions & 0 deletions src/main/java/org/perlonjava/frontend/parser/Parser.java
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,9 @@ public class Parser {
public boolean isInClassBlock = false;
// Are we parsing inside a method?
public boolean isInMethod = false;
// Are we parsing inside a braced dereference like %{...} or @{...}?
// When true, inner {} should default to hash constructor, not block.
public boolean insideBracedDereference = false;
// List to store ADJUST blocks for the current class
public List<Node> classAdjustBlocks = new ArrayList<>();
// List to store heredoc nodes encountered during parsing.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -933,6 +933,12 @@ public static boolean isHashLiteral(Parser parser) {
// { %hash } or { @array } or { %{$ref} } - treat as hash constructor
if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("isHashLiteral RESULT: TRUE - starts with sigil (% or @)");
return true;
} else if (parser.insideBracedDereference) {
// Inside %{...}, inner {} should default to hash constructor, not block.
// Perl 5 sets PL_expect = XTERM after %{, making the next { a hash constructor.
// Example: %{ {map { $_ => 1 } @_} } — inner {} is a hashref.
if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("isHashLiteral RESULT: TRUE - inside braced dereference context");
return true;
} else {
if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("isHashLiteral RESULT: FALSE - default for ambiguous case (assuming block)");
return false; // Default: assume block when we can't determine
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -708,7 +708,12 @@ public static Node parseSubroutineDefinition(Parser parser, boolean wantName, St
boolean previousInSubroutineBody = parser.ctx.symbolTable.isInSubroutineBody();

// Set the current subroutine name (use empty string for anonymous subs)
parser.ctx.symbolTable.setCurrentSubroutine(subName != null ? subName : "");
// Use fully qualified name so ByteCodeSourceMapper records the declaration-time
// package, not whatever package might be set inside the sub body
String qualifiedSubName = subName != null
? NameNormalizer.normalizeVariableName(subName, parser.ctx.symbolTable.getCurrentPackage())
: "";
parser.ctx.symbolTable.setCurrentSubroutine(qualifiedSubName);
// We are now parsing inside a subroutine body (named or anonymous)
parser.ctx.symbolTable.setInSubroutineBody(true);

Expand Down
Loading
Loading