diff --git a/dev/design/log4perl-compatibility.md b/dev/design/log4perl-compatibility.md new file mode 100644 index 000000000..0fc7f7e65 --- /dev/null +++ b/dev/design/log4perl-compatibility.md @@ -0,0 +1,354 @@ +# Log::Log4perl Compatibility Plan + +## Overview + +This document tracks the work needed to make `./jcpan Log::Log4perl` fully pass its test suite on PerlOnJava. + +## Current Status (2026-03-18) + +### Test Results + +``` +Files=73, Tests=695 +Failed 8/73 test programs (down from 9) +Failed 23/695 subtests (down from 28) +``` + +### Failing Tests Summary + +| Test File | Failed/Total | Issue Category | +|-----------|--------------|----------------| +| t/016Export.t | 1/16 | DESTROY message | +| t/020Easy.t | 5/21 | Carp.pm undef GLOB reference (4 are filename mismatches) | +| 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/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 +``` + +**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 + +**Reproduction Path (simplified):** +```perl +open IN, "<", "somefile"; +my @lines = ; # Sets ${^LAST_FH} +use Carp; +my $m = Carp::longmess(); # Sometimes fails with undef GLOB +``` + +**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 + +**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 + +**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 + +## Completed Fixes + +### 1. *{NAME} Glob Slot Accessor (Committed 2026-03-18) + +**Problem:** `*{$glob}{NAME}` returned empty string instead of the glob's name. + +**Root Cause:** The NAME slot was not implemented in RuntimeGlob's `getSlot()` method. + +**Fix:** Added case for "NAME" that extracts the name from globName after the last `::`. + +**Files Changed:** +- `src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java` + +**Commit:** 0a5e92556 + +### 2. Interpreter: local *$dynamic Support (Committed 2026-03-18) + +**Problem:** `local *$probe = sub { ... }` failed with "Assignment to unsupported operator: local" + +**Root Cause:** The interpreter's `handleLocalAssignment()` only handled static glob names, not dynamic ones like `*$probe`. + +**Fix:** Added case for dynamic glob names (when operand is not IdentifierNode) using LOAD_GLOB_DYNAMIC opcode. + +**Files Changed:** +- `src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java` + +**Commit:** 68d295287 + +### 3. Interpreter: gethostbyname Opcode (Committed 2026-03-18) + +**Problem:** `gethostbyname` was not implemented in the interpreter backend. + +**Fix:** Added GETHOSTBYNAME opcode (389) and routing to ExtendedNativeUtils. + +**Files Changed:** +- `src/main/java/org/perlonjava/backend/bytecode/Opcodes.java` +- `src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java` +- `src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java` + +**Commit:** 68d295287 + +### 4. Bareword Filehandle Method Calls (Committed 2026-03-18) + +**Problem:** `IN->clearerr()` failed with "Can't locate object method 'clearerr' via package 'IN'" + +**Root Cause:** `isGlobalIODefined()` was checking `glob.value instanceof RuntimeIO` but IO handles are stored in `glob.IO`, not `glob.value`. + +**Fix:** Changed `isGlobalIODefined()` to check `glob.IO.getDefinedBoolean()` instead. + +**Files Changed:** +- `src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java` + +**Commit:** 3d0bf9b59 + +**Tests Fixed:** Unblocked 15+ tests in t/020Easy.t (from 1 to 16 passing) + +### 5. $( and $) Special Variables (Committed 2026-03-18) + +**Problem:** `$(` and `$)` (real/effective GID) were not working - returned literal `$(` in strings. + +**Root Cause:** +1. Variables not initialized with actual GID values +2. `(` and `)` not in special variable character lists +3. `(` and `)` in non-interpolating character list blocked string interpolation + +**Fix:** +- Initialize `$(` and `$)` in GlobalContext with `getgid()`/`getegid()` values +- Add `(` and `)` to special variable character lists in IdentifierParser +- Remove `(` and `)` from non-interpolating character list + +**Files Changed:** +- `src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java` +- `src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java` +- `src/main/java/org/perlonjava/frontend/parser/StringSegmentParser.java` + +**Commit:** a82bf0c66 + +**Tests Fixed:** t/033UsrCspec.t - all 17 tests now pass (was 5 failing) + +### 6. AUTOLOAD $AUTOLOAD Variable (Committed Earlier) + +**Problem:** `$AUTOLOAD` was not being set correctly in two scenarios: +1. Method call cache hits were skipping the `$AUTOLOAD` assignment +2. `our $AUTOLOAD` declared in different packages within the same lexical scope was silently ignored + +**Fix:** +- Added `$AUTOLOAD` assignment in the cache hit path in `RuntimeCode.java` +- Modified `SymbolTable.addVariable()` to create new entries for `our` declarations in different packages + +**Files Changed:** +- `src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java` +- `src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java` + +**Commit:** 9f2d0aaf2 + +### 7. Parser/Runtime Fixes (Committed Earlier) + +- `sprintf %s` treating "INFO" as Infinity +- `oct("0")` crash +- `splitpath()` returning wrong component +- Newlines between sigil and variable name + +## Remaining Issues + +### Issue 1: Carp.pm / warnings.pm Interaction + +**Symptom:** t/020Easy.t tests 17-21 - error after %T logging: +``` +Can't use an undefined value as a GLOB reference at jar:PERL5LIB/Carp.pm line 755 +``` + +**Root Cause:** PerlOnJava's warnings.pm lacks `$VERSION`, causing Carp.pm to execute a workaround code path (line 752) that fails in certain contexts. + +**Proposed Fix:** Add `our $VERSION = "1.78";` to PerlOnJava's warnings.pm to skip the problematic code path. + +**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. + +**Affected Tests:** +- t/020Easy.t (tests 17-21) + +### Issue 2: caller() Stack Trace Format + +**Symptom:** Stack traces from `Carp::shortmess` include internal PerlOnJava frames. + +**Example from t/022Wrap.t:** +``` +Expected: 'File: 022Wrap.t Line number: 70 package: main trace: at 022Wrap.t line 70' +Got: 'File: 022Wrap.t Line number: 70 package: main trace: Log::Log4perl::Appender::log() called at ... line 1115, ...' +``` + +**Root Cause:** The `caller()` implementation is exposing internal call frames that Perl would filter out. + +**Fix Needed:** Review `ExceptionFormatter.formatException()` and filter out internal frames from Log::Log4perl's perspective. + +**Affected Tests:** +- t/022Wrap.t (2 failures) +- t/024WarnDieCarp.t (11 failures) - tests 51-53, 58-62, 67, 69-70 +- t/051Extra.t (2 failures) - line number reporting + +### Issue 3: File Permissions (stat/chmod) + +**Symptom:** t/026FileApp.t tests 6-7 fail comparing expected vs actual file permissions. + +**Example:** +```perl +# Expected: '488' (octal 0750) +# Got: '511' (octal 0777) +``` + +**Root Cause:** Likely issue with `umask` handling or `chmod` implementation. + +**Affected Tests:** +- t/026FileApp.t (tests 6-7, 25) + +### Issue 4: Safe.pm / Opcode.pm + +**Symptom:** t/041SafeEval.t tests 4-5, 20 fail. + +**Root Cause:** PerlOnJava's Safe.pm implementation may not properly restrict opcodes. + +**Affected Tests:** +- t/041SafeEval.t (3 failures) + +### Issue 5: Source Filters (###l4p) + +**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. + +**Affected Tests:** +- t/049Unhide.t (1 failure) + +### Issue 6: DESTROY Message + +**Symptom:** t/016Export.t test 16 fails - expected DESTROY message not appearing. + +**Test:** +```perl +# Expected: 'Log::Log4perl::Appender::TestBuffer destroyed' +# Got: '' +``` + +**Root Cause:** The `DESTROY` method on TestBuffer may not be called during global destruction, or the message is not being captured correctly. + +**Affected Tests:** +- t/016Export.t (1 failure) + +## Priority Order + +1. **Carp.pm undef GLOB** - Blocks 5 tests in t/020Easy.t +2. **caller() stack trace format** - Affects 15 tests across multiple files +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 + +## Recent Debugging Session (2026-03-18) + +### PR 328 Test Timeout Investigation + +**Symptom:** Tests reported as timeouts in PR 328: +``` +✗ io/crlf_through.t 942/942 0/0 -942 +✗ io/through.t 942/942 0/0 -942 +✗ op/heredoc.t 66/138 0/0 -66 +✗ op/tie.t 45/95 0/0 -45 +✗ lib/croak.t 44/334 0/334 -44 +``` + +**Root Cause Found:** Staged (but uncommitted) changes had accidentally removed the `ForkOpenCompleteException` catch blocks from `RuntimeCode.java`. These catch blocks are essential for the fork-open emulation feature added in commit 764c256cc. + +**Impact:** Without the exception handling: +- `exec` inside fork-open patterns (`open FH, "-|"; if (!$pid) { exec @cmd }`) throws an uncaught exception +- Tests that spawn subprocesses (fresh_perl_is, run_multiple_progs, pipe opens) hang or fail +- All tests using `test.pl`'s subprocess spawning are affected + +**Fix:** Restored the files from the committed version: +```bash +git reset HEAD -- . +git checkout -- . +``` + +**Verification:** After restoring, `grep -c "ForkOpenCompleteException"` returns 5 in RuntimeCode.java (correct). + +**Note:** The tests themselves are NOT broken - they just take longer than the CI timeout due to JVM startup overhead for each subprocess. The 0/0 results were from the uncaught exception causing early termination. + +### Next Steps for PR 328 + +1. **Ensure all files are committed with fork-open emulation intact** +2. **Consider increasing CI timeout** for subprocess-heavy tests (io/through.t has 942 tests) +3. **Always verify working tree is clean before testing** + +### Additional Fixes (2026-03-18) + +#### Fix: $( and $) in regex patterns +The commit adding `$(` and `$)` variable support caused ExifTool.t to fail because regex patterns containing `$)` were incorrectly interpolating the EGID variable instead of treating `$)` as end-of-string anchor + closing paren. + +**Fix:** Added check in `StringSegmentParser.shouldInterpolateVariable()` to skip interpolation of `$(` and `$)` specifically in regex context, while still allowing interpolation in double-quoted strings. + +#### Fix: sprintf %c with Inf/NaN regression +The sprintf fix for "INFO" being treated as Infinity incorrectly moved `%c` handling before the Inf/NaN check, causing `sprintf "%c", Inf` to format a character instead of erroring. + +**Fix:** Only skip the Inf/NaN check for `%s` (string) and `%p` (pointer), while `%c` still goes through the Inf/NaN check. + +**Result:** op/infnan.t restored from 1041/1088 back to 1071/1088 (same as master). + +### Remaining CI Timeout Issues + +The following tests timeout in CI but are NOT regressions - they just take a long time due to JVM startup overhead for subprocess-heavy tests: + +- **io/crlf_through.t** (942 tests) - spawns many subprocesses via pipe opens +- **io/through.t** (942 tests) - spawns many subprocesses via pipe opens +- **lib/croak.t** (334 tests) - spawns many subprocesses + +These tests pass locally but exceed CI timeout limits. The CI may need longer timeouts for these specific tests. + +## How to Test + +```bash +# Run all Log::Log4perl tests +./jcpan -t Log::Log4perl + +# Run a specific test +cd ~/.cpan/build/Log-Log4perl-1.57* && /path/to/jperl t/020Easy.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";' +``` + +## 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: +- `src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java` - `caller()` method +- `src/main/java/org/perlonjava/runtime/ExceptionFormatter.java` + +## 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 diff --git a/dev/design/pr328-startup-performance.md b/dev/design/pr328-startup-performance.md new file mode 100644 index 000000000..e3baf7532 --- /dev/null +++ b/dev/design/pr328-startup-performance.md @@ -0,0 +1,147 @@ +# PR #328 Startup Performance Investigation + +## Overview + +PR #328 introduced a 2x performance regression that caused io/through.t, io/crlf_through.t, and lib/croak.t tests to timeout in CI. This document details the investigation and fixes. + +## Root Cause + +Commit a82bf0c66 ("Add support for $( and $) special variables") introduced expensive JNA function calls during static initialization: + +```java +// In GlobalContext.java lines 84-85 (before fix) +GlobalVariable.getGlobalVariable("main::(").set(NativeUtils.getgid(0)); // $( - real GID +GlobalVariable.getGlobalVariable("main::)").set(NativeUtils.getegid(0)); // $) - effective GID +``` + +These JNA calls (`getgid` and `getegid`) are expensive (~5ms each) and were executed at every startup, including every subprocess fork. This doubled the startup time from ~80ms to ~160ms per process. + +## Performance Measurements + +| Scenario | Tests in 60s | Startup Time | +|----------|--------------|--------------| +| Master branch | 936-942 | ~80ms | +| After a82bf0c66 | 464-468 | ~160ms | +| After fix | 942 | ~140ms | + +The io/through.t test spawns many subprocesses, so a 2x startup slowdown resulted in exactly 2x fewer tests completing. + +## Fixes Applied + +### 1. Lazy $( and $) Variables (commit b462539b7) + +Made $( and $) lazy-loaded via `ScalarSpecialVariable` so JNA calls only happen when the variables are accessed: + +**ScalarSpecialVariable.java**: +```java +// Added to enum Id: +REAL_GID, // $( - Real group ID (lazy, JNA call only on access) +EFFECTIVE_GID, // $) - Effective group ID (lazy, JNA call only on access) + +// Added to getValueAsScalar(): +case REAL_GID -> { + yield new RuntimeScalar(NativeUtils.getgid(0)); +} +case EFFECTIVE_GID -> { + yield new RuntimeScalar(NativeUtils.getegid(0)); +} +``` + +**GlobalContext.java**: +```java +// Changed from eager initialization to lazy ScalarSpecialVariable +GlobalVariable.globalVariables.put("main::(", new ScalarSpecialVariable(ScalarSpecialVariable.Id.REAL_GID)); +GlobalVariable.globalVariables.put("main::)", new ScalarSpecialVariable(ScalarSpecialVariable.Id.EFFECTIVE_GID)); +``` + +### 2. Deferred Module Initialization (commit 15c5c1a83) + +Deferred initialization of less commonly used modules to XSLoader::load(): + +- `UnicodeNormalize` - Has XSLoader in its Perl file +- `TimeHiRes` - Has XSLoader in its Perl file (updated Time/HiRes.pm) +- `JavaSystem` - Only needed for java:: integration + +Modules kept at startup (no XSLoader in Perl files): +- `UnicodeUCD`, `TermReadLine`, `TermReadKey`, `FileTemp`, `Encode` + +## Current Performance Profile + +### Startup Time Breakdown (~140ms total) + +| Phase | Time | Notes | +|-------|------|-------| +| JVM startup | ~43ms | Inherent to Java | +| Class loading | ~14ms | 1444 classes loaded | +| Class initialization | ~50ms | Static blocks, HashMap setup | +| Bytecode verification | ~33ms | Security verification | + +### Profiling Commands Used + +```bash +# Measure tests per 60 seconds +cd perl5_t/t && timeout 60 ../../jperl io/through.t 2>&1 | grep "^ok" | wc -l + +# Measure single startup +time ./jperl -e '1' + +# Profile class loading +java -Xlog:class+load:file=/tmp/classload.log -cp "build/install/perlonjava/lib/*" \ + org.perlonjava.app.cli.Main -e '1' + +# Profile class initialization +java -Xlog:class+init=info:file=/tmp/classinit.log -cp "build/install/perlonjava/lib/*" \ + org.perlonjava.app.cli.Main -e '1' +``` + +## Why Further Optimization is Limited + +The ~140ms startup is near-optimal for a JVM application of this complexity: + +1. **JVM overhead is fixed** (~43ms) - Cannot be reduced without native compilation +2. **Class count is necessary** (1444 classes) - Required for Perl compatibility +3. **Static initialization is minimal** - Most work deferred to runtime + +### Potential Future Optimizations + +| Approach | Savings | Feasibility | +|----------|---------|-------------| +| GraalVM native-image | ~100ms | Low - breaks dynamic class loading | +| AppCDS (Class Data Sharing) | ~20-30ms | Medium - helps repeated runs only | +| Reduce class count | Variable | Low - requires major refactoring | +| Lazy module loading | ~5-10ms | Done - already implemented | + +## Files Modified + +1. `src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java` + - Added REAL_GID and EFFECTIVE_GID enum values + - Added lazy getters for $( and $) + +2. `src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java` + - Changed $( and $) to use ScalarSpecialVariable + - Deferred UnicodeNormalize, TimeHiRes, JavaSystem initialization + +3. `src/main/perl/lib/Time/HiRes.pm` + - Enabled XSLoader::load() call for lazy loading + +## Verification + +```bash +# Build and test +make + +# Verify io/through.t performance (should be ~940+ tests in 60s) +cd perl5_t/t && timeout 60 ../../jperl io/through.t 2>&1 | grep "^ok" | wc -l + +# Verify $( and $) still work +./jperl -e 'print "REAL_GID: $(; EFFECTIVE_GID: $)\n"' +``` + +## Related Issues + +- PR #328: Module::Build support +- Commit a82bf0c66: Add support for $( and $) special variables + +## Date + +2024-03-18 diff --git a/dev/design/todo.md b/dev/design/todo.md index aa4598eaf..8d30f3bb8 100644 --- a/dev/design/todo.md +++ b/dev/design/todo.md @@ -11,6 +11,10 @@ ## Cleanup - Cleanup the closure code to only add the lexical variables mentioned in the AST +- Refactor ScalarSpecialVariable: Override `scalar()` to return `getValueAsScalar()`, + then change `ReferenceOperators.ref()` to call `.scalar()` instead of the + `instanceof ScalarSpecialVariable` check. This keeps special handling in the + special variable class where it belongs. ## Local Variables - Set up localization in for-loop diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 9ad602449..d66a14a1d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -95,6 +95,36 @@ private static boolean handleLocalAssignment(BytecodeCompiler bc, BinaryOperator bc.lastResultReg = localReg; return true; } + // Handle dynamic glob names: local *$probe = sub { ... } + if (sigil.equals("*") && !(sigilOp.operand instanceof IdentifierNode)) { + // Compile the glob name expression (e.g., $probe) + bc.compileNode(sigilOp.operand, -1, RuntimeContextType.SCALAR); + int nameScalarReg = bc.lastResultReg; + + // Load the glob using dynamic name + int globReg = bc.allocateRegister(); + int pkgIdx = bc.addToStringPool(bc.getCurrentPackage()); + bc.emitWithToken(Opcodes.LOAD_GLOB_DYNAMIC, node.getIndex()); + bc.emitReg(globReg); + bc.emitReg(nameScalarReg); + bc.emit(pkgIdx); + + // Push the glob onto the local stack + bc.emit(Opcodes.PUSH_LOCAL_VARIABLE); + bc.emitReg(globReg); + + // Compile the RHS value + bc.compileNode(node.right, -1, rhsContext); + int valueReg = bc.lastResultReg; + + // Store value to glob + bc.emit(Opcodes.STORE_GLOB); + bc.emitReg(globReg); + bc.emitReg(valueReg); + + bc.lastResultReg = globReg; + return true; + } if (sigil.equals("our") && sigilOp.operand instanceof OperatorNode innerSigilOp && innerSigilOp.operand instanceof IdentifierNode idNode) { return handleLocalOurAssignment(bc, node, innerSigilOp, idNode, rhsContext); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java index 5352663e2..aadc9428b 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileOperator.java @@ -682,6 +682,7 @@ public static void visitOperator(BytecodeCompiler bytecodeCompiler, OperatorNode case "qx" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.QX); case "system" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.SYSTEM); case "kill" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.KILL); + case "gethostbyname" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.GETHOSTBYNAME); case "caller" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.CALLER); case "pack" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.PACK); case "unpack" -> visitGenericListOpCase(bytecodeCompiler, node, Opcodes.UNPACK); diff --git a/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java b/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java index 2c832bb97..1e5d6a8aa 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/MiscOpcodeHandler.java @@ -1,5 +1,6 @@ package org.perlonjava.backend.bytecode; +import org.perlonjava.runtime.nativ.ExtendedNativeUtils; import org.perlonjava.runtime.nativ.NativeUtils; import org.perlonjava.runtime.operators.*; import org.perlonjava.runtime.runtimetypes.*; @@ -85,6 +86,7 @@ public static int execute(int opcode, int[] bytecode, int pc, RuntimeBase[] regi case Opcodes.READDIR -> Directory.readdir(args.elements.isEmpty() ? null : (RuntimeScalar) args.elements.get(0), ctx); case Opcodes.SEEKDIR -> Directory.seekdir(args); + case Opcodes.GETHOSTBYNAME -> ExtendedNativeUtils.gethostbyname(ctx, argsArray); default -> throw new IllegalStateException("Unknown opcode in MiscOpcodeHandler: " + opcode); }; diff --git a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java index e84434cd3..92b528790 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java +++ b/src/main/java/org/perlonjava/backend/bytecode/Opcodes.java @@ -1892,6 +1892,13 @@ public class Opcodes { */ public static final short KILL = 380; + /** + * Get host information by name. + * Format: GETHOSTBYNAME rd args_reg ctx + * Effect: rd = ExtendedNativeUtils.gethostbyname(ctx, args...) + */ + public static final short GETHOSTBYNAME = 389; + // ================================================================= // SUPEROPERATORS (381+) - Combined instruction sequences // ================================================================= diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index 9fd82bfa8..e2f79daaf 100644 --- a/src/main/java/org/perlonjava/core/Configuration.java +++ b/src/main/java/org/perlonjava/core/Configuration.java @@ -33,14 +33,14 @@ 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 = "04c9bcbbe"; + public static final String gitCommitId = "7f9f95dd0"; /** * Git commit date of the build (ISO format: YYYY-MM-DD). * Automatically populated by Gradle/Maven during build. * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String gitCommitDate = "2026-03-17"; + public static final String gitCommitDate = "2026-03-18"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java b/src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java index 52b93d710..6df966824 100644 --- a/src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/IdentifierParser.java @@ -42,22 +42,19 @@ public static String parseComplexIdentifier(Parser parser, boolean isTypeglob) { // Save the current token index to allow backtracking if needed int saveIndex = parser.tokenIndex; - // Skip horizontal whitespace to find the start of the identifier - // (do not skip NEWLINE; "$\n" must be a syntax error) - int afterWs = parser.tokenIndex; - while (afterWs < parser.tokens.size() && parser.tokens.get(afterWs).type == LexerTokenType.WHITESPACE) { - afterWs++; - } + // Skip whitespace (including newlines) to find the start of the identifier. + // Perl allows newlines between sigil and variable name (e.g. "$ \n var" is valid). + int afterWs = Whitespace.skipWhitespace(parser, parser.tokenIndex, parser.tokens); boolean skippedWhitespace = afterWs != parser.tokenIndex; parser.tokenIndex = afterWs; // Whitespace between sigil and an identifier is allowed in Perl (e.g. "$ var"), // but whitespace characters themselves are not valid length-1 variable names. // If we consumed whitespace and the following token does not look like an identifier, - // treat it as a syntax error (e.g. "$\t", "$ ", "$\n"). + // treat it as a syntax error (e.g. "$\t", "$ "). if (skippedWhitespace) { LexerToken tokenAfter = parser.tokens.get(parser.tokenIndex); - if (tokenAfter.type == LexerTokenType.EOF || tokenAfter.type == LexerTokenType.NEWLINE) { + if (tokenAfter.type == LexerTokenType.EOF) { parser.throwError("syntax error"); } @@ -65,7 +62,7 @@ public static String parseComplexIdentifier(Parser parser, boolean isTypeglob) { // For example "$\t = 4" must be a syntax error, not "$= 4". if (tokenAfter.type == LexerTokenType.OPERATOR && tokenAfter.text.length() == 1 - && "!|/*+-<>&~.=%'?".indexOf(tokenAfter.text.charAt(0)) >= 0) { + && "!|/*+-<>&~.=%'?()".indexOf(tokenAfter.text.charAt(0)) >= 0) { parser.throwError("syntax error"); } } @@ -140,15 +137,12 @@ public static String parseComplexIdentifierInner(Parser parser, boolean insideBr public static String parseComplexIdentifierInner(Parser parser, boolean insideBraces, boolean isTypeglob) { // Perl allows whitespace between the sigil and the variable name (e.g. "$ a" parses as "$a"). + // Perl also allows newlines between sigil and variable name. // But if whitespace is skipped and the next token is not a valid identifier start (e.g. "$\t = 4"), // the variable name is missing and we should trigger a plain "syntax error". int wsStart = parser.tokenIndex; - // Skip horizontal whitespace to find the start of the identifier. - // Do not skip NEWLINE here: "$\n" is not a valid variable name. - while (parser.tokenIndex < parser.tokens.size() - && parser.tokens.get(parser.tokenIndex).type == LexerTokenType.WHITESPACE) { - parser.tokenIndex++; - } + // Skip whitespace (including newlines) to find the start of the identifier. + parser.tokenIndex = Whitespace.skipWhitespace(parser, parser.tokenIndex, parser.tokens); boolean skippedWhitespace = parser.tokenIndex != wsStart; boolean isFirstToken = true; @@ -196,9 +190,9 @@ public static String parseComplexIdentifierInner(Parser parser, boolean insideBr return null; } - // Special case for special variables like `$|`, `$'`, etc. + // Special case for special variables like `$|`, `$'`, `$(`, `$)`, etc. char firstChar = token.text.charAt(0); - if (token.type == LexerTokenType.OPERATOR && "!|/*+-<>&~.=%'?".indexOf(firstChar) >= 0) { + if (token.type == LexerTokenType.OPERATOR && "!|/*+-<>&~.=%'?()".indexOf(firstChar) >= 0) { // Special case: * followed by { is glob dereference when inside braces // @{*{expr}} should be parsed as @{ *{expr} }, not @*{expr} (hash slice on @*) // But @*{key} outside braces IS a hash slice on @*, so only apply when insideBraces diff --git a/src/main/java/org/perlonjava/frontend/parser/StringSegmentParser.java b/src/main/java/org/perlonjava/frontend/parser/StringSegmentParser.java index 9b0524df2..b8a92561a 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StringSegmentParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StringSegmentParser.java @@ -954,6 +954,14 @@ private boolean shouldInterpolateVariable(String sigil) { return false; } + // In regex patterns, $) should NOT be interpolated as the EGID variable. + // The ) typically closes a regex group, so $) means end-of-string anchor + closing paren. + // Similarly, $( in regex is usually $ anchor + opening paren, not the real GID variable. + // But in double-quoted strings, $( and $) SHOULD interpolate to the GID/EGID values. + if (isRegex && "$".equals(sigil) && (")".equals(nextToken.text) || "(".equals(nextToken.text))) { + return false; + } + // For @ sigil, only allow specific characters that can start array variable names // Valid: identifiers, digits, _, {, $, +, - // Invalid: ;, /, !, etc. (these are only valid after $ sigil) @@ -1010,8 +1018,8 @@ private boolean isValidArrayVariableStart(LexerToken token) { */ private boolean isNonInterpolatingCharacter(String text) { return switch (text) { - case ")", "%", "|", "#", "\"", "\\", - "?", "(" -> true; + case "%", "|", "#", "\"", "\\", + "?" -> true; default -> false; }; } diff --git a/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java b/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java index 9bfe654d2..9cb137649 100644 --- a/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java +++ b/src/main/java/org/perlonjava/frontend/semantic/SymbolTable.java @@ -28,8 +28,15 @@ public SymbolTable(int index) { public int addVariable(String name, String variableDeclType, String perlPackage, OperatorNode ast) { // Check if the variable is not already in the table // XXX TODO under 'no strict', we may need to allow variable redeclaration - if (!variableIndex.containsKey(name)) { - // Add the variable with a unique index + SymbolEntry existing = variableIndex.get(name); + if (existing == null) { + // Variable doesn't exist, add it + variableIndex.put(name, new SymbolEntry(index++, name, variableDeclType, perlPackage, ast)); + } else if ("our".equals(variableDeclType) && existing.perlPackage != null + && !existing.perlPackage.equals(perlPackage)) { + // For 'our' declarations in a different package, create a new entry + // This handles the case where 'our $AUTOLOAD' is declared in multiple packages + // within the same lexical scope - each should refer to its own package's variable variableIndex.put(name, new SymbolEntry(index++, name, variableDeclType, perlPackage, ast)); } // Return the index of the variable diff --git a/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java b/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java index b026c0bb4..86f490aa9 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ReferenceOperators.java @@ -46,6 +46,10 @@ public static RuntimeScalar bless(RuntimeScalar runtimeScalar, RuntimeScalar cla * - Empty string if not a reference */ public static RuntimeScalar ref(RuntimeScalar runtimeScalar) { + // Handle special variables that need to compute their value + if (runtimeScalar instanceof ScalarSpecialVariable specialVar) { + return ref(specialVar.getValueAsScalar()); + } String str; int blessId; switch (runtimeScalar.type) { diff --git a/src/main/java/org/perlonjava/runtime/operators/ScalarOperators.java b/src/main/java/org/perlonjava/runtime/operators/ScalarOperators.java index 798d01f3e..1b7b4d215 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ScalarOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/ScalarOperators.java @@ -23,11 +23,22 @@ public static RuntimeScalar oct(RuntimeScalar runtimeScalar) { expr = expr.replace("_", ""); int length = expr.length(); + + // Handle empty string or just "0" + if (length == 0) { + return scalarZero; + } + int start = 0; if (expr.startsWith("0")) { start++; } + // Check if we've consumed the entire string (e.g., input was just "0") + if (start >= length) { + return scalarZero; + } + if (expr.charAt(start) == 'x' || expr.charAt(start) == 'X') { // Hexadecimal string start++; diff --git a/src/main/java/org/perlonjava/runtime/operators/sprintf/SprintfValueFormatter.java b/src/main/java/org/perlonjava/runtime/operators/sprintf/SprintfValueFormatter.java index bffc2ef06..c5660c965 100644 --- a/src/main/java/org/perlonjava/runtime/operators/sprintf/SprintfValueFormatter.java +++ b/src/main/java/org/perlonjava/runtime/operators/sprintf/SprintfValueFormatter.java @@ -51,15 +51,33 @@ public class SprintfValueFormatter { */ public String formatValue(RuntimeScalar value, String flags, int width, int precision, char conversion) { - // Check for special floating-point values first - // BUT exclude %p - it should format the address even for Inf/NaN + // For string conversion (%s), don't call getDouble() to check for Inf/NaN + // because that would incorrectly convert strings like "INFO" to "Inf" + // (getDouble on "INFO" returns Infinity because it starts with "INF") + if (conversion == 's') { + return formatString(value.toString(), flags, width, precision); + } + + // For %p (pointer) and %n, also skip the Inf/NaN check + if (conversion == 'p') { + return formatPointer(value, flags); + } + if (conversion == 'n') { + throw new PerlCompilerException("%n specifier not supported"); + } + + // Check for special floating-point values for numeric conversions + // This includes %c - sprintf "%c", Inf should error in Perl double doubleValue = value.getDouble(); - if ((Double.isInfinite(doubleValue) || Double.isNaN(doubleValue)) && conversion != 'p') { + if (Double.isInfinite(doubleValue) || Double.isNaN(doubleValue)) { return numericFormatter.formatSpecialValue(doubleValue, flags, width, conversion); } // Dispatch to appropriate formatter based on conversion type return switch (conversion) { + // Character conversion + case 'c' -> formatCharacter(value, flags, width); + // Numeric conversions - delegate to numeric formatter case 'd', 'i' -> numericFormatter.formatInteger(value.getLong(), flags, width, precision, 10, false); case 'u' -> numericFormatter.formatUnsigned(value, flags, width, precision); @@ -71,12 +89,6 @@ public String formatValue(RuntimeScalar value, String flags, int width, numericFormatter.formatFloatingPoint(value.getDouble(), flags, width, precision, conversion); case 'f', 'F' -> numericFormatter.formatFloatingPoint(value.getDouble(), flags, width, precision, 'f'); - // String and character conversions - handle directly - case 'c' -> formatCharacter(value, flags, width); - case 's' -> formatString(value.toString(), flags, width, precision); - case 'p' -> formatPointer(value, flags); // This should already pass flags - case 'n' -> throw new PerlCompilerException("%n specifier not supported"); - // Uppercase variants (synonyms) case 'D' -> numericFormatter.formatInteger(value.getLong(), flags, width, precision, 10, false); case 'O' -> numericFormatter.formatOctal(value.getLong(), flags, width, precision); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java index ea1663211..94e83ce95 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java @@ -312,7 +312,7 @@ public static RuntimeList splitpath(RuntimeArray args, int ctx) { String path = args.get(1).toString(); boolean noFile = args.size() == 3 && args.get(2).getBoolean(); String volume = ""; - String directory = path; + String directory = ""; String file = ""; if (SystemUtils.osIsWindows()) { @@ -323,11 +323,17 @@ public static RuntimeList splitpath(RuntimeArray args, int ctx) { } } - if (!noFile) { + if (noFile) { + // If noFile is true, entire path is directory + directory = path; + } else { int lastSeparator = path.lastIndexOf(File.separator); if (lastSeparator != -1) { - directory = path.substring(0, lastSeparator); + directory = path.substring(0, lastSeparator + 1); file = path.substring(lastSeparator + 1); + } else { + // No separator - entire path is the filename + file = path; } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java index 34db534df..8d8a69726 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java @@ -80,8 +80,8 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { GlobalVariable.getGlobalVariable("main::>"); // TODO GlobalVariable.getGlobalVariable("main::<"); // TODO GlobalVariable.getGlobalVariable("main::;").set("\034"); // initialize $; (SUBSEP) to \034 - GlobalVariable.getGlobalVariable("main::("); // TODO - GlobalVariable.getGlobalVariable("main::)"); // TODO + GlobalVariable.globalVariables.put("main::(", new ScalarSpecialVariable(ScalarSpecialVariable.Id.REAL_GID)); // $( - real GID (lazy) + GlobalVariable.globalVariables.put("main::)", new ScalarSpecialVariable(ScalarSpecialVariable.Id.EFFECTIVE_GID)); // $) - effective GID (lazy) GlobalVariable.getGlobalVariable("main::="); // TODO GlobalVariable.getGlobalVariable("main::^"); // TODO GlobalVariable.getGlobalVariable("main:::"); // TODO @@ -211,14 +211,15 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { Re.initialize(); // Cwd.initialize(); // Use Perl Cwd.pm instead (has pure Perl fallbacks) FileSpec.initialize(); - UnicodeNormalize.initialize(); - UnicodeUCD.initialize(); - TimeHiRes.initialize(); - TermReadLine.initialize(); - TermReadKey.initialize(); - FileTemp.initialize(); - Encode.initialize(); - JavaSystem.initialize(); + // Deferred to XSLoader::load() for faster startup - only loaded when actually used: + // UnicodeNormalize.initialize(); // Has XSLoader in Perl file + // TimeHiRes.initialize(); // Has XSLoader in Perl file + UnicodeUCD.initialize(); // No XSLoader in Perl file - needed at startup + TermReadLine.initialize(); // No Perl file - needed at startup + TermReadKey.initialize(); // No Perl file - needed at startup + FileTemp.initialize(); // Perl uses eval require - keep for cleanup hooks + Encode.initialize(); // Common enough to keep + // JavaSystem.initialize(); // Only for java:: integration PerlIO.initialize(); IOHandle.initialize(); // IO::Handle methods (_sync, _error, etc.) Version.initialize(); // Initialize version module for version objects diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index 54fb38d8a..018b12141 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -523,7 +523,8 @@ public static boolean existsGlobalIO(String key) { public static boolean isGlobalIODefined(String key) { RuntimeGlob glob = globalIORefs.get(key); if (glob != null && glob.type == RuntimeScalarType.GLOB) { - return glob.value instanceof RuntimeIO; + // Check the IO slot, not glob.value - IO is stored in glob.IO + return glob.IO != null && glob.IO.getDefinedBoolean(); } return false; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 5e8ad649e..11f3d5ece 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -1263,6 +1263,16 @@ public static RuntimeList callCached(int callsiteId, for (RuntimeBase arg : args) { arg.setArrayOfAlias(a); } + + // If this is an AUTOLOAD, set $AUTOLOAD before calling + String autoloadVariableName = cachedCode.autoloadVariableName; + if (autoloadVariableName != null) { + String methodName = method.toString(); + String className = autoloadVariableName.substring(0, autoloadVariableName.lastIndexOf("::")); + String fullMethodName = NameNormalizer.normalizeVariableName(methodName, className); + getGlobalVariable(autoloadVariableName).set(fullMethodName); + } + // Prefer PerlSubroutine interface over MethodHandle if (cachedCode.subroutine != null) { return cachedCode.subroutine.apply(a, callContext); @@ -1379,6 +1389,19 @@ public static RuntimeList call(RuntimeScalar runtimeScalar, if (perlClassName.isEmpty()) { throw new PerlCompilerException("Can't call method \"" + methodName + "\" on an undefined value"); } + + // Check if this string is a bareword filehandle (like IN, OUT, etc.) + // If so, look up the glob and call the method on it + String normalizedGlobName = NameNormalizer.normalizeVariableName(perlClassName, "main"); + if (GlobalVariable.isGlobalIODefined(normalizedGlobName)) { + // This is a filehandle - get the glob reference and recurse + RuntimeGlob glob = GlobalVariable.getGlobalIO(normalizedGlobName); + RuntimeScalar globRef = glob.createReference(); + // Remove the invocant we already added and re-add with the glob reference + args.elements.removeFirst(); + return call(globRef, method, currentSub, args, callContext); + } + if (perlClassName.endsWith("::")) { perlClassName = perlClassName.substring(0, perlClassName.length() - 2); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index adb239949..913f11e84 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -322,6 +322,12 @@ private RuntimeScalar getGlobSlot(RuntimeScalar index) { String pkg = lastColonIndex >= 0 ? this.globName.substring(0, lastColonIndex) : "main"; yield new RuntimeScalar(NameNormalizer.getBlessStrForClassName(pkg)); } + case "NAME" -> { + // Return the name of this glob (without the package prefix) + int lastColonIndex = this.globName.lastIndexOf("::"); + String name = lastColonIndex >= 0 ? this.globName.substring(lastColonIndex + 2) : this.globName; + yield new RuntimeScalar(name); + } case "IO" -> { // Accessing the IO slot yields a blessable reference-like value. // We model this by returning a GLOBREFERENCE wrapper around the RuntimeIO. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java index 39e314c67..71ffe0203 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java @@ -2,6 +2,7 @@ import org.perlonjava.frontend.parser.SpecialBlockParser; import org.perlonjava.frontend.semantic.ScopedSymbolTable; +import org.perlonjava.runtime.nativ.NativeUtils; import org.perlonjava.runtime.regex.RuntimeRegex; import java.util.Stack; @@ -174,11 +175,14 @@ public RuntimeScalar getValueAsScalar() { } // Get the stash and access the glob - RuntimeHash stash = HashSpecialVariable.getStash(packageName); + // The stash key must end with "::" for package stashes + RuntimeHash stash = HashSpecialVariable.getStash(packageName + "::"); RuntimeScalar glob = stash.get(name); - if (glob.type == RuntimeScalarType.GLOB || glob.type == RuntimeScalarType.UNDEF) { - // Return a reference to the glob - yield glob.createReference(); + if (glob.type == RuntimeScalarType.GLOB) { + // ${^LAST_FH} returns a GLOB reference (like \*FH) + // This allows *{${^LAST_FH}} to work under strict refs + RuntimeGlob runtimeGlob = (RuntimeGlob) glob.value; + yield runtimeGlob.createReference(); } } // Fallback to the RuntimeIO object if no glob name is available @@ -219,6 +223,14 @@ public RuntimeScalar getValueAsScalar() { } yield getScalarInt(0); } + case REAL_GID -> { + // $( - Real group ID (lazy evaluation to avoid JNA overhead at startup) + yield new RuntimeScalar(NativeUtils.getgid(0)); + } + case EFFECTIVE_GID -> { + // $) - Effective group ID (lazy evaluation to avoid JNA overhead at startup) + yield new RuntimeScalar(NativeUtils.getegid(0)); + } }; return result; } catch (IllegalStateException e) { @@ -300,6 +312,29 @@ public RuntimeIO getRuntimeIO() { return this.getValueAsScalar().getRuntimeIO(); } + /** + * Dereference as a glob (strict refs version). + * This delegates to the computed value's globDeref(). + * + * @return The RuntimeGlob from the computed value. + */ + @Override + public RuntimeGlob globDeref() { + return this.getValueAsScalar().globDeref(); + } + + /** + * Dereference as a glob (non-strict refs version). + * This delegates to the computed value's globDerefNonStrict(). + * + * @param packageName The package name for symbolic reference resolution. + * @return The RuntimeGlob from the computed value. + */ + @Override + public RuntimeGlob globDerefNonStrict(String packageName) { + return this.getValueAsScalar().globDerefNonStrict(packageName); + } + /** * Adds this entity to the specified RuntimeList. * @@ -380,6 +415,8 @@ public enum Id { LAST_SUCCESSFUL_PATTERN, // ${^LAST_SUCCESSFUL_PATTERN} LAST_REGEXP_CODE_RESULT, // $^R - Result of last (?{...}) code block in regex HINTS, // $^H - Compile-time hints (strict, etc.) + REAL_GID, // $( - Real group ID (lazy, JNA call only on access) + EFFECTIVE_GID, // $) - Effective group ID (lazy, JNA call only on access) } private record InputLineState(RuntimeIO lastHandle, int lastLineNumber, RuntimeScalar localValue) { diff --git a/src/main/perl/lib/Time/HiRes.pm b/src/main/perl/lib/Time/HiRes.pm index 8d6a2aaf4..aa49f9202 100644 --- a/src/main/perl/lib/Time/HiRes.pm +++ b/src/main/perl/lib/Time/HiRes.pm @@ -21,8 +21,8 @@ use Exporter 'import'; our @EXPORT_OK = qw(usleep nanosleep ualarm gettimeofday tv_interval time sleep alarm); -# require XSLoader; -# XSLoader::load('Time::HiRes'); +require XSLoader; +XSLoader::load('Time::HiRes'); sub tv_interval { my ($start, $end) = @_;