Skip to content

Fix test regressions: tied handles, sparse arrays, autovivification#253

Merged
fglock merged 13 commits intomasterfrom
fix-test-regressions
Mar 1, 2026
Merged

Fix test regressions: tied handles, sparse arrays, autovivification#253
fglock merged 13 commits intomasterfrom
fix-test-regressions

Conversation

@fglock
Copy link
Owner

@fglock fglock commented Feb 28, 2026

Summary

  • Fix sysread/syswrite on tied handles: Delegate to TieHandle.tiedRead()/tiedWrite() instead of throwing. Improves tiehandle.t from 16→49 tests.
  • Fix null array elements in getList(): Handle Java null elements (sparse arrays with holes) when returning arrays from subroutines. Improves array.t from 128→173 tests.
  • Fix negative index vivification crash: Throw correct Perl error for $a[-1] = 0 on empty array instead of IndexOutOfBoundsException.
  • Fix @{undef} without strict refs: Return empty array instead of throwing when dereferencing undef as array in non-strict mode. Improves array.t from 119→128 tests.
  • Fix $#array++ not extending arrays: Override auto-increment/decrement on RuntimeArraySizeLvalue. Improves sub.t from 10→41 tests.
  • Guard tied() against ClassCastException: Add instanceof checks after local *glob unwind to prevent StableHashMap→TieHash cast failure.

Test plan

  • All 154 unit tests pass
  • op/array.t: 128→173 tests passing
  • op/tiehandle.t: 16→49 tests passing
  • op/sub.t: 10→41 tests passing

Generated with Devin

fglock and others added 13 commits February 28, 2026 23:10
In Perl 5, @{undef} without strict refs converts undef to "" and
accesses @{""} (an empty package global). PerlOnJava was always throwing
"Can't use an undefined value as an ARRAY reference" regardless of
strict mode.

- Add strictAutovivify flag to RuntimeArray
- Set flag true in arrayDeref() (strict), false in arrayDerefNonStrict()
- In addToArray/scalar/lastElementIndex: throw only when strict, else
  return empty/0/-1
- array.t: 119 → 128 tests passing

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
When assigning to a negative index on an empty array (e.g. $a[-1] = 0),
the resolved key stays negative, causing IndexOutOfBoundsException.
Now throws the correct Perl error message instead.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
- IOOperator: delegate sysread() on tied handles to TieHandle.tiedRead()
- IOOperator: delegate syswrite() on tied handles to TieHandle.tiedWrite()
- RuntimeArray.getList(): handle null elements (sparse arrays) to prevent
  NPE/wrong error when returning arrays with holes from subroutines

Fixes: tiehandle.t 16→46 tests, array.t 128→173 tests

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
After local *glob restores a glob, the hash/array type field may still
be TIED_HASH/TIED_ARRAY while elements are restored to the original
StableHashMap/ArrayList. Add instanceof checks to prevent ClassCastException.

Fixes: tiehandle.t 46→49 tests

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
Handle cases where a register contains a RuntimeList instead of
RuntimeScalar by checking the type and calling .scalar() when needed,
preventing ClassCastException in the bytecode interpreter.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
When a register contains a RuntimeScalarReadOnly (e.g. cached undef
from an empty list scalar() call), replace it with a mutable
RuntimeScalar before assigning. This prevents Modification of a
read-only value attempted errors in the bytecode interpreter.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
The bytecode compiler only handled scalar variables in local lists
like local ($x, *GLOB, @arr, %hash). Typeglob, array, and hash
elements were silently skipped. This fix adds proper LOCAL_GLOB,
LOCAL_ARRAY, and LOCAL_HASH opcode emission for all variable types
in the list form of local.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
The eval STRING compiler path called createClassWithMethod directly
without marking the AST block as blockIsSubroutine. This caused
EmitBlock to emit redundant block-level regex state save/restore,
which clobbered $1/$&/etc before the return value was collected.

Also add getList() override to ScalarSpecialVariable to eagerly
materialize regex capture values, preventing stale lazy references.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
Two bugs fixed:

1. `undef $scalar` was not working in the bytecode interpreter because
   CompileOperator compiled it identically to bare `undef` — it never
   modified the variable. Now emits UNDEFINE_SCALAR opcode on the
   operand register before loading undef into the result register.

2. RuntimeScalarReadOnly values leaked into variable registers and hash
   elements, causing "Modification of a read-only value attempted"
   errors in large functions using the interpreter backend. Root cause:
   MathOperators returns cached RuntimeScalarReadOnly from getScalarInt()
   (e.g. unaryMinus(-1)), and MOVE copied the reference directly into
   variable registers. Fixed by:
   - MOVE: unwrap RuntimeScalarReadOnly into mutable RuntimeScalar
   - HASH_SET: unwrap before storing in hash
   - INC_REG/DEC_REG: unwrap results before storing back
   - All compound assignment opcodes (+=, -=, *=, /=, %=, .=, etc.):
     ensure target register is mutable before in-place modification
   - Added ensureMutableScalar() helper for consistent unwrapping

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
Java's regex engine can hang indefinitely on certain patterns due to
exponential backtracking inside Matcher.find(). This wraps regex input
with a TimeoutCharSequence that checks elapsed time every 4096 charAt()
calls and aborts after 10 seconds with a warning.

Applied to all three regex entry points: match (m//), replace (s///),
and split. Fixes #254.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
… interpreter

The bytecode interpreter read-only checks only caught RuntimeScalarReadOnly
but missed ScalarSpecialVariable (e.g. $1, $&), which also extends
RuntimeBaseProxy and is effectively immutable. This caused read-only errors
when registers containing special variables were used in mutating operations.

- Add ScalarSpecialVariable handling to ensureMutableScalar()
- Introduce isImmutableProxy() helper that checks both types
- Replace all instanceof RuntimeScalarReadOnly guards with isImmutableProxy()
  across BytecodeInterpreter, OpcodeHandlerExtended, and SlowOpcodeHandler
- Make ScalarSpecialVariable.getValueAsScalar() public for cross-package access

Fixes ExifTool PLUS.t test 2 crash at STRING_CONCAT_ASSIGN opcode.

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
- Fix parser to correctly handle `sort SubName @list` syntax by
  detecting bare identifier comparators before the try/catch path,
  constructing a code reference AST node directly instead of letting
  the sub be called with the list as arguments
- Propagate current package to sub-compilers in BytecodeCompiler for
  both named and anonymous subroutines, fixing package resolution
  when bytecode interpreter fallback is triggered
- Add string-to-coderef resolution in ListOperators.sort() for
  comparators passed as string subroutine names

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
- Fix invalidateCacheForClass to also remove the exact class entry
  from linearizedClassesCache (was only removing subclass entries
  matching "className::" prefix, missing the class itself)
- Fix splice to handle null array elements (from $#a++) by replacing
  them with undef RuntimeScalar instead of propagating Java null

These fix splice.t tests 30-32 (splice @isa cache invalidation,
splice with nonexistent array elements).

Generated with [Devin](https://cli.devin.ai/docs)

Co-Authored-By: Devin <noreply@cognition.ai>
@fglock fglock merged commit 72748ef into master Mar 1, 2026
2 checks passed
@fglock fglock deleted the fix-test-regressions branch March 1, 2026 15:39
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant