diff --git a/dev/design/cpan_client.md b/dev/design/cpan_client.md index 430e6a3d5..af52594b7 100644 --- a/dev/design/cpan_client.md +++ b/dev/design/cpan_client.md @@ -427,35 +427,65 @@ When a built-in function like `shift`, `pop`, `caller`, etc. is followed by `->` **Files changed**: - `src/main/java/org/perlonjava/frontend/parser/Variable.java` - Added check for built-in functions followed by `->` +- [x] **Phase 9a: YAML version update** (2026-03-17) + - Updated YAML.pm $VERSION from 0.01 to 1.31 + - Silences "YAML version too low" warning in CPAN.pm + - CPAN.pm requires >= 0.60; our YAML::PP-based implementation is fully capable + +- [x] **Phase 9b: Module::Build partial support** (2026-03-18) + - Added `Module::Build::Base` stub that overrides `have_forkpipe()` to return 0 + - When Module::Build is installed via system Perl's CPAN, PerlOnJava can use it + - Fork pipes are disabled, forcing backticks/system() instead + - This allows some Module::Build-based distributions to work + - **Limitation**: Module::Build itself is not bundled; must be installed separately + +### Files Changed (Phase 9b) +- `src/main/perl/lib/Module/Build/Base.pm` - Stub that loads real Module::Build and disables fork pipes + ### Next Steps -#### Phase 9: Extended Compatibility -1. **Module::Build support** - Medium priority - - Some CPAN modules use Module::Build instead of MakeMaker - - Needs stub similar to ExtUtils::MakeMaker - - Blocks: modules that only provide Build.PL +#### Phase 10: Further Compatibility Improvements -2. ~~**Core module detection**~~ - ✅ Resolved - - CPAN::DistnameInfo now installable via jcpan - - Warning about it no longer appears +1. **Test harness improvements** - Low priority + - `make test` uses fork which isn't fully supported + - Current workaround: `notest("install", "Module")` or use `-f` flag + - Test::Harness subprocess spawning could be improved -3. **Test running improvements** - Low priority - - `make test` uses fork which isn't supported in PerlOnJava - - Current workaround: `notest("install", "Module")` - - Long-term: Consider IPC::Open3 for test harness +2. **CPAN shell experience** - Medium priority + - Some warnings during module installation could be suppressed + - Better error messages for XS module detection -4. ~~**YAML.pm improvements**~~ - ✅ FIXED - - Updated YAML.pm version to 1.31 (matches CPAN version) - - "YAML version '0.01' is too low" warning no longer appears - - Our YAML.pm wraps YAML::PP which provides full functionality +3. **Dependency graph improvements** - Low priority + - CPAN.pm sometimes tries to install core modules unnecessarily + - Could benefit from better @INC handling -- [x] **Phase 9a: YAML version update** (2026-03-17) - - Updated YAML.pm $VERSION from 0.01 to 1.31 - - Silences "YAML version too low" warning in CPAN.pm - - CPAN.pm requires >= 0.60; our YAML::PP-based implementation is fully capable +### Current jcpan Capabilities (as of 2026-03-19) + +**Working well:** +```bash +# Install pure Perl module +jcpan install Try::Tiny + +# Install with force (skip tests) +jcpan -f install Module::Name + +# Test a module +jcpan -t Module::Name + +# Interactive shell +jcpan +cpan> install Module::Name +``` + +**Known limitations:** +- XS modules require manual porting (see `.cognition/skills/port-cpan-module/`) +- Module::Build-only modules need Module::Build installed separately +- Tests that heavily use fork may fail or skip +- Safe.pm compartment restrictions are not enforced ### Open Questions - How important is Safe compartmentalization for users? +- Should we bundle Module::Build or keep it as an optional external dependency? ### Resolved Questions - ✅ User-friendly installer: `jcpan` wrapper script provides `jcpan install Module` command @@ -467,3 +497,30 @@ When a built-in function like `shift`, `pop`, `caller`, etc. is followed by `->` - ✅ Try::Tiny compatibility: `try`/`catch` now feature-gated, module works correctly - ✅ parse_version: Implemented using regex extraction to avoid package block scoping issues in compiled modules - ✅ Makefile creation: Stub Makefile satisfies CPAN.pm's checks +- ✅ YAML version: Updated to 1.31, silences "version too low" warning +- ✅ Module::Build partial: Base.pm stub disables fork pipes, allowing external Module::Build to work + +## Progress Tracking (Updated 2026-03-19) + +### Current Status: Phase 9 complete - CPAN client fully functional for pure Perl modules + +### Summary Statistics +- **Phases completed:** 9 (plus sub-phases 9a, 9b) +- **Pure Perl modules:** Can be installed via `jcpan install Module::Name` +- **Test harness:** Works with some limitations (fork-heavy tests may skip) +- **Build systems:** ExtUtils::MakeMaker (native), Module::Build (partial via external) + +### Completed Phases +- [x] Phase 1: Low-hanging fruit (DirHandle, Dumpvalue, Sys::Hostname, flock) +- [x] Phase 2: Archive/Network (IO::Socket, Archive::Tar, Net::FTP) +- [x] Phase 3: Process Control (IPC::Open2, IPC::Open3) +- [x] Phase 4: Archive::Zip + cpanm analysis +- [x] Phase 5: ExtUtils::MakeMaker implementation +- [x] Phase 6: CPAN.pm support + Safe.pm stub +- [x] Phase 7: Errno dualvar + regex fixes +- [x] Phase 8: jcpan wrapper script +- [x] Phase 9a: YAML version update +- [x] Phase 9b: Module::Build partial support + +### Active Development +- [ ] Phase 10: Further compatibility improvements (low priority) diff --git a/dev/design/log4perl-compatibility.md b/dev/design/log4perl-compatibility.md index ef0291dbf..423c8dc47 100644 --- a/dev/design/log4perl-compatibility.md +++ b/dev/design/log4perl-compatibility.md @@ -4,66 +4,49 @@ This document tracks the work needed to make `./jcpan Log::Log4perl` fully pass its test suite on PerlOnJava. -## Current Status (2026-03-19) +## Current Status (2026-03-19, Updated) ### Test Results ``` Files=73, Tests=700 -Failed 8/73 test programs -Failed 26/700 subtests +Failed 6/73 test programs +Failed 18/700 subtests ``` +**Improvement from previous run:** Was 8/73 programs, 26/700 subtests failing. + ### Failing Tests Summary | Test File | Failed/Total | Issue Category | |-----------|--------------|----------------| -| t/016Export.t | 1/16 | DESTROY message | -| t/020Easy.t | 3/21 | caller() / Carp line numbers | -| t/022Wrap.t | 2/5 | caller() stack trace format | -| t/024WarnDieCarp.t | 11/73 | caller() / Carp line numbers | -| t/026FileApp.t | 3/27 | File permissions / substr issues | -| t/041SafeEval.t | 3/23 | Safe.pm / Opcode.pm | +| t/016Export.t | 1/16 | DESTROY message during global destruction | +| t/022Wrap.t | 2/5 | %T (stack trace) format - too many frames | +| t/024WarnDieCarp.t | 8/73 | caller() line numbers wrong (reports EOF) | +| t/026FileApp.t | 3/27 | File permissions / chmod | +| t/041SafeEval.t | 3/23 | Safe.pm compartment restrictions | | t/049Unhide.t | 1/1 | Source filter / ###l4p | -| t/051Extra.t | 2/11 | Line number reporting | - -### Current Investigation: t/020Easy.t Carp.pm Error - -**Status:** Partially debugged - the error is intermittent and context-dependent. -**Symptom:** -``` -Can't use an undefined value as a GLOB reference at jar:PERL5LIB/Carp.pm line 755 -``` +### Tests Now Passing (since original doc) -**Key Finding:** The error occurs when: -1. A bareword filehandle `IN` is opened and read from (``) -2. Log4perl's `%T` layout is used (which calls `Carp::longmess()`) -3. The `%T` pattern is rendered during logging +| Test File | Previous | Current | What Fixed It | +|-----------|----------|---------|---------------| +| t/020Easy.t | 3/21 failed | All pass | local $pkg::var bug fixed, bareword IO handles | +| t/051Extra.t | 2/11 failed | All pass | Line number reporting improvements | +| t/024WarnDieCarp.t | 11/73 failed | 8/73 failed | Partial improvement in caller() | -**Reproduction Path (simplified):** -```perl -open IN, "<", "somefile"; -my @lines = ; # Sets ${^LAST_FH} -use Carp; -my $m = Carp::longmess(); # Sometimes fails with undef GLOB -``` +### Resolved: t/020Easy.t Carp.pm Error -**What's NOT the issue:** -- `*{NAME}` slot - now implemented and working -- `local *$dynamic` - now implemented for interpreter backend -- `${^LAST_FH}` basic functionality - works in isolation +**Status:** FIXED - all 21 tests now pass. -**Investigation Notes:** -- The error happens at Carp.pm line 752: `*{"warnings::$_"} = \&$_ foreach @EXPORT;` -- This code runs when `$warnings::VERSION` is undefined (which it is in PerlOnJava's warnings.pm) -- The bareword filehandle name check (`*{${^LAST_FH}}{NAME}`) now works -- Error is NOT reproducible in simple test cases - only in specific call stack contexts -- May be related to how Carp.pm is loaded/initialized in the presence of active I/O +The Carp.pm error (`Can't use an undefined value as a GLOB reference`) was fixed by a combination of: +- `local $pkg::var` bug fix (PR #333) +- Bareword filehandle method call fix (`IN->clearerr()`) +- `*{NAME}` glob slot implementation -**Next Steps:** -1. Add `$VERSION` to PerlOnJava's warnings.pm to skip the problematic code path -2. Or investigate why `$_` becomes undefined during the foreach loop in certain contexts +The investigation notes below are kept for historical reference: +- The error happened at Carp.pm line 752 when `$warnings::VERSION` was undefined +- Fixed without needing to add `$VERSION` to warnings.pm - the underlying glob/local issues were resolved ## Completed Fixes @@ -187,44 +170,66 @@ BEGIN failed--compilation aborted at -e line 1, near "" - `splitpath()` returning wrong component - Newlines between sigil and variable name -## Remaining Issues +## Remaining Issues (Updated 2026-03-19) -### Issue 1: `local` Package Variable Bug - VERIFIED FIXED +### Issue 1: caller() Line Number Reporting - ACTIVE -**Status:** The basic reproduction case now works correctly. Need to verify if Log4perl tests improved. +**Status:** Partially working but still has issues in some contexts. -**Verification (2026-03-19):** -```perl -package Foo; -our $X = 0; -sub check { print "X=$X\n"; } +**Symptom:** t/024WarnDieCarp.t tests fail because caller() reports wrong line numbers. Instead of the actual call site, it reports line 397 (end of file). -package main; -local $Foo::X = 1; -Foo::check(); # jperl now correctly prints "X=1" +**Example failure:** +``` +got: 't/024WarnDieCarp.t-397: Inferno! at t/024WarnDieCarp.t line 193.' +expected to match: '184' (the line where logcroak was called) ``` -The issue may have been fixed by earlier changes (possibly the `our` variable handling in SymbolTable). The design doc at `dev/design/local-package-variable-fix.md` was created but implementation may not have been needed. +**Affected Tests:** +- t/024WarnDieCarp.t (8 failures: tests 51-53, 58, 60, 62, 67, 69) + +**Investigation Notes:** +- The file line (193 in example) is correct for the immediate caller +- The logging line (397) is the end of file - suggests stack frame issue +- May be related to how Log4perl wraps caller() for %F/%L/%T patterns + +### Issue 2: Stack Trace Format (%T) - ACTIVE -**Note:** Some Log4perl tests still fail with caller() / Carp line number issues - these may be separate from the `local` issue. +**Status:** Working but includes too many frames. -### Issue 2: Carp.pm / warnings.pm Interaction +**Symptom:** t/022Wrap.t tests fail because %T (Carp::longmess) includes internal Log4perl frames. -**Symptom:** t/020Easy.t tests 17-21 - error after %T logging: +**Example:** ``` -Can't use an undefined value as a GLOB reference at jar:PERL5LIB/Carp.pm line 755 +got: 'trace: Log::Log4perl::Layout::PatternLayout::render() called at ... line 306, + Log::Log4perl::Appender::log() called at ... line 1115, ...' +expected: 'trace: at 022Wrap.t line 69' ``` -**Root Cause:** PerlOnJava's warnings.pm lacks `$VERSION`, causing Carp.pm to execute a workaround code path (line 752) that fails in certain contexts. +**Root Cause:** PerlOnJava's Carp::longmess includes all stack frames. Perl's version filters out internal frames based on `@CARP_NOT` and caller level adjustments that Log4perl uses. + +**Affected Tests:** +- t/022Wrap.t (2 failures: tests 1-2) + +### Issue 3: DESTROY During Global Destruction -**Proposed Fix:** Add `our $VERSION = "1.78";` to PerlOnJava's warnings.pm to skip the problematic code path. +**Status:** DESTROY not called or output not captured. + +**Symptom:** t/016Export.t test 16 fails - expected DESTROY message not appearing. + +**Test:** +```perl +# Expected: 'Log::Log4perl::Appender::TestBuffer destroyed' +# Got: '' +``` -**Note:** 4 of the 5 failing tests in t/020Easy.t are just filename pattern mismatches (test expects "020Easy.t" but gets "-" from stdin). Only 1 failure is the Carp.pm error. +**Root Cause:** The `DESTROY` method on TestBuffer may not be called during global destruction, or the message is printed but not captured by the test framework. **Affected Tests:** -- t/020Easy.t (tests 17-21) +- t/016Export.t (1 failure) + +### Issue 4: File Permissions (stat/chmod) -### Issue 3: File Permissions (stat/chmod) +**Status:** Unchanged - needs investigation. **Symptom:** t/026FileApp.t tests 6-7 fail comparing expected vs actual file permissions. @@ -239,47 +244,60 @@ Can't use an undefined value as a GLOB reference at jar:PERL5LIB/Carp.pm line 75 **Affected Tests:** - t/026FileApp.t (tests 6-7, 25) -### Issue 4: Safe.pm / Opcode.pm +### Issue 5: Safe.pm Compartment Restrictions -**Symptom:** t/041SafeEval.t tests 4-5, 20 fail. +**Status:** Safe.pm stub doesn't enforce opcode restrictions. -**Root Cause:** PerlOnJava's Safe.pm implementation may not properly restrict opcodes. +**Symptom:** t/041SafeEval.t tests 4-5, 20 fail. Code that should be blocked by restrictive Safe settings still executes. + +**Test expectation:** When `ALLOW_CODE_IN_CONFIG_FILE` is true with a restrictive mask, harmful code should be blocked. + +**Root Cause:** PerlOnJava's Safe.pm stub uses plain `eval` with `no strict 'vars'` - it doesn't actually restrict any operations. **Affected Tests:** - t/041SafeEval.t (3 failures) -### Issue 5: Source Filters (###l4p) +### Issue 6: Source Filters (###l4p) + +**Status:** Source filters not supported. **Symptom:** t/049Unhide.t fails - the `###l4p` source filter mechanism doesn't work. -**Root Cause:** Log::Log4perl uses a source filter to hide/unhide statements prefixed with `###l4p`. PerlOnJava may not support this source filtering. +**Root Cause:** Log::Log4perl uses a source filter to hide/unhide statements prefixed with `###l4p`. PerlOnJava doesn't support source filtering. **Affected Tests:** - t/049Unhide.t (1 failure) -### Issue 6: DESTROY Message +## Resolved Issues -**Symptom:** t/016Export.t test 16 fails - expected DESTROY message not appearing. +### RESOLVED: Issue 1 (Previous): `local` Package Variable Bug + +**Status:** FIXED in PR #333 (2026-03-19) -**Test:** ```perl -# Expected: 'Log::Log4perl::Appender::TestBuffer destroyed' -# Got: '' +package Foo; +our $X = 0; +sub check { print "X=$X\n"; } + +package main; +local $Foo::X = 1; +Foo::check(); # jperl now correctly prints "X=1" ``` -**Root Cause:** The `DESTROY` method on TestBuffer may not be called during global destruction, or the message is not being captured correctly. +### RESOLVED: Issue 2 (Previous): Carp.pm / warnings.pm Interaction -**Affected Tests:** -- t/016Export.t (1 failure) +**Status:** FIXED - t/020Easy.t now passes all 21 tests. + +The Carp.pm error was resolved by fixing the underlying `local` and glob issues. -## Priority Order +## Priority Order (Updated) -1. **`local` package variable bug** - ROOT CAUSE affecting 16+ tests across multiple files -2. **Carp.pm/warnings.pm interaction** - Separate issue, quick fix possible -3. **DESTROY message** - May be a minor timing/output issue -4. **File permissions** - Likely straightforward fix -5. **Safe.pm** - May require significant work -6. **Source filters** - May require parser changes +1. **caller() line number reporting** - Affects 8+ tests, needs stack frame investigation +2. **%T stack trace filtering** - Affects 2 tests, may need Carp.pm adjustments +3. **DESTROY during global destruction** - 1 test, may be fundamental JVM limitation +4. **File permissions** - 3 tests, likely straightforward fix +5. **Safe.pm restrictions** - 3 tests, requires significant architectural work +6. **Source filters** - 1 test, requires parser changes ## Recent Debugging Session (2026-03-18) @@ -348,27 +366,101 @@ These tests pass locally but exceed CI timeout limits. The CI may need longer ti ./jcpan -t Log::Log4perl # Run a specific test -cd ~/.cpan/build/Log-Log4perl-1.57* && /path/to/jperl t/020Easy.t +cd ~/.cpan/build/Log-Log4perl-1.57* && /path/to/jperl t/024WarnDieCarp.t # Quick test for bareword filehandle ./jperl -e 'open IN, "clearerr(); print "OK\n"; close IN;' # Quick test for $( and $) ./jperl -e 'print "GID: $(\nEGID: $)\n";' + +# Test caller() behavior +./jperl -e 'sub foo { print caller(), "\n"; } sub bar { foo(); } bar();' ``` ## Files to Investigate -For Carp.pm fix: -- `src/main/perl/lib/Carp.pm` - line 755 -- `src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java` - `caller()` method - -For caller() fix: +For caller() line number fix: - `src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java` - `caller()` method - `src/main/java/org/perlonjava/runtime/ExceptionFormatter.java` +- `src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java` - line number tracking + +For %T (Carp) stack trace: +- `src/main/perl/lib/Carp.pm` - longmess(), shortmess() +- May need @CARP_NOT handling adjustments + +For DESTROY: +- `src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java` - object destruction +- Global destruction order in Main.java + +For chmod/umask: +- `src/main/java/org/perlonjava/runtime/operators/IOOperator.java` - chmod implementation +- Check umask application ## Related Documentation - Perl's IO::Handle: https://perldoc.perl.org/IO::Handle - Perl's caller(): https://perldoc.perl.org/functions/caller - Log::Log4perl: https://metacpan.org/pod/Log::Log4perl + +## Progress Tracking + +### Current Status: 18/700 subtests failing (was 26/700) + +### Completed +- [x] *{NAME} glob slot accessor (2026-03-18) +- [x] local *$dynamic interpreter support (2026-03-18) +- [x] gethostbyname interpreter opcode (2026-03-18) +- [x] Bareword filehandle method calls (2026-03-18) +- [x] $( and $) special variables (2026-03-18) +- [x] exit() inside BEGIN blocks (2026-03-19) +- [x] local $Pkg::Var bug fix (2026-03-19, PR #333) + +### Active Issues +- [ ] caller() line number reporting (8 tests) +- [ ] %T stack trace format (2 tests) +- [ ] DESTROY during global destruction (1 test) +- [ ] chmod/file permissions (3 tests) +- [ ] Safe.pm restrictions (3 tests) +- [ ] Source filters (1 test) + +### Next Steps +1. Investigate caller() line number issue in t/024WarnDieCarp.t +2. Consider Log4perl's caller level adjustment mechanism +3. Check if Carp.pm @CARP_NOT is being respected + +--- + +## Related: Try::Tiny Test Analysis (2026-03-19) + +### Test Results (After Fix) +``` +Files=11, Tests=67 +Failed 4/11 test programs, 7/67 subtests failed +``` + +**Improvement:** Was 5/11 failing, 9/67 subtests. Fixed shift bug in t/basic.t. + +### Fixed Bug: `shift` in `(&)` Prototype Blocks + +**Commit:** da227ff44 + +**Root Cause:** When a block was captured via `(&)` prototype (e.g., Try::Tiny's `catch { }`), the parser was not setting `isInSubroutineBody` flag. This caused implicit `shift`/`pop` to default to `@ARGV` instead of `@_`. + +**Fix:** In `PrototypeArgs.handleCodeReferenceArgument()`, save and set `isInSubroutineBody(true)` before parsing the block, then restore it afterward. + +**Files Changed:** +- `src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java` + +### Remaining Failures (Expected/Acceptable) + +| Test | Failed | Category | Details | +|------|--------|----------|---------| +| t/context.t | 12/25 | DESTROY | `finally` blocks use DESTROY scope guards | +| t/finally.t | 19/30 | DESTROY | Same - finally not running | +| t/global_destruction_forked.t | 3/3 | DESTROY | Tests global destruction with fork | +| t/named.t | 3/3 | caller() | `set_subname` works but `caller()[3]` doesn't reflect it | + +### Separate Issue: caller() and set_subname + +`Sub::Util::set_subname` correctly stores the name (verified via `subname()`), but `caller(0)[3]` doesn't return it. This affects t/named.t but is a separate issue. diff --git a/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java b/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java index e1d7cd5da..39d22889f 100644 --- a/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java +++ b/src/main/java/org/perlonjava/frontend/parser/PrototypeArgs.java @@ -499,12 +499,22 @@ private static boolean handleCodeReferenceArgument(Parser parser, ListNode args, if (TokenUtils.peek(parser).text.equals("{")) { TokenUtils.consume(parser); - Node block = new SubroutineNode(null, null, null, ParseBlock.parseBlock(parser), false, parser.tokenIndex); - TokenUtils.consume(parser, LexerTokenType.OPERATOR, "}"); - // Code references/blocks are evaluated in SCALAR context - block.setAnnotation("context", "SCALAR"); - args.elements.add(block); - return false; + + // Save and set subroutine body context so that shift/pop default to @_ instead of @ARGV + boolean previousInSubroutineBody = parser.ctx.symbolTable.isInSubroutineBody(); + parser.ctx.symbolTable.setInSubroutineBody(true); + + try { + Node block = new SubroutineNode(null, null, null, ParseBlock.parseBlock(parser), false, parser.tokenIndex); + TokenUtils.consume(parser, LexerTokenType.OPERATOR, "}"); + // Code references/blocks are evaluated in SCALAR context + block.setAnnotation("context", "SCALAR"); + args.elements.add(block); + return false; + } finally { + // Restore previous subroutine body context + parser.ctx.symbolTable.setInSubroutineBody(previousInSubroutineBody); + } } Node codeRef = parseRequiredArgument(parser, isOptional); diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java index dd3717cef..6205ab606 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java +++ b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessor.java @@ -1240,18 +1240,7 @@ private static int handleCharacterClass(String s, boolean flag_xx, StringBuilder StringBuilder rejected = new StringBuilder(); offset = RegexPreprocessorHelper.handleRegexCharacterClassEscape(offset, s, sb, length, flag_xx, rejected); - if (!rejected.isEmpty()) { - // Process \b inside character class - String subseq; - if ((sb.length() - len) == 2) { - subseq = "(?:" + rejected + ")"; - } else { - subseq = "(?:" + sb.substring(len) + "|" + rejected + ")"; - } - rejected.setLength(0); - sb.setLength(len); - sb.append(subseq); - } + // Note: rejected is kept for future use but currently \b is handled by direct substitution to \x08 return offset; } diff --git a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessorHelper.java b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessorHelper.java index 273da9bc7..13e94614b 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessorHelper.java +++ b/src/main/java/org/perlonjava/runtime/regex/RegexPreprocessorHelper.java @@ -630,9 +630,16 @@ static int handleRegexCharacterClassEscape(int offset, String s, StringBuilder s RegexPreprocessor.regexError(s, offset, "Missing right brace on \\o{}"); } } else if (s.codePointAt(offset) == 'b') { - rejected.append("\\b"); // Java doesn't support \b inside [...] - offset++; - lastChar = -1; + // \b inside character class = backspace in Perl + // Java doesn't support \b in [...], so convert to \x08 + // Remove the \ that was already appended to sb + sb.setLength(sb.length() - 1); + // Use \x08 directly in the class (works for ranges too) + sb.append("\\x08"); + // Don't increment offset here - the outer loop will do it + lastChar = 0x08; // Backspace character for range validation + first = false; + afterCaret = false; } else { int c2 = s.codePointAt(offset); if (c2 >= '0' && c2 <= '7') {