diff --git a/AGENTS.md b/AGENTS.md index e5b7e513c..9811018b2 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -198,6 +198,20 @@ The perl_test_runner.pl sets these automatically based on the test file being ru ### Commits +- **Single quotes in commit messages:** The `$(cat <<'EOF' ... EOF)` heredoc pattern breaks when the message body contains single quotes (e.g., Perl's `$_`, `don't`). Write the message to a temp file and use `git commit -F` instead: + ```bash + cat > /tmp/commit_msg.txt << 'ENDMSG' + feat: description here + + Body with single quotes like $_ and don't is fine. + + Generated with [TOOL_NAME](TOOL_DOCS_URL) + + Co-Authored-By: TOOL_NAME + ENDMSG + git commit -F /tmp/commit_msg.txt + ``` + Replace `TOOL_NAME`, `TOOL_DOCS_URL`, and `TOOL_BOT_EMAIL` as described in [AI_POLICY.md](AI_POLICY.md). - Reference the design doc or issue in commit messages when relevant - Use conventional commit format when possible - **Write commit messages to a file** to avoid shell quoting issues (apostrophes, backticks, special characters). Use `git commit -F /tmp/commit_msg.txt` instead of `-m`: diff --git a/dev/design/README.md b/dev/design/README.md index 711114d60..b3b365564 100644 --- a/dev/design/README.md +++ b/dev/design/README.md @@ -113,9 +113,9 @@ When adding new design documents: For the most important architectural decisions and current work, check: -- **multiplicity.md** - Multiple independent Perl runtimes (enables fork/threads/web concurrency) +- **concurrency.md** - Unified concurrency design (supersedes multiplicity.md, fork.md, threads.md). Includes multiplicity demo and progress tracking. - **jsr223-perlonjava-web.md** - JSR-223 compliance and web server integration -- **fork.md** / **threads.md** - Concurrency model and limitations +- **multiplicity.md** / **fork.md** / **threads.md** - Superseded by concurrency.md These represent major architectural directions for the project. diff --git a/dev/design/concurrency.md b/dev/design/concurrency.md index a4c26181a..4e151a548 100644 --- a/dev/design/concurrency.md +++ b/dev/design/concurrency.md @@ -892,3 +892,683 @@ comparable to Perl 5 interpreter clones. - Updated virtual thread pinning caveat with JEP 491 reference - Updated timeline with risk assessment - Moved resolved questions out of open questions + +--- + +## Progress Tracking + +### Current Status: Phase 0 complete, full runtime isolation achieved (2026-04-10) + +All mutable runtime state has been migrated from static fields into `PerlRuntime` +instance fields with ThreadLocal-based access. Multiple independent Perl interpreters +can now coexist within the same JVM process with isolated state. Compilation is +thread-safe via a global `COMPILE_LOCK` (ReentrantLock) that serializes all +parsing/emitting, while allowing concurrent execution of compiled code. + +### Completed Phases + +- [x] **Phase 1: PerlRuntime Shell** (2026-04-10) + - Created `PerlRuntime.java` with `ThreadLocal CURRENT` + - Added `current()`, `initialize()`, `setCurrent()` API + - Wired `PerlRuntime.initialize()` into `Main.main()` and test setUp methods + - Added `ensureRuntimeInitialized()` safety net in `PerlLanguageProvider` + +- [x] **Phase 2: De-static-ify I/O** (2026-04-10) + - Moved `RuntimeIO.stdout/stderr/stdin` into `PerlRuntime` + - Moved `selectedHandle`, `lastWrittenHandle`, `lastAccessedHandle`, `lastReadlineHandleName` + - Added static getter/setter methods on `RuntimeIO` + - Updated `EmitOperator` to use `INVOKESTATIC` instead of `PUTSTATIC` + - Updated 15 consumer files (IOOperator, RuntimeGlob, TieOperators, etc.) + +- [x] **Phase 3: De-static-ify CallerStack + DynamicScope** (2026-04-10) + - Moved `CallerStack.callerStack` to `PerlRuntime.callerStack` + - Moved `DynamicVariableManager.variableStack` to `PerlRuntime.dynamicVariableStack` + - Moved `RuntimeScalar.dynamicStateStack` to `PerlRuntime.dynamicStateStack` + +- [x] **Phase 4: De-static-ify SpecialBlocks** (2026-04-10) + - Moved `SpecialBlock.endBlocks/initBlocks/checkBlocks` to PerlRuntime + - Added public getters on SpecialBlock + +- [x] **Phase 5a: De-static-ify InheritanceResolver** (2026-04-10) + - Moved 7 static fields: linearizedClassesCache, packageMRO, methodCache, + overloadContextCache, isaStateCache, autoloadEnabled, currentMRO + - Updated DFS.java, C3.java, and 4 consumer files + +- [x] **Phase 5b: De-static-ify GlobalVariable** (2026-04-10) + - Moved all 17 static fields: symbol tables (globalVariables, globalArrays, + globalHashes, globalCodeRefs), IO/Format refs, aliasing maps, caches, + classloader, declared variable tracking + - Added static accessor methods (getGlobalVariablesMap(), etc.) + - Updated 20 consumer files across frontend, backend, and runtime packages + +- [x] **Phase 5c: De-static-ify Regex State** (2026-04-10) + - Moved 14 static fields from RuntimeRegex into PerlRuntime: globalMatcher, + globalMatchString, lastMatchedString, lastMatch start/end, lastSuccessful*, + lastSuccessfulPattern, lastMatchUsedPFlag, lastMatchUsedBackslashK, + lastCaptureGroups, lastMatchWasByteString + - Added static getter/setter methods on RuntimeRegex + - Updated RegexState.java, ScalarSpecialVariable.java, HashSpecialVariable.java + +- [x] **Phase 5d: De-static-ify RuntimeCode Caches** (2026-04-10) + - Moved evalBeginIds, evalCache, methodHandleCache, anonSubs, interpretedSubs, + evalContext, evalDepth, inline method cache arrays into PerlRuntime + - Added static getter methods on RuntimeCode (getEvalBeginIds(), getEvalCache(), + getAnonSubs(), getInterpretedSubs(), getEvalContext()) + - Added incrementEvalDepth()/decrementEvalDepth()/getEvalDepth() methods + - Changed EmitterMethodCreator bytecode from GETSTATIC/PUTSTATIC to INVOKESTATIC + - Changed EmitSubroutine bytecode from GETSTATIC to INVOKESTATIC for interpretedSubs + - Updated 13 consumer files: ScalarSpecialVariable, BytecodeInterpreter, + EmitterMethodCreator, EmitEval, EmitSubroutine, WarnDie, EvalStringHandler, + SpecialBlockParser, SubroutineParser, BytecodeCompiler, EmitVariable, + CompileAssignment + - Decision: evalCache/methodHandleCache are per-runtime (simpler, no sharing) + +### Files Created +- `src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java` + +### Key Design Decisions +- Kept original static method signatures on migrated classes — callers don't change +- Used public accessor methods (e.g., `GlobalVariable.getGlobalVariablesMap()`) for + cross-package access to PerlRuntime fields +- ThreadLocal overhead is negligible (~1ns per access, JIT-optimized) +- evalCache/methodHandleCache are per-runtime (not shared) — simpler, avoids + cross-runtime class compatibility issues + +### Multiplicity Demo (2026-04-10) +- Created `dev/sandbox/multiplicity/MultiplicityDemo.java` — launches N threads, each with its + own PerlRuntime, compiles and executes a Perl script, captures per-thread STDOUT +- Uses `PerlLanguageProvider.executePerlCode()` which handles the full lifecycle: + initialization, compilation (under COMPILE_LOCK), and execution (no lock) +- INIT/CHECK/UNITCHECK/END blocks execute correctly for each interpreter +- Successfully tested with 126 concurrent interpreters running unit tests +- **122/126 tests pass**; remaining 4 failures are pre-existing `DESTROY` TODO: + - `tie_array.t`, `tie_handle.t`, `tie_hash.t`, `tie_scalar.t` — object destructors not implemented +- Run with: `./dev/sandbox/multiplicity/run_multiplicity_demo.sh` + +### Local Save/Restore Stack Fix (2026-04-10) + +**Problem:** After Phase 3 migrated `DynamicVariableManager.variableStack` and +`RuntimeScalar.dynamicStateStack` to per-runtime, `local` still failed under +multiplicity. With 2+ interpreters, `local $x` would not restore the original value +at scope exit — all "restored" assertions failed. + +**Root cause:** Phase 3 only migrated 2 of 17 dynamic state stacks. The remaining +15 were still shared static fields. The most critical was +`GlobalRuntimeScalar.localizedStack` — this is the stack used when `local` is +applied to package variables (the most common case). With 2 threads doing +`local $global_var` concurrently, they pushed/popped from the same stack, causing +each thread to restore the other thread's saved state. + +**Fix (commit e2f16ec07):** Migrated all 16 remaining stacks to per-PerlRuntime +instance fields, following the same accessor-method pattern: + +| Class | Stack Field(s) | Type | +|-------|----------------|------| +| `GlobalRuntimeScalar` | `localizedStack` | `Stack` (SavedGlobalState) | +| `GlobalRuntimeArray` | `localizedStack` | `Stack` (SavedGlobalArrayState) | +| `GlobalRuntimeHash` | `localizedStack` | `Stack` (SavedGlobalHashState) | +| `RuntimeArray` | `dynamicStateStack` | `Stack` | +| `RuntimeHash` | `dynamicStateStack` | `Stack` | +| `RuntimeStash` | `dynamicStateStack` | `Stack` | +| `RuntimeGlob` | `globSlotStack` | `Stack` (GlobSlotSnapshot) | +| `RuntimeHashProxyEntry` | `dynamicStateStack` | `Stack` | +| `RuntimeArrayProxyEntry` | `dynamicStateStackInt` + `dynamicStateStack` | `Stack` + `Stack` | +| `ScalarSpecialVariable` | `inputLineStateStack` | `Stack` (InputLineState) | +| `OutputAutoFlushVariable` | `stateStack` | `Stack` (State) | +| `OutputRecordSeparator` | `orsStack` | `Stack` | +| `OutputFieldSeparator` | `ofsStack` | `Stack` | +| `ErrnoVariable` | `errnoStack` + `messageStack` | `Stack` + `Stack` | + +Each class now has a `private static Stack stackName()` accessor that delegates +to `PerlRuntime.current().`. Inner types (SavedGlobalState, etc.) remain +private to their classes; `PerlRuntime` stores them as `Stack` with +`@SuppressWarnings("unchecked")` casts in the accessor methods. + +**Impact:** Fixed 8 previously-failing tests under multiplicity: `local.t` (74/74), +`chomp.t`, `defer.t`, `local_glob_dynamic.t`, `sysread_syswrite.t`, +`array_autovivification.t`, `vstring.t`, `nested_for_loops.t`. + +### Phase 0: Compilation Thread Safety (2026-04-10) + +**Problem:** The multiplicity demo serializes initial compilation with a `COMPILE_LOCK`, +but `eval "string"` at runtime goes through `EvalStringHandler` → `Lexer` → `Parser` → +emitter → class loading with **no locking**. Concurrent `eval` from multiple threads +will corrupt shared mutable static state. + +Additionally, `executePerlCode()` (the main entry point for running Perl code) had no +locking at all — it was only safe for single-threaded CLI use. And `globalInitialized` +was a shared static boolean, causing thread 2+ to skip `initializeGlobals()` entirely. + +**Architecture fix (commit TBD):** + +1. **`globalInitialized` moved to per-PerlRuntime** — Each runtime tracks its own + initialization state. Previously, thread 1 set the shared static to `true`, + causing threads 2-N to skip `initializeGlobals()` and run without `$_`, `@INC`, + built-in modules, etc. + +2. **`executePerlCode()` now uses COMPILE_LOCK** — The compilation phase (tokenize, + parse, compile) runs under the lock, then the lock is released before execution: + ``` + COMPILE_LOCK.lock() + savedScope = getCurrentScope() + initializeGlobals() (per-runtime, idempotent) + tokenize → parse → compileToExecutable() + COMPILE_LOCK.unlock() + + executeCode() — runs UNITCHECK, CHECK, INIT, main code, END (no lock) + ``` + +3. **Demo simplified** — Uses `executePerlCode()` instead of `compilePerlCode()` + + `apply()`. No more redundant demo-level lock. INIT/CHECK/UNITCHECK blocks now + execute correctly (previously skipped, causing begincheck.t failures). + +**Audit results** — shared mutable state found in three subsystems: + +#### Parser (frontend/) — 11 fields + +| Severity | File | Field | Issue | +|----------|------|-------|-------| +| HIGH | `SpecialBlockParser.java:25` | `symbolTable` | Global parser scope, read/written from 27 call sites | +| HIGH | `NumberParser.java:27` | `numificationCache` | LRU LinkedHashMap; `.get()` mutates internal state | +| MEDIUM | `ScopedSymbolTable.java:38` | `nextWarningBitPosition` | Non-atomic counter for `use warnings::register` | +| MEDIUM | `StringSegmentParser.java:50` | `codeBlockCaptureCounter` | Non-atomic counter for regex code block captures | +| MEDIUM | `ScopedSymbolTable.java:18` | `warningBitPositions` | HashMap mutated by `registerCustomWarningCategory()` | +| MEDIUM | `ScopedSymbolTable.java:21` | `packageVersions` | HashMap mutated during `use` and `clear()`ed on reset | +| MEDIUM | `DataSection.java:24` | `processedPackages` | HashSet mutated during `__DATA__` parsing | +| MEDIUM | `DataSection.java:29` | `placeholderCreated` | HashSet mutated during `__DATA__` parsing | +| MEDIUM | `FieldRegistry.java:17` | `classFields` | HashMap mutated by `registerField()` | +| MEDIUM | `FieldRegistry.java:21` | `classParents` | HashMap mutated by `registerField()` | +| LOW | `Lexer.java:44` | `isOperator` | Not final but never mutated after class init | + +#### Emitter (backend/) — 8 fields + +| Severity | File | Field | Issue | +|----------|------|-------|-------| +| HIGH | `ByteCodeSourceMapper.java:17-29` | 7 HashMap/ArrayList collections | Source mapping; concurrent `computeIfAbsent()` corrupts HashMap internals | +| HIGH | `LargeBlockRefactorer.java:29` | `controlFlowDetector` | Single shared visitor; `reset()`/`scan()` race → wrong bytecode | +| HIGH | `EmitterMethodCreator.java:52` | `classCounter` | Non-atomic `++`; duplicate class names → `LinkageError` | +| MEDIUM | `BytecodeCompiler.java:80` | `nextCallsiteId` | Non-atomic `++`; duplicate IDs corrupt `/o` regex cache | +| MEDIUM | `EmitRegex.java:21` | `nextCallsiteId` | Non-atomic `++`; same issue for JVM path | +| MEDIUM | `Dereference.java:19` | `nextMethodCallsiteId` | Non-atomic `++`; duplicate IDs corrupt inline method cache | +| LOW | `EmitterMethodCreator.java:50` | `skipVariables` | Never mutated; should be `final` | + +#### Class loader — already safe +`CustomClassLoader` is per-`PerlRuntime` (migrated in Phase 5b). + +**Implementation plan (two-part):** + +1. **Quick fixes (no lock needed):** ✅ Done + - Replaced 4 counters with `AtomicInteger`: `classCounter`, `nextCallsiteId` (×2), + `nextMethodCallsiteId` + - Marked `skipVariables` as `final` + - Replaced `LargeBlockRefactorer.controlFlowDetector` singleton with new instance + per call (matches the existing `controlFlowFinderTl` ThreadLocal pattern on line 34) + +2. **Global compile lock:** ✅ Done + - Added `static final ReentrantLock COMPILE_LOCK` to `PerlLanguageProvider` + - Acquired in `compilePerlCode()` and in both `EvalStringHandler.evalString()` overloads + - This serializes all compilation (initial + runtime eval) but guarantees safety + - Lock is reentrant so nested evals work without deadlock + - Future optimization: migrate parser/emitter static state to per-runtime, remove lock + +### Reentrancy Analysis (2026-04-10) + +**Question:** What happens when `eval "string"` triggers a BEGIN block that itself +requires a module (nested compilation)? + +**Answer:** `ReentrantLock` handles this correctly. The call chain runs entirely on +the same thread: + +``` +eval "use Foo" + → EvalStringHandler.evalString() acquires COMPILE_LOCK (count=1) + → Parser.parse() encounters `use Foo` → BEGIN block + → SpecialBlockParser.runSpecialBlock() → executePerlAST() + → require Foo → PerlLanguageProvider.compilePerlCode() + → COMPILE_LOCK.lock() — same thread, count=2 + → compile module → unlock (count=1) + → continue executing BEGIN block (execution, no lock needed — but lock + is still held at count=1 by the outer compilation) + → Parser continues parsing the rest of the eval string + → unlock (count=0) +``` + +Same-thread reentrancy works because `ReentrantLock` increments the hold count on +each nested `lock()` and decrements on each `unlock()`. + +**Bug found and fixed:** `evalStringWithInterpreter` used `isHeldByCurrentThread()` +in its `finally` block to decide whether to release the lock. This over-decrements +in nested scenarios: + +``` +Outer compilation holds lock (count=1) + → BEGIN triggers inner evalStringWithInterpreter + → lock (count=2) + → compile OK, explicit unlock before execution (count=1) + → execution runs + → finally: isHeldByCurrentThread() → TRUE (outer holds it!) + → unlock (count=0) ← BUG: released the outer's lock! +``` + +**Fix (commit 510106cd9):** Replaced `isHeldByCurrentThread()` with a `boolean +compileLockReleased` flag that tracks whether the success-path unlock already +happened. The finally block only unlocks if the flag is false (error path). + +**Why not release the lock during BEGIN execution?** `runSpecialBlock` is called +**mid-parse** — the parser is suspended with its state intact (token position, +symbol table, scope depth). That state lives in shared statics like +`SpecialBlockParser.symbolTable` and `ByteCodeSourceMapper` collections. If the +lock were released, another thread could start compiling and corrupt this state. +Releasing the lock around BEGIN blocks is only viable after migrating parser/emitter +state from shared statics to per-compilation-context (which would eliminate the +lock entirely). + +### Per-Runtime CWD Isolation (2026-04-10) + +**Problem:** `chdir()` called `System.setProperty("user.dir", ...)` which is JVM-global. +When multiple interpreters called `chdir()` concurrently, they overwrote each other's +working directory. This caused `directory.t` and `glob.t` to fail under multiplicity. + +**Fix (commit c30eeb487):** Added per-runtime `String cwd` field to `PerlRuntime`, +initialized from `System.getProperty("user.dir")` at construction time. + +- `PerlRuntime.cwd` — per-runtime CWD field +- `PerlRuntime.getCwd()` — static accessor with fallback to `System.getProperty("user.dir")` +- `Directory.chdir()` — updates `PerlRuntime.current().cwd` instead of `System.setProperty()` +- `RuntimeIO.resolvePath()` — resolves relative paths against `PerlRuntime.getCwd()` +- Updated all 21 `System.getProperty("user.dir")` call sites across 12 files: + `SystemOperator.java`, `FileSpec.java`, `POSIX.java`, `Internals.java`, + `IPCOpen3.java`, `XMLParserExpat.java`, `ScalarGlobOperator.java`, `DirectoryIO.java`, + `PipeInputChannel.java`, `PipeOutputChannel.java`, `Directory.java`, `RuntimeIO.java` +- `ArgumentParser.java` kept as-is (sets initial `user.dir` before runtime creation for `-C` flag) + +**Impact:** `directory.t` (9/9) and `glob.t` (15/15) now pass under concurrent interpreters. + +### Per-Runtime PID and Pipe Thread Fix (2026-04-10) + +**Problem 1 — Shared `$$`:** All interpreters shared the same JVM PID via +`ProcessHandle.current().pid()`. Tests that use `$$` in temp filenames +(`io_read.t`, `io_seek.t`, `io_layers.t`) produced identical filenames across +concurrent interpreters, causing file collisions and data races. + +**Problem 2 — Unbound pipe threads:** `PipeInputChannel` and `PipeOutputChannel` +spawn background daemon threads for stderr/stdout consumption. These threads had +no `PerlRuntime` bound via `ThreadLocal`, so `GlobalVariable.getGlobalIO("main::STDERR")` +calls threw `IllegalStateException` and fell back to `System.out`/`System.err`, +bypassing per-runtime STDOUT/STDERR redirection. Under concurrent multiplicity testing, +this caused `io_pipe.t` failures. + +**Fix (commit 0179c888e):** + +1. **Per-runtime unique PID:** Added `AtomicLong PID_COUNTER` to `PerlRuntime`, starting + at the real JVM PID. Each runtime gets `PID_COUNTER.getAndIncrement()` — first runtime + gets the real PID (backward compatible), subsequent runtimes get unique incrementing values. + `GlobalContext.initializeGlobals()` sets `$$` from `PerlRuntime.current().pid`. + +2. **Pipe thread runtime binding:** Both `PipeInputChannel.setupProcess()` and + `PipeOutputChannel.setupProcess()` now capture `PerlRuntime.currentOrNull()` before + spawning background threads, and call `PerlRuntime.setCurrent(parentRuntime)` inside + the thread lambda. This ensures pipe stderr/stdout consumer threads can access the + correct per-runtime IO handles. + +**Impact:** Fixed 5 previously-failing tests: `io_read.t`, `io_seek.t`, `io_pipe.t`, +`io_layers.t`, `digest.t`. Multiplicity stress test improved from 117/126 to **122/126**. + +### Next Steps + +1. **Phase 6:** Implement `threads` module (requires runtime cloning — see Sections 5.1-5.4 + for the full cloning protocol). This is new functionality: deep-cloning runtime state, + closure capture fixup, `CLONE($pkg)` callbacks, `threads::shared` wrappers. + +2. **Phase 7: Runtime Pool** — An optimization/convenience layer, not a new capability. + The core multiplicity infrastructure is already complete; `PerlRuntime` instances can + be created and destroyed ad-hoc (as the `MultiplicityDemo` does). The pool amortizes + runtime initialization cost (loading built-in modules, setting up `@INC`/`%ENV`, etc.) + by reusing warm runtimes instead of re-creating them per request. Also provides + concurrency limiting (cap simultaneous runtimes to prevent OOM) and clean reset + between uses. Same pattern as JDBC connection pools or servlet thread pools. Primarily + useful for high-throughput web server embedding (mod_perl model), not needed for CLI + usage or the demo. + +3. **Future optimization:** Migrate parser/emitter static state to per-runtime, remove COMPILE_LOCK + +### Performance Baseline: master vs feature/multiplicity (2026-04-10) + +Benchmarks run on both branches with `make clean ; make` before each run. +All benchmarks are in `dev/bench/`. + +#### Speed Benchmarks + +| Benchmark | master (ops/s) | branch (ops/s) | Change | +|-----------|---------------|-----------------|--------| +| lexical (local var loop) | 394,139 | 374,732 | **-4.9%** | +| global (global var loop) | 77,720 | 73,550 | **-5.4%** | +| eval_string (`eval "..."`) | 86,327 | 82,183 | **-4.8%** | +| closure (create + call) | 863 | 569 | **-34.1%** | +| method (dispatch) | 436 | 319 | **-26.9%** | +| regex (matching) | 50,760 | 47,219 | **-7.0%** | +| string (operations) | 28,884 | 30,752 | **+6.5%** | + +#### Memory Benchmarks + +Memory is essentially unchanged (within noise): ~88MB RSS startup, +identical delta ratios for arrays (15.4x), hashes (2.3x), strings (8.0x), +nested structures (2.7x). + +#### Analysis + +Most benchmarks show a 5-7% slowdown from ThreadLocal routing, consistent with +the design doc estimate of "0.25-25%." Two benchmarks show larger regressions: + +**Closure (-34%):** The closure call path (`RuntimeCode.apply()`) has **zero** +`PerlRuntime.current()` lookups but **14-17 other ThreadLocal lookups** per +invocation from `WarningBitsRegistry` (7 ThreadLocals x push/pop), +`HintHashRegistry` (3 ops), and `argsStack` (2 ops). These are the pre-existing +ThreadLocal stacks that were already present on master. The regression likely +comes from increased ThreadLocal contention or JIT optimization interference +from the additional ThreadLocal fields on `PerlRuntime`. + +**Method (-27%):** The method dispatch path has two modes: +- **Cache hit** (`callCached()`): Only 1 `PerlRuntime.current()` lookup — fast +- **Cache miss** (`findMethodInHierarchy()`): **12-14** `PerlRuntime.current()` + lookups plus 14-17 from `apply()` = ~26-31 total ThreadLocal lookups + +The regression suggests the inline cache hit rate may have decreased, or the +cache-miss path is being exercised more due to per-runtime cache isolation +(each runtime starts with a cold cache). + +#### Optimization Plan + +**Goal:** Reduce the closure and method dispatch regressions to under 10% +(matching the 5-7% range of other benchmarks). The general 5-7% slowdown +from ThreadLocal routing is acceptable and expected. + +**Git workflow:** + +``` +feature/multiplicity (this branch — known good, all tests pass) + └── feature/multiplicity-opt (create this — do optimization work here) +``` + +1. Fetch and check out this branch: + ```bash + git fetch origin feature/multiplicity + git checkout feature/multiplicity + ``` +2. Create a new branch for optimization work: + ```bash + git checkout -b feature/multiplicity-opt + ``` +3. Do the optimization work on `feature/multiplicity-opt` (see tiers below). + Commit after each step so progress is preserved. +4. **If the optimization succeeds** (target benchmarks within 10% of master): + merge back into `feature/multiplicity` and push: + ```bash + git checkout feature/multiplicity + git merge feature/multiplicity-opt + git push origin feature/multiplicity + ``` +5. **If the optimization fails** (no measurable gain, or introduces regressions): + go back to `feature/multiplicity`, document the failure in this section + (what was tried, what the benchmark numbers were, why it did not work), + commit and **push** so the findings are preserved, then delete the work branch: + ```bash + git checkout feature/multiplicity + # Edit this file: add findings to the "Failed Optimization Attempts" section below + git commit -am "docs: document failed optimization attempt" + git push origin feature/multiplicity + git branch -D feature/multiplicity-opt + ``` + This ensures the next engineer knows what was already tried and can + avoid repeating the same work. + +**Methodology for each optimization step:** + +1. Create a commit on `feature/multiplicity-opt` with the optimization +2. Run `make clean ; make` to verify no test regressions +3. Run the relevant benchmark(s) 3 times, take the median: + ```bash + ./jperl dev/bench/benchmark_closure.pl # target: closure + ./jperl dev/bench/benchmark_method.pl # target: method + ./jperl dev/bench/benchmark_lexical.pl # control: should not regress + ./jperl dev/bench/benchmark_global.pl # control: should not regress + ``` +4. Compare against the baseline numbers in the table above +5. **Revert if:** the optimization does not measurably improve the target + benchmark AND does not improve code architecture (e.g., reducing + unnecessary abstraction layers). Keep only if it delivers measurable + improvement or is architecturally cleaner regardless of performance. +6. Run the 126-interpreter stress test to verify multiplicity still works: + ```bash + bash dev/sandbox/multiplicity/run_multiplicity_demo.sh src/test/resources/unit/*.t + ``` + +Listed in order of expected impact. Each tier is independent — do not +proceed to Tier 2 unless Tier 1 has been completed and benchmarked. + +--- + +**Tier 1: Cache `PerlRuntime.current()` in local variables (LOW RISK)** + +These are mechanical changes — cache the ThreadLocal result at method entry +instead of calling `PerlRuntime.current()` multiple times. Pattern: + +```java +// BEFORE: N ThreadLocal lookups +public static Foo doSomething(String key) { + Foo a = PerlRuntime.current().mapA.get(key); // lookup 1 + Foo b = PerlRuntime.current().mapB.get(key); // lookup 2 + PerlRuntime.current().mapC.put(key, b); // lookup 3 + return a; +} + +// AFTER: 1 ThreadLocal lookup +public static Foo doSomething(String key) { + PerlRuntime rt = PerlRuntime.current(); // lookup 1 + Foo a = rt.mapA.get(key); + Foo b = rt.mapB.get(key); + rt.mapC.put(key, b); + return a; +} +``` + +**Step 1a: `GlobalVariable.getGlobalCodeRef()`** +- File: `src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java` +- Currently 4 `PerlRuntime.current()` calls per invocation (pinnedCodeRefs, + globalCodeRefs get, globalCodeRefs put, pinnedCodeRefs put) +- Called N times during method hierarchy traversal in `findMethodInHierarchy()` +- Expected savings: 3 lookups per call x N calls per method dispatch +- **Expected impact on method benchmark:** moderate (reduces cache-miss cost) +- Benchmark after this step before proceeding + +**Step 1b: `InheritanceResolver.findMethodInHierarchy()`** +- File: `src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java` +- Currently 12-14 `PerlRuntime.current()` calls per cache-miss invocation + across `getMethodCache()`, `getIsaStateCache()`, `getLinearizedClassesCache()`, + `getPackageMROMap()`, `getCurrentMRO()`, `isAutoloadEnabled()` +- Two approaches (pick one): + - (a) Add `PerlRuntime rt` parameter to `findMethodInHierarchy()` and its + internal methods — cleaner but changes method signatures + - (b) Cache `PerlRuntime rt` as a local at the top of `findMethodInHierarchy()` + and replace each `getXxxCache()` call with direct `rt.xxxCache` access — + fewer signature changes but less encapsulated +- **Expected impact on method benchmark:** high (this is the main cache-miss path) +- Benchmark after this step + +**Step 1c: Other `GlobalVariable` accessors** +- Same file as 1a +- Apply the same pattern to `getGlobalVariable()`, `getGlobalArray()`, + `getGlobalHash()`, `existsGlobalCodeRef()`, `resolveStashAlias()` +- Each saves 1 lookup per call; these are called pervasively +- **Expected impact:** small per-method, cumulative across all benchmarks +- Benchmark all 4 benchmarks after this step + +After Tier 1, re-run all benchmarks and record results. If closure and method +are within 10% of master, the optimization work is done. If not, proceed to +Tier 2. + +--- + +**Tier 2: Consolidate WarningBits/HintHash stacks into PerlRuntime (MEDIUM RISK)** + +This tier targets the **closure** regression specifically. The closure call +path has zero `PerlRuntime.current()` lookups (Tier 1 will not help it) but +14-17 ThreadLocal lookups from 7 separate ThreadLocal stacks: + +| ThreadLocal | Class | push/pop per call | +|-------------|-------|-------------------| +| `currentBitsStack` | WarningBitsRegistry | 2 (push + pop) | +| `callerBitsStack` | WarningBitsRegistry | 2 | +| `callerHintsStack` | WarningBitsRegistry | 2 | +| `callSiteBits` | WarningBitsRegistry | 1 (get) | +| `callSiteHints` | WarningBitsRegistry | 1 (get) | +| `callSiteSnapshotId` | HintHashRegistry | 2 (get + set) | +| `callerSnapshotIdStack` | HintHashRegistry | 2 | +| `argsStack` | RuntimeCode | 2 | + +**Approach:** Migrate these 8 ThreadLocal stacks into `PerlRuntime` instance +fields, following the same accessor-method pattern used for all other +migrated stacks (see "Local Save/Restore Stack Fix" section above). This +turns 14-17 ThreadLocal lookups into 1 (`PerlRuntime.current()` at method +entry, then direct field access). + +Concrete steps: +1. Add 8 stack fields to `PerlRuntime.java` +2. Add `private static` accessor methods in each source class that delegate + to `PerlRuntime.current().` (same pattern as `localizedStack()` etc.) +3. Replace `threadLocalField.get()` with the accessor call in each push/pop site +4. Remove the ThreadLocal field declarations from WarningBitsRegistry, + HintHashRegistry, and RuntimeCode +5. Verify: `make` passes, then benchmark closure + +**Expected impact on closure benchmark:** high — 14 fewer ThreadLocal lookups +per call. This is the dominant cost in the closure path. + +**Revert criteria:** If closure benchmark does not improve by at least 15% +(i.e., does not recover at least half the 34% regression), revert unless the +migration is considered architecturally desirable for consistency with the +other stack migrations. + +--- + +**Tier 3: Investigate inline method cache effectiveness (LOW RISK)** + +Only pursue this if method dispatch is still >10% slower after Tier 1. + +Each `PerlRuntime` starts with empty inline method cache arrays +(`inlineCacheBlessId`, `inlineCacheMethodHash`, `inlineCacheCode`). The +cache is indexed by `callsiteId % CACHE_SIZE`, so it relies on a warm +steady state. + +**Diagnostic step (before any code change):** +```bash +# Add temporary counters to callCached() to measure hit/miss ratio: +# - Count cache hits (blessId matches AND methodHash matches) +# - Count cache misses +# Run benchmark_method.pl and report hit rate +``` + +If hit rate is >95%, the cache is working and the regression is from the +single `PerlRuntime.current()` lookup per call (unavoidable overhead of ~2-5ns). +If hit rate is low, investigate why — possible causes: +- `callsiteId` collisions across runtimes (IDs are global AtomicIntegers) +- Cache size too small for the workload +- BlessId instability across runtime initialization + +--- + +**Failed Optimization Attempts** + +(Document failed attempts here so future engineers know what was already tried. +For each attempt, record: what was changed, benchmark numbers before/after, +and why it was reverted.) + +*None yet.* + +--- + +### Optimization Results (2026-04-10) + +**Branch:** `feature/multiplicity-opt` (created from `feature/multiplicity`) + +#### JFR Profiling Findings + +Profiled the closure benchmark (`dev/bench/benchmark_closure.pl`) with Java +Flight Recorder to identify the dominant overhead sources. Key findings: + +| Category | JFR Samples | Source | +|----------|-------------|--------| +| `PerlRuntime.current()` / ThreadLocal | 143 | The ThreadLocal.get() call itself | +| RuntimeRegex accessors | ~126 | **Dominant source** — 13 getters + 13 setters per sub call via RegexState save/restore | +| DynamicVariableManager | ~90 | `local` variable save/restore (includes regex state push) | +| pushCallerState (batch) | 10 | Caller bits/hints/hint-hash state | +| pushSubState (batch) | 3 | Args + warning bits | +| ArrayDeque.push | 13 | Stack operations | + +**Critical finding:** `RegexState` save/restore was calling 13 individual +`RuntimeRegex` static accessors (each doing its own `PerlRuntime.current()` +ThreadLocal lookup) on every subroutine entry AND exit — **26 ThreadLocal +lookups per sub call** — even when the subroutine never uses regex. + +#### Optimizations Applied + +| Tier | Optimization | Files Changed | ThreadLocal Lookups Eliminated | +|------|-------------|---------------|-------------------------------| +| 1 | Cache `PerlRuntime.current()` in local variables | GlobalVariable.java, InheritanceResolver.java | ~12 per method cache miss | +| 2 | Migrate WarningBits/HintHash/args stacks to PerlRuntime fields | WarningBitsRegistry.java, HintHashRegistry.java, RuntimeCode.java, PerlRuntime.java | 14-17 per sub call (separate ThreadLocals -> 1 ThreadLocal) | +| 2b | Batch pushCallerState/popCallerState and pushSubState/popSubState | PerlRuntime.java, RuntimeCode.java | 8-12 per sub call -> 2 | +| 2c | Batch RegexState save/restore | RegexState.java | **24 per sub call** (13 getters + 13 setters -> 2) | +| 2d | Skip RegexState save/restore for subs without regex | EmitterMethodCreator.java, RegexUsageDetector.java | **Entire save/restore eliminated** for non-regex subs | +| 3 | JVM-compile anonymous subs inside `eval STRING` | BytecodeCompiler.java, OpcodeHandlerExtended.java, JvmClosureTemplate.java (new) | N/A (execution speedup, not ThreadLocal reduction) | + +**Tier 3: JVM compilation of eval STRING anonymous subs.** Previously, +`BytecodeCompiler.visitAnonymousSubroutine()` always compiled anonymous sub bodies +to `InterpretedCode`. This meant hot closures created via `eval STRING` (e.g., +Benchmark.pm's `sub { for (1..$n) { &$c } }`) ran in the bytecode interpreter. +The fix tries JVM compilation first via `EmitterMethodCreator.createClassWithMethod()`, +falling back to the interpreter on any failure (e.g., ASM frame computation crash). +A new `JvmClosureTemplate` class holds the JVM-compiled class and instantiates it +with captured variables via reflection. Measured 4.5x speedup for eval STRING closures +in isolation (6.4M iter/s vs 1.4M iter/s). The broader benchmark improvements (method ++36.7% vs pre-opt, global +10.8%) likely reflect this change improving Benchmark.pm's +own infrastructure which is used by all benchmarks. + +#### Benchmark Results (updated 2026-04-10, after JVM eval STRING closures) + +| Benchmark | master | branch (pre-opt) | **Current** | vs master | vs pre-opt | +|-----------|--------|-------------------|-------------|-----------|------------| +| **closure** | 863 | 569 (-34.1%) | **1,220** | **+41.4%** | +114.4% | +| **method** | 436 | 319 (-26.9%) | **436** | **0.0%** | +36.7% | +| **lexical** | 394K | 375K (-4.9%) | **480K** | **+21.8%** | +28.0% | +| global | 78K | 74K (-5.4%) | **82K** | +5.1% | +10.8% | +| eval_string | 86K | 82K (-4.8%) | **89K** | +3.1% | +8.5% | +| regex | 51K | 47K (-7.0%) | **46K** | -9.4% | -2.1% | +| string | 29K | 31K (+6.5%) | **29K** | +0.3% | -5.6% | + +All benchmarks that originally regressed are now **at or above master**. Closure is +41% faster than master, method matches master exactly, lexical is 22% faster, and +global is 5% faster. The closure and lexical improvements come from eliminating +unnecessary RegexState save/restore for subroutines that don't use regex — an +overhead that existed on master too (via separate ThreadLocals) but was masked by +lower per-lookup cost. The regex benchmark shows a small regression (~9%) from +ThreadLocal routing overhead in regex-heavy code paths. + +#### Remaining Optimization Opportunities (not yet pursued) + +These are lower-priority since the main goal (closure/method within 10%) is exceeded: + +| Option | Effort | Expected Impact | Notes | +|--------|--------|-----------------|-------| +| Pass `PerlRuntime rt` from static apply to instance apply | Low | Eliminates 1 of 2 remaining lookups per sub call | Changes method signatures | +| Cache warning bits on RuntimeCode field | Low | Avoids ConcurrentHashMap lookup per call | `getWarningBitsForCode()` in profile | +| Batch RuntimeRegex field access in match methods | Medium | Eliminates ~10-15 lookups per regex match | Profile showed many individual accessors in RuntimeRegex.java; may help the -9% regex regression | +| DynamicVariableManager.variableStack() caching | Low | 1 lookup per call eliminated | 10 samples in profile | + +Note: "Lazy regex state save (skip when sub doesn't use regex)" was listed here previously +and has been implemented as Tier 2d above. + +### Open Questions +- `runtimeEvalCounter` and `nextCallsiteId` remain static (shared across runtimes) — + acceptable for unique ID generation but may want per-runtime counters in future +- Should `ByteCodeSourceMapper` collections be migrated to per-runtime long-term? + (Currently they're only needed during compilation, so the global lock is sufficient) diff --git a/dev/sandbox/multiplicity/MultiplicityDemo.java b/dev/sandbox/multiplicity/MultiplicityDemo.java new file mode 100644 index 000000000..c50bba828 --- /dev/null +++ b/dev/sandbox/multiplicity/MultiplicityDemo.java @@ -0,0 +1,136 @@ +package org.perlonjava.demo; + +import org.perlonjava.app.cli.CompilerOptions; +import org.perlonjava.app.scriptengine.PerlLanguageProvider; +import org.perlonjava.runtime.io.StandardIO; +import org.perlonjava.runtime.runtimetypes.*; + +import java.io.ByteArrayOutputStream; +import java.io.PrintStream; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** + * Demonstrates PerlOnJava's "multiplicity" feature: multiple independent Perl + * interpreters running concurrently within a single JVM process. + * + * Each interpreter has its own global variables, regex state, @INC, %ENV, etc. + * They share the JVM heap; generated classes are loaded into each runtime's own + * ClassLoader and become eligible for GC once the runtime is discarded. + * + * Usage: + * java -cp target/perlonjava-5.42.0.jar \ + * org.perlonjava.demo.MultiplicityDemo script1.pl script2.pl ... + * + * Or with the helper script: + * ./dev/sandbox/multiplicity/run_multiplicity_demo.sh script1.pl script2.pl ... + * + * See also: dev/design/concurrency.md (Multiplicity Demo section) + */ +public class MultiplicityDemo { + + public static void main(String[] args) throws Exception { + if (args.length == 0) { + System.err.println("Usage: MultiplicityDemo [script2.pl] ..."); + System.err.println("Runs each Perl script in its own interpreter, concurrently."); + System.exit(1); + } + + // Read all script files upfront + List scriptNames = new ArrayList<>(); + List scriptSources = new ArrayList<>(); + for (String arg : args) { + Path p = Path.of(arg); + if (!Files.isRegularFile(p)) { + System.err.println("Error: not a file: " + arg); + System.exit(1); + } + scriptNames.add(p.getFileName().toString()); + scriptSources.add(Files.readString(p)); + } + + int n = scriptNames.size(); + System.out.println("=== PerlOnJava Multiplicity Demo ==="); + System.out.println("Starting " + n + " independent Perl interpreter(s)...\n"); + + // Per-thread output capture + ByteArrayOutputStream[] outputs = new ByteArrayOutputStream[n]; + long[] durations = new long[n]; + Throwable[] errors = new Throwable[n]; + + Thread[] threads = new Thread[n]; + for (int i = 0; i < n; i++) { + final int idx = i; + final String name = scriptNames.get(i); + final String source = scriptSources.get(i); + outputs[idx] = new ByteArrayOutputStream(); + + threads[i] = new Thread(() -> { + try { + // --- Create an independent PerlRuntime for this thread --- + PerlRuntime.initialize(); + + // Redirect this interpreter's STDOUT to a private buffer. + // RuntimeIO.setStdout() operates on the current thread's PerlRuntime. + PrintStream ps = new PrintStream(outputs[idx], true); + RuntimeIO customOut = new RuntimeIO(new StandardIO(ps, true)); + RuntimeIO.setStdout(customOut); + RuntimeIO.setSelectedHandle(customOut); + RuntimeIO.setLastWrittenHandle(customOut); + + // Set up compiler options + CompilerOptions options = new CompilerOptions(); + options.code = source; + options.fileName = name; + + // Use executePerlCode() which handles the full lifecycle: + // - GlobalContext.initializeGlobals() (per-runtime, under COMPILE_LOCK) + // - Tokenize, parse, compile (under COMPILE_LOCK — serialized) + // - Run UNITCHECK, CHECK, INIT blocks + // - Execute the main code (no lock — concurrent) + // - Run END blocks + long t0 = System.nanoTime(); + PerlLanguageProvider.executePerlCode(options, true); + durations[idx] = System.nanoTime() - t0; + + // Flush buffered output + RuntimeIO.flushFileHandles(); + + } catch (Throwable t) { + errors[idx] = t; + } + }, "perl-" + name); + } + + // Start all threads + long wallStart = System.nanoTime(); + for (Thread t : threads) t.start(); + for (Thread t : threads) t.join(60_000); // 60s safety timeout + long wallElapsed = System.nanoTime() - wallStart; + + // --- Print results --- + System.out.println("=== Output from each interpreter ===\n"); + for (int i = 0; i < n; i++) { + System.out.println("--- " + scriptNames.get(i) + " ---"); + if (errors[i] != null) { + System.out.println(" ERROR: " + errors[i].getMessage()); + errors[i].printStackTrace(System.out); + } else { + // Indent each line for readability + String out = outputs[i].toString().stripTrailing(); + for (String line : out.split("\n")) { + System.out.println(" " + line); + } + System.out.printf(" (executed in %.1f ms)%n", durations[i] / 1_000_000.0); + } + System.out.println(); + } + + System.out.printf("=== All %d interpreters finished (wall time: %.1f ms) ===%n", + n, wallElapsed / 1_000_000.0); + } +} diff --git a/dev/sandbox/multiplicity/README.md b/dev/sandbox/multiplicity/README.md new file mode 100644 index 000000000..33d18dcc4 --- /dev/null +++ b/dev/sandbox/multiplicity/README.md @@ -0,0 +1,45 @@ +# Multiplicity Demo + +Demonstrates PerlOnJava's "multiplicity" feature: multiple independent Perl +interpreters running concurrently within a single JVM process. + +Each interpreter has its own global variables, regex state, `@INC`, `%ENV`, +current working directory, process ID (`$$`), etc. They share the JVM heap; +generated classes are loaded into each runtime's own ClassLoader and become +eligible for GC once the runtime is discarded. + +## Quick Start + +```bash +# Build the project first +make dev + +# Run with the bundled demo scripts +./dev/sandbox/multiplicity/run_multiplicity_demo.sh + +# Run with custom scripts +./dev/sandbox/multiplicity/run_multiplicity_demo.sh script1.pl script2.pl + +# Run all unit tests concurrently (stress test) +./dev/sandbox/multiplicity/run_multiplicity_demo.sh src/test/resources/unit/*.t +``` + +## Files + +| File | Description | +|------|-------------| +| `MultiplicityDemo.java` | Java driver that creates N threads, each with its own `PerlRuntime` | +| `run_multiplicity_demo.sh` | Shell wrapper that compiles and runs the demo | +| `multiplicity_script1.pl` | Demo script: basic variable isolation | +| `multiplicity_script2.pl` | Demo script: regex state isolation | +| `multiplicity_script3.pl` | Demo script: module loading isolation | + +## Current Status + +- **122/126** unit tests pass with 126 concurrent interpreters +- Only 4 failures remain: `tie_*.t` (pre-existing `DESTROY` not implemented) + +## Design Document + +See [dev/design/concurrency.md](../../design/concurrency.md) for the full +concurrency design, implementation phases, and progress tracking. diff --git a/dev/sandbox/multiplicity/multiplicity_script1.pl b/dev/sandbox/multiplicity/multiplicity_script1.pl new file mode 100644 index 000000000..e81dadfa8 --- /dev/null +++ b/dev/sandbox/multiplicity/multiplicity_script1.pl @@ -0,0 +1,26 @@ +# multiplicity_script1.pl — Demonstrates isolated state in interpreter 1 +use strict; +use warnings; + +my $id = "Interpreter-1"; +$_ = "Hello from $id"; + +# Set a global variable +our $shared_test = 42; + +# Regex match — regex state ($1, $&, etc.) should be isolated +"The quick brown fox" =~ /(\w+)\s+(\w+)/; +my $match = "$1 $2"; + +# Simulate some work +my $sum = 0; +for my $i (1..1000) { + $sum += $i; +} + +print "[$id] \$_ = $_\n"; +print "[$id] Regex match: $match\n"; +print "[$id] \$shared_test = $shared_test\n"; +print "[$id] Sum 1..1000 = $sum\n"; +print "[$id] \@INC has " . scalar(@INC) . " entries\n"; +print "[$id] Done!\n"; diff --git a/dev/sandbox/multiplicity/multiplicity_script2.pl b/dev/sandbox/multiplicity/multiplicity_script2.pl new file mode 100644 index 000000000..3ddda4079 --- /dev/null +++ b/dev/sandbox/multiplicity/multiplicity_script2.pl @@ -0,0 +1,26 @@ +# multiplicity_script2.pl — Demonstrates isolated state in interpreter 2 +use strict; +use warnings; + +my $id = "Interpreter-2"; +$_ = "Greetings from $id"; + +# This variable should NOT see interpreter 1's value +our $shared_test = 99; + +# Different regex — state should not leak from interpreter 1 +"2025-04-10" =~ /(\d{4})-(\d{2})-(\d{2})/; +my $match = "$1/$2/$3"; + +# Different computation +my $product = 1; +for my $i (1..10) { + $product *= $i; +} + +print "[$id] \$_ = $_\n"; +print "[$id] Regex match: $match\n"; +print "[$id] \$shared_test = $shared_test\n"; +print "[$id] 10! = $product\n"; +print "[$id] \@INC has " . scalar(@INC) . " entries\n"; +print "[$id] Done!\n"; diff --git a/dev/sandbox/multiplicity/multiplicity_script3.pl b/dev/sandbox/multiplicity/multiplicity_script3.pl new file mode 100644 index 000000000..a34b0e7ae --- /dev/null +++ b/dev/sandbox/multiplicity/multiplicity_script3.pl @@ -0,0 +1,26 @@ +# multiplicity_script3.pl — Demonstrates isolated state in interpreter 3 +use strict; +use warnings; + +my $id = "Interpreter-3"; +$_ = "Bonjour from $id"; + +# Independent global +our $shared_test = -1; + +# Yet another regex +"foo\@bar.com" =~ /(\w+)\@(\w+)\.(\w+)/; +my $match = "user=$1 domain=$2 tld=$3"; + +# Fibonacci +my @fib = (0, 1); +for my $i (2..20) { + push @fib, $fib[-1] + $fib[-2]; +} + +print "[$id] \$_ = $_\n"; +print "[$id] Regex match: $match\n"; +print "[$id] \$shared_test = $shared_test\n"; +print "[$id] Fib(20) = $fib[20]\n"; +print "[$id] \@INC has " . scalar(@INC) . " entries\n"; +print "[$id] Done!\n"; diff --git a/dev/sandbox/multiplicity/run_multiplicity_demo.sh b/dev/sandbox/multiplicity/run_multiplicity_demo.sh new file mode 100755 index 000000000..2c6084d19 --- /dev/null +++ b/dev/sandbox/multiplicity/run_multiplicity_demo.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# +# Compile and run the Multiplicity Demo. +# +# Usage: +# ./dev/sandbox/multiplicity/run_multiplicity_demo.sh [script1.pl script2.pl ...] +# +# If no scripts are given, runs the three bundled demo scripts. +# +# See also: dev/design/concurrency.md (Multiplicity Demo section) +# +set -euo pipefail +cd "$(git -C "$(dirname "$0")" rev-parse --show-toplevel)" + +DEMO_SRC="dev/sandbox/multiplicity/MultiplicityDemo.java" +DEMO_DIR="dev/sandbox/multiplicity" + +# Find the fat JAR the same way jperl does +if [ -f "target/perlonjava-5.42.0.jar" ]; then + JAR="target/perlonjava-5.42.0.jar" +elif [ -f "perlonjava-5.42.0.jar" ]; then + JAR="perlonjava-5.42.0.jar" +else + echo "Fat JAR not found. Run 'make dev' first." + exit 1 +fi + +# Compile the demo against the fat JAR +echo "Compiling MultiplicityDemo.java..." +javac -d "$DEMO_DIR" -cp "$JAR" "$DEMO_SRC" + +# Default scripts if none provided +if [ $# -eq 0 ]; then + set -- dev/sandbox/multiplicity/multiplicity_script1.pl \ + dev/sandbox/multiplicity/multiplicity_script2.pl \ + dev/sandbox/multiplicity/multiplicity_script3.pl +fi + +echo "" +# Run with the demo class prepended to the classpath +java -cp "$DEMO_DIR:$JAR" org.perlonjava.demo.MultiplicityDemo "$@" diff --git a/src/main/java/org/perlonjava/app/cli/Main.java b/src/main/java/org/perlonjava/app/cli/Main.java index 9600e299f..66dc86e8a 100644 --- a/src/main/java/org/perlonjava/app/cli/Main.java +++ b/src/main/java/org/perlonjava/app/cli/Main.java @@ -4,6 +4,7 @@ import org.perlonjava.runtime.runtimetypes.ErrorMessageUtil; import org.perlonjava.runtime.runtimetypes.GlobalVariable; import org.perlonjava.runtime.runtimetypes.PerlExitException; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; import java.util.Locale; @@ -26,6 +27,9 @@ public class Main { * @param args Command-line arguments. */ public static void main(String[] args) { + // Initialize the PerlRuntime for the main thread + PerlRuntime.initialize(); + CompilerOptions parsedArgs = ArgumentParser.parseArguments(args); if (parsedArgs.code == null) { diff --git a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java index 82fde36f4..cae9c06df 100644 --- a/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java +++ b/src/main/java/org/perlonjava/app/scriptengine/PerlLanguageProvider.java @@ -26,6 +26,7 @@ import java.lang.invoke.MethodHandle; import java.lang.reflect.Constructor; import java.util.List; +import java.util.concurrent.locks.ReentrantLock; import static org.perlonjava.runtime.runtimetypes.GlobalVariable.resetAllGlobals; import static org.perlonjava.runtime.runtimetypes.SpecialBlock.*; @@ -52,10 +53,33 @@ */ public class PerlLanguageProvider { - private static boolean globalInitialized = false; + /** + * Global compile lock. The parser and emitter have shared mutable static state + * (SpecialBlockParser.symbolTable, ByteCodeSourceMapper collections, etc.) that + * is not yet thread-safe. All compilation paths — initial compilePerlCode() and + * runtime eval "string" via EvalStringHandler — must acquire this lock. + *

+ * This serializes compilation across threads but allows concurrent execution + * of already-compiled code. Future work (Phase 0 completion) will migrate the + * remaining shared state to per-PerlRuntime instances, eliminating this lock. + */ + public static final ReentrantLock COMPILE_LOCK = new ReentrantLock(); + + /** + * Ensures a PerlRuntime is bound to the current thread. + * Called at the start of every entry point (executePerlCode, compilePerlCode, etc.) + * to support both CLI (where Main.main() initializes) and JSR-223 (where the + * ScriptEngine may be called from any thread). + */ + private static void ensureRuntimeInitialized() { + if (PerlRuntime.currentOrNull() == null) { + PerlRuntime.initialize(); + } + } public static void resetAll() { - globalInitialized = false; + ensureRuntimeInitialized(); + PerlRuntime.current().globalInitialized = false; resetAllGlobals(); DataSection.reset(); } @@ -85,9 +109,8 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions, boolean isTopLevelScript, int callerContext) throws Exception { - // Save the current scope so we can restore it after execution. - // This is critical because require/do should not leak their scope to the caller. - ScopedSymbolTable savedCurrentScope = SpecialBlockParser.getCurrentScope(); + ensureRuntimeInitialized(); + PerlRuntime runtime = PerlRuntime.current(); // Save and clear the eval runtime context so that modules loaded via require/do // during eval STRING execution don't see the eval's captured variables. @@ -100,125 +123,141 @@ public static RuntimeList executePerlCode(CompilerOptions compilerOptions, // Store the isMainProgram flag in CompilerOptions for use during code generation compilerOptions.isMainProgram = isTopLevelScript; - ScopedSymbolTable globalSymbolTable = new ScopedSymbolTable(); - // Enter a new scope in the symbol table and add special Perl variables - globalSymbolTable.enterScope(); - globalSymbolTable.addVariable("this", "", null); // anon sub instance is local variable 0 - globalSymbolTable.addVariable("@_", "our", null); // Argument list is local variable 1 - globalSymbolTable.addVariable("wantarray", "", null); // Call context is local variable 2 + // ---- Compilation phase (under COMPILE_LOCK) ---- + // The parser and emitter have shared mutable static state that requires serialization. + // The lock is released before execution so compiled code can run concurrently. + ScopedSymbolTable savedCurrentScope; + RuntimeCode runtimeCode; + EmitterContext ctx; - if (compilerOptions.codeHasEncoding) { - globalSymbolTable.enableStrictOption(Strict.HINT_UTF8); - } - - // Use caller's context if specified, otherwise default based on script type - int contextType = callerContext >= 0 ? callerContext : - (isTopLevelScript ? RuntimeContextType.VOID : RuntimeContextType.SCALAR); + COMPILE_LOCK.lock(); + try { + // Save the current scope so we can restore it after execution. + // This is critical because require/do should not leak their scope to the caller. + savedCurrentScope = SpecialBlockParser.getCurrentScope(); + + ScopedSymbolTable globalSymbolTable = new ScopedSymbolTable(); + // Enter a new scope in the symbol table and add special Perl variables + globalSymbolTable.enterScope(); + globalSymbolTable.addVariable("this", "", null); // anon sub instance is local variable 0 + globalSymbolTable.addVariable("@_", "our", null); // Argument list is local variable 1 + globalSymbolTable.addVariable("wantarray", "", null); // Call context is local variable 2 + + if (compilerOptions.codeHasEncoding) { + globalSymbolTable.enableStrictOption(Strict.HINT_UTF8); + } - // Create the compiler context - EmitterContext ctx = new EmitterContext( - new JavaClassInfo(), // internal java class name - globalSymbolTable.snapShot(), // Top-level symbol table - null, // Method visitor - null, // Class writer - contextType, // Call context - scalar for require/do, void for top-level - true, // Is boxed - null, // errorUtil - compilerOptions, - new RuntimeArray() - ); + // Use caller's context if specified, otherwise default based on script type + int contextType = callerContext >= 0 ? callerContext : + (isTopLevelScript ? RuntimeContextType.VOID : RuntimeContextType.SCALAR); + + // Create the compiler context + ctx = new EmitterContext( + new JavaClassInfo(), // internal java class name + globalSymbolTable.snapShot(), // Top-level symbol table + null, // Method visitor + null, // Class writer + contextType, // Call context - scalar for require/do, void for top-level + true, // Is boxed + null, // errorUtil + compilerOptions, + new RuntimeArray() + ); - if (!globalInitialized) { - GlobalContext.initializeGlobals(compilerOptions); - globalInitialized = true; - } + if (!runtime.globalInitialized) { + GlobalContext.initializeGlobals(compilerOptions); + runtime.globalInitialized = true; + } - if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("parse code: " + compilerOptions.code); - if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug(" call context " + ctx.contextType); + if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("parse code: " + compilerOptions.code); + if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug(" call context " + ctx.contextType); - // Apply any BEGIN-block filters before tokenization if requested - // This is a workaround for the limitation that our architecture tokenizes all source upfront - if (compilerOptions.applySourceFilters) { - compilerOptions.code = FilterUtilCall.preprocessWithBeginFilters(compilerOptions.code); - } + // Apply any BEGIN-block filters before tokenization if requested + // This is a workaround for the limitation that our architecture tokenizes all source upfront + if (compilerOptions.applySourceFilters) { + compilerOptions.code = FilterUtilCall.preprocessWithBeginFilters(compilerOptions.code); + } - // Create the LexerToken list - Lexer lexer = new Lexer(compilerOptions.code); - List tokens = lexer.tokenize(); // Tokenize the Perl code - if (ctx.compilerOptions.tokenizeOnly) { - // Printing the tokens - for (LexerToken token : tokens) { - System.out.println(token); + // Create the LexerToken list + Lexer lexer = new Lexer(compilerOptions.code); + List tokens = lexer.tokenize(); // Tokenize the Perl code + if (ctx.compilerOptions.tokenizeOnly) { + // Printing the tokens + for (LexerToken token : tokens) { + System.out.println(token); + } + RuntimeIO.closeAllHandles(); + return null; // success } - RuntimeIO.closeAllHandles(); - return null; // success - } - compilerOptions.code = null; // Throw away the source code to spare memory + compilerOptions.code = null; // Throw away the source code to spare memory - // Create the AST - // Create an instance of ErrorMessageUtil with the file name and token list - ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); - Parser parser = new Parser(ctx, tokens); // Parse the tokens - parser.isTopLevelScript = isTopLevelScript; + // Create the AST + // Create an instance of ErrorMessageUtil with the file name and token list + ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); + Parser parser = new Parser(ctx, tokens); // Parse the tokens + parser.isTopLevelScript = isTopLevelScript; - // Create placeholder DATA filehandle early so it's available during BEGIN block execution - // This ensures *ARGV = *DATA aliasing works correctly in BEGIN blocks - DataSection.createPlaceholderDataHandle(parser); + // Create placeholder DATA filehandle early so it's available during BEGIN block execution + // This ensures *ARGV = *DATA aliasing works correctly in BEGIN blocks + DataSection.createPlaceholderDataHandle(parser); - Node ast; - if (isTopLevelScript) { - CallerStack.push( - "main", - ctx.compilerOptions.fileName, - ctx.errorUtil.getLineNumber(parser.tokenIndex)); - // Push the main script onto BHooksEndOfScope's loading stack so that - // on_scope_end callbacks (e.g., from namespace::clean) are deferred - // until end of parsing, matching Perl 5 behavior. - BHooksEndOfScope.beginFileLoad(ctx.compilerOptions.fileName); - } - try { - ast = parser.parse(); // Generate the abstract syntax tree (AST) - } finally { + Node ast; if (isTopLevelScript) { - // Fire on_scope_end callbacks now that parsing is complete. - // This is the "end of compilation scope" equivalent. - BHooksEndOfScope.endFileLoad(ctx.compilerOptions.fileName); - CallerStack.pop(); + CallerStack.push( + "main", + ctx.compilerOptions.fileName, + ctx.errorUtil.getLineNumber(parser.tokenIndex)); + // Push the main script onto BHooksEndOfScope's loading stack so that + // on_scope_end callbacks (e.g., from namespace::clean) are deferred + // until end of parsing, matching Perl 5 behavior. + BHooksEndOfScope.beginFileLoad(ctx.compilerOptions.fileName); + } + try { + ast = parser.parse(); // Generate the abstract syntax tree (AST) + } finally { + if (isTopLevelScript) { + // Fire on_scope_end callbacks now that parsing is complete. + // This is the "end of compilation scope" equivalent. + BHooksEndOfScope.endFileLoad(ctx.compilerOptions.fileName); + CallerStack.pop(); + } } - } - - // ast = ConstantFoldingVisitor.foldConstants(ast); - // Constant folding: inline user-defined constant subs and fold constant expressions. - // This runs after parsing (so BEGIN blocks have executed and constants are defined) - // and before code emission. The package from the symbol table is used to resolve - // bare constant identifiers (e.g., PI from `use constant PI => 3.14`). - ast = ConstantFoldingVisitor.foldConstants(ast, ctx.symbolTable.getCurrentPackage()); + // ast = ConstantFoldingVisitor.foldConstants(ast); - if (ctx.compilerOptions.parseOnly) { - // Printing the ast - System.out.println(ast); - RuntimeIO.closeAllHandles(); - return null; // success - } - if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("-- AST:\n" + ast + "--\n"); + // Constant folding: inline user-defined constant subs and fold constant expressions. + // This runs after parsing (so BEGIN blocks have executed and constants are defined) + // and before code emission. The package from the symbol table is used to resolve + // bare constant identifiers (e.g., PI from `use constant PI => 3.14`). + ast = ConstantFoldingVisitor.foldConstants(ast, ctx.symbolTable.getCurrentPackage()); - // Create the Java class from the AST - if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("createClassWithMethod"); - // Create a new instance of ErrorMessageUtil, resetting the line counter - ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); - // Snapshot the symbol table after parsing. - // The parser records lexical declarations (e.g., `for my $p (...)`) and pragma state - // (strict/warnings/features) into ctx.symbolTable. Resetting to a fresh global snapshot - // loses those declarations and causes strict-vars failures during codegen. - ctx.symbolTable = ctx.symbolTable.snapShot(); - SpecialBlockParser.setCurrentScope(ctx.symbolTable); + if (ctx.compilerOptions.parseOnly) { + // Printing the ast + System.out.println(ast); + RuntimeIO.closeAllHandles(); + return null; // success + } + if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("-- AST:\n" + ast + "--\n"); + + // Create the Java class from the AST + if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("createClassWithMethod"); + // Create a new instance of ErrorMessageUtil, resetting the line counter + ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); + // Snapshot the symbol table after parsing. + // The parser records lexical declarations (e.g., `for my $p (...)`) and pragma state + // (strict/warnings/features) into ctx.symbolTable. Resetting to a fresh global snapshot + // loses those declarations and causes strict-vars failures during codegen. + ctx.symbolTable = ctx.symbolTable.snapShot(); + SpecialBlockParser.setCurrentScope(ctx.symbolTable); - try { // Compile to executable (compiler or interpreter based on flag) - RuntimeCode runtimeCode = compileToExecutable(ast, ctx); + runtimeCode = compileToExecutable(ast, ctx); + } finally { + COMPILE_LOCK.unlock(); + } - // Execute (unified path for both backends) + // ---- Execution phase (no lock — compiled code is thread-safe) ---- + try { return executeCode(runtimeCode, ctx, isTopLevelScript, callerContext); } finally { // Restore the caller's scope so require/do doesn't leak its scope to the caller. @@ -261,6 +300,8 @@ public static RuntimeList executePerlAST(Node ast, CompilerOptions compilerOptions, int contextType) throws Exception { + ensureRuntimeInitialized(); + // Save the current scope so we can restore it after execution. ScopedSymbolTable savedCurrentScope = SpecialBlockParser.getCurrentScope(); @@ -305,9 +346,9 @@ public static RuntimeList executePerlAST(Node ast, new RuntimeArray() ); - if (!globalInitialized) { + if (!PerlRuntime.current().globalInitialized) { GlobalContext.initializeGlobals(compilerOptions); - globalInitialized = true; + PerlRuntime.current().globalInitialized = true; } if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Using provided AST"); @@ -577,51 +618,58 @@ private static boolean needsInterpreterFallback(Throwable e) { * @throws Exception if compilation fails */ public static Object compilePerlCode(CompilerOptions compilerOptions) throws Exception { - ScopedSymbolTable globalSymbolTable = new ScopedSymbolTable(); - globalSymbolTable.enterScope(); - globalSymbolTable.addVariable("this", "", null); // anon sub instance is local variable 0 - globalSymbolTable.addVariable("@_", "our", null); // Argument list is local variable 1 - globalSymbolTable.addVariable("wantarray", "", null); // Call context is local variable 2 + ensureRuntimeInitialized(); - if (compilerOptions.codeHasEncoding) { - globalSymbolTable.enableStrictOption(Strict.HINT_UTF8); - } + COMPILE_LOCK.lock(); + try { + ScopedSymbolTable globalSymbolTable = new ScopedSymbolTable(); + globalSymbolTable.enterScope(); + globalSymbolTable.addVariable("this", "", null); // anon sub instance is local variable 0 + globalSymbolTable.addVariable("@_", "our", null); // Argument list is local variable 1 + globalSymbolTable.addVariable("wantarray", "", null); // Call context is local variable 2 + + if (compilerOptions.codeHasEncoding) { + globalSymbolTable.enableStrictOption(Strict.HINT_UTF8); + } - EmitterContext ctx = new EmitterContext( - new JavaClassInfo(), - globalSymbolTable.snapShot(), - null, - null, - RuntimeContextType.SCALAR, // Default to SCALAR context - true, - null, - compilerOptions, - new RuntimeArray() - ); + EmitterContext ctx = new EmitterContext( + new JavaClassInfo(), + globalSymbolTable.snapShot(), + null, + null, + RuntimeContextType.SCALAR, // Default to SCALAR context + true, + null, + compilerOptions, + new RuntimeArray() + ); - if (!globalInitialized) { - GlobalContext.initializeGlobals(compilerOptions); - globalInitialized = true; - } + if (!PerlRuntime.current().globalInitialized) { + GlobalContext.initializeGlobals(compilerOptions); + PerlRuntime.current().globalInitialized = true; + } - // Tokenize - Lexer lexer = new Lexer(compilerOptions.code); - List tokens = lexer.tokenize(); - compilerOptions.code = null; // Free memory + // Tokenize + Lexer lexer = new Lexer(compilerOptions.code); + List tokens = lexer.tokenize(); + compilerOptions.code = null; // Free memory - // Parse - ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); - Parser parser = new Parser(ctx, tokens); - parser.isTopLevelScript = false; // Not top-level for compiled script - Node ast = parser.parse(); + // Parse + ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); + Parser parser = new Parser(ctx, tokens); + parser.isTopLevelScript = false; // Not top-level for compiled script + Node ast = parser.parse(); - // Compile to class or bytecode based on flag - ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); - ctx.symbolTable = ctx.symbolTable.snapShot(); - SpecialBlockParser.setCurrentScope(ctx.symbolTable); + // Compile to class or bytecode based on flag + ctx.errorUtil = new ErrorMessageUtil(ctx.compilerOptions.fileName, tokens); + ctx.symbolTable = ctx.symbolTable.snapShot(); + SpecialBlockParser.setCurrentScope(ctx.symbolTable); - // Use unified compilation path (works for JSR 223 too!) - return compileToExecutable(ast, ctx); + // Use unified compilation path (works for JSR 223 too!) + return compileToExecutable(ast, ctx); + } finally { + COMPILE_LOCK.unlock(); + } } } diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java index 59dcac611..04b1d247b 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeCompiler.java @@ -3,6 +3,9 @@ import org.perlonjava.backend.jvm.EmitterContext; import org.perlonjava.backend.jvm.EmitterMethodCreator; +import org.perlonjava.backend.jvm.InterpreterFallbackException; +import org.perlonjava.backend.jvm.JavaClassInfo; +import org.perlonjava.backend.jvm.JvmClosureTemplate; import org.perlonjava.frontend.analysis.ConstantFoldingVisitor; import org.perlonjava.frontend.analysis.FindDeclarationVisitor; import org.perlonjava.frontend.analysis.RegexUsageDetector; @@ -76,8 +79,8 @@ public class BytecodeCompiler implements Visitor { // Token index tracking for error reporting private final TreeMap pcToTokenIndex = new TreeMap<>(); int currentTokenIndex = -1; // Track current token for error reporting - // Callsite ID counter for /o modifier support (unique across all compilations) - private static int nextCallsiteId = 1; + // Callsite ID counter for /o modifier support (unique across all compilations, thread-safe) + private static final java.util.concurrent.atomic.AtomicInteger nextCallsiteId = new java.util.concurrent.atomic.AtomicInteger(1); // Track last result register for expression chaining int lastResultReg = -1; // Target output register for ALIAS elimination (same save/restore pattern as currentCallContext). @@ -2426,7 +2429,7 @@ void compileVariableDeclaration(OperatorNode node, String op) { boolean isDeclaredReference = node.annotations != null && Boolean.TRUE.equals(node.annotations.get("isDeclaredReference")); - Integer beginId = RuntimeCode.evalBeginIds.get(sigilOp); + Integer beginId = RuntimeCode.getEvalBeginIds().get(sigilOp); if (beginId != null) { // BEGIN-captured variable: use RETRIEVE_BEGIN_* (destructive removal from global storage) int persistId = beginId; @@ -2831,7 +2834,7 @@ void compileVariableDeclaration(OperatorNode node, String op) { continue; } - Integer beginId2 = RuntimeCode.evalBeginIds.get(sigilOp); + Integer beginId2 = RuntimeCode.getEvalBeginIds().get(sigilOp); if (beginId2 != null || op.equals("state")) { int persistId = beginId2 != null ? beginId2 : sigilOp.id; int reg = allocateRegister(); @@ -4461,7 +4464,7 @@ int allocateRegister() { * Each callsite with /o gets a unique ID so the pattern is compiled only once per callsite. */ int allocateCallsiteId() { - return nextCallsiteId++; + return nextCallsiteId.getAndIncrement(); } int allocateOutputRegister() { @@ -4875,7 +4878,7 @@ private void visitNamedSubroutine(SubroutineNode node) { int beginId = 0; if (!closureVarIndices.isEmpty()) { - beginId = EmitterMethodCreator.classCounter++; + beginId = EmitterMethodCreator.classCounter.getAndIncrement(); // Store each closure variable in PersistentVariable globals for (int i = 0; i < closureVarNames.size(); i++) { @@ -4999,6 +5002,10 @@ private void visitNamedSubroutine(SubroutineNode node) { *

* Compiles the subroutine body to bytecode with closure support. * Anonymous subs capture lexical variables from the enclosing scope. + *

+ * When compiled inside eval STRING with an EmitterContext available, + * attempts JVM compilation first for better runtime performance. + * Falls back to interpreter bytecode if JVM compilation fails. */ private void visitAnonymousSubroutine(SubroutineNode node) { // Step 1: Collect closure variables. @@ -5014,7 +5021,141 @@ private void visitAnonymousSubroutine(SubroutineNode node) { closureCapturedVarNames.addAll(closureVarNames); - // Step 3: Create a new BytecodeCompiler for the subroutine body + // Step 2: Try JVM compilation first if we have an EmitterContext (eval STRING path) + // Skip JVM attempt for defer blocks and map/grep blocks which have special control flow + Boolean isDeferBlock = (Boolean) node.getAnnotation("isDeferBlock"); + Boolean isMapGrepBlock = (Boolean) node.getAnnotation("isMapGrepBlock"); + boolean skipJvm = (isDeferBlock != null && isDeferBlock) + || (isMapGrepBlock != null && isMapGrepBlock); + + if (this.emitterContext != null && !skipJvm) { + try { + emitJvmAnonymousSub(node, closureVarNames, closureVarIndices); + return; // JVM compilation succeeded + } catch (Exception e) { + // JVM compilation failed, fall through to interpreter path + if (System.getenv("JPERL_SHOW_FALLBACK") != null) { + System.err.println("JVM compilation failed for anonymous sub in eval STRING, using interpreter: " + + e.getClass().getSimpleName() + ": " + e.getMessage()); + } + } + } + + // Step 3: Interpreter compilation (existing path) + emitInterpretedAnonymousSub(node, closureVarNames, closureVarIndices); + } + + /** + * Attempt to compile an anonymous sub body to JVM bytecode. + * Creates an EmitterContext, calls EmitterMethodCreator.createClassWithMethod(), + * and emits interpreter opcodes to instantiate the JVM class at runtime. + */ + private void emitJvmAnonymousSub(SubroutineNode node, + List closureVarNames, + List closureVarIndices) { + // Build a ScopedSymbolTable for the sub body with captured variables + ScopedSymbolTable newSymbolTable = new ScopedSymbolTable(); + newSymbolTable.enterScope(); + + // Add reserved variables first to occupy slots 0-2 + // EmitterMethodCreator skips these (skipVariables=3) but they must be present + // in the symbol table to keep captured variable indices aligned at 3+ + newSymbolTable.addVariable("this", "", getCurrentPackage(), null); + newSymbolTable.addVariable("@_", "", getCurrentPackage(), null); + newSymbolTable.addVariable("wantarray", "", getCurrentPackage(), null); + + // Add captured variables to the symbol table + // They will be at indices 3, 4, 5, ... (after this/@_/wantarray) + for (String varName : closureVarNames) { + newSymbolTable.addVariable(varName, "my", getCurrentPackage(), null); + } + + // Copy package and pragma flags from the current BytecodeCompiler state + newSymbolTable.setCurrentPackage(getCurrentPackage(), symbolTable.currentPackageIsClass()); + newSymbolTable.strictOptionsStack.pop(); + newSymbolTable.strictOptionsStack.push(symbolTable.strictOptionsStack.peek()); + newSymbolTable.featureFlagsStack.pop(); + newSymbolTable.featureFlagsStack.push(symbolTable.featureFlagsStack.peek()); + newSymbolTable.warningFlagsStack.pop(); + newSymbolTable.warningFlagsStack.push((java.util.BitSet) symbolTable.warningFlagsStack.peek().clone()); + newSymbolTable.warningFatalStack.pop(); + newSymbolTable.warningFatalStack.push((java.util.BitSet) symbolTable.warningFatalStack.peek().clone()); + newSymbolTable.warningDisabledStack.pop(); + newSymbolTable.warningDisabledStack.push((java.util.BitSet) symbolTable.warningDisabledStack.peek().clone()); + + // Reset variable index past the captured variables + String[] newEnv = newSymbolTable.getVariableNames(); + int currentVarIndex = newSymbolTable.getCurrentLocalVariableIndex(); + int resetTo = Math.max(newEnv.length, currentVarIndex); + newSymbolTable.resetLocalVariableIndex(resetTo); + + // Create EmitterContext for JVM compilation + JavaClassInfo newJavaClassInfo = new JavaClassInfo(); + EmitterContext subCtx = new EmitterContext( + newJavaClassInfo, + newSymbolTable, + null, // mv - will be set by EmitterMethodCreator + null, // cw - will be set by EmitterMethodCreator + RuntimeContextType.RUNTIME, + true, + this.errorUtil, + this.emitterContext.compilerOptions, + new RuntimeArray() + ); + + // Try JVM compilation - may throw InterpreterFallbackException or other exceptions + Class generatedClass = EmitterMethodCreator.createClassWithMethod( + subCtx, node.block, false); + + // Cache the generated class + RuntimeCode.getAnonSubs().put(subCtx.javaClassInfo.javaClassName, generatedClass); + + // Emit interpreter opcodes to create the code reference at runtime + int codeReg = allocateRegister(); + String packageName = getCurrentPackage(); + + if (closureVarIndices.isEmpty()) { + // No closures - instantiate JVM class at compile time + JvmClosureTemplate template = new JvmClosureTemplate( + generatedClass, node.prototype, packageName); + RuntimeScalar codeScalar = template.instantiateNoClosure(); + + // Handle attributes + if (node.attributes != null && !node.attributes.isEmpty() && packageName != null) { + RuntimeCode code = (RuntimeCode) codeScalar.value; + code.attributes = node.attributes; + Attributes.runtimeDispatchModifyCodeAttributes(packageName, codeScalar); + } + + int constIdx = addToConstantPool(codeScalar); + emit(Opcodes.LOAD_CONST); + emitReg(codeReg); + emit(constIdx); + } else { + // Has closures - store JvmClosureTemplate in constant pool + // CREATE_CLOSURE opcode handles both InterpretedCode and JvmClosureTemplate + JvmClosureTemplate template = new JvmClosureTemplate( + generatedClass, node.prototype, packageName); + int templateIdx = addToConstantPool(template); + emit(Opcodes.CREATE_CLOSURE); + emitReg(codeReg); + emit(templateIdx); + emit(closureVarIndices.size()); + for (int regIdx : closureVarIndices) { + emit(regIdx); + } + } + + lastResultReg = codeReg; + } + + /** + * Compile an anonymous sub to InterpretedCode (the fallback/default path). + * This is the original implementation of visitAnonymousSubroutine. + */ + private void emitInterpretedAnonymousSub(SubroutineNode node, + List closureVarNames, + List closureVarIndices) { // Build a variable registry from current scope to pass to sub-compiler // This allows nested closures to see grandparent scope variables Map parentRegistry = new HashMap<>(); diff --git a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java index a5d166f7e..52f002a9d 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java +++ b/src/main/java/org/perlonjava/backend/bytecode/BytecodeInterpreter.java @@ -549,13 +549,13 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } case Opcodes.STORE_GLOBAL_CODE -> { - // Store global code: GlobalVariable.globalCodeRefs.put(name, codeRef) + // Store global code: GlobalVariable.getGlobalCodeRefsMap().put(name, codeRef) int nameIdx = bytecode[pc++]; int codeReg = bytecode[pc++]; String name = code.stringPool[nameIdx]; RuntimeScalar codeRef = (RuntimeScalar) registers[codeReg]; // Store the code reference in the global namespace - GlobalVariable.globalCodeRefs.put(name, codeRef); + GlobalVariable.getGlobalCodeRefsMap().put(name, codeRef); } case Opcodes.CREATE_CLOSURE -> { @@ -1061,7 +1061,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } // Jump to eval catch handler pc = evalCatchStack.pop(); - RuntimeCode.evalDepth--; + RuntimeCode.decrementEvalDepth(); break; } return result; @@ -1175,7 +1175,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c DynamicVariableManager.popToLocalLevel(savedLevel); } pc = evalCatchStack.pop(); - RuntimeCode.evalDepth--; + RuntimeCode.decrementEvalDepth(); break; } return result; @@ -1553,7 +1553,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c evalLocalLevelStack.push(DynamicVariableManager.getLocalLevel()); // Track eval depth for $^S - RuntimeCode.evalDepth++; + RuntimeCode.incrementEvalDepth(); // Clear $@ at start of eval block GlobalVariable.setGlobalVariable("main::@", ""); @@ -1579,7 +1579,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } // Track eval depth for $^S - RuntimeCode.evalDepth--; + RuntimeCode.decrementEvalDepth(); } case Opcodes.EVAL_CATCH -> { @@ -2104,7 +2104,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c int savedLevel = evalLocalLevelStack.pop(); DynamicVariableManager.popToLocalLevel(savedLevel); } - RuntimeCode.evalDepth--; + RuntimeCode.decrementEvalDepth(); WarnDie.catchEval(e); pc = catchPc; continue outer; @@ -2147,7 +2147,7 @@ public static RuntimeList execute(InterpretedCode code, RuntimeArray args, int c } // Track eval depth for $^S - RuntimeCode.evalDepth--; + RuntimeCode.decrementEvalDepth(); // Call WarnDie.catchEval() to set $@ WarnDie.catchEval(e); diff --git a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java index 1153cf356..b9a98db35 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java +++ b/src/main/java/org/perlonjava/backend/bytecode/CompileAssignment.java @@ -326,7 +326,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, if (sigilOp.operator.equals("$") && sigilOp.operand instanceof IdentifierNode) { String varName = "$" + ((IdentifierNode) sigilOp.operand).name; - Integer beginIdObj = RuntimeCode.evalBeginIds.get(sigilOp); + Integer beginIdObj = RuntimeCode.getEvalBeginIds().get(sigilOp); if (beginIdObj != null) { int beginId = beginIdObj; int nameIdx = bytecodeCompiler.addToStringPool(varName); @@ -415,7 +415,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, // Handle my @array = ... String varName = "@" + ((IdentifierNode) sigilOp.operand).name; - Integer beginIdArr = RuntimeCode.evalBeginIds.get(sigilOp); + Integer beginIdArr = RuntimeCode.getEvalBeginIds().get(sigilOp); if (beginIdArr != null) { int beginId = beginIdArr; int nameIdx = bytecodeCompiler.addToStringPool(varName); @@ -479,7 +479,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, // Handle my %hash = ... String varName = "%" + ((IdentifierNode) sigilOp.operand).name; - Integer beginIdHash = RuntimeCode.evalBeginIds.get(sigilOp); + Integer beginIdHash = RuntimeCode.getEvalBeginIds().get(sigilOp); if (beginIdHash != null) { int beginId = beginIdHash; int nameIdx = bytecodeCompiler.addToStringPool(varName); @@ -575,7 +575,7 @@ public static void compileAssignmentOperator(BytecodeCompiler bytecodeCompiler, String varName = sigil + ((IdentifierNode) sigilOp.operand).name; int varReg; - Integer beginIdList = RuntimeCode.evalBeginIds.get(sigilOp); + Integer beginIdList = RuntimeCode.getEvalBeginIds().get(sigilOp); if (beginIdList != null) { int beginId = beginIdList; int nameIdx = bytecodeCompiler.addToStringPool(varName); diff --git a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java index 0172c88b7..e9064cbe7 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java +++ b/src/main/java/org/perlonjava/backend/bytecode/EvalStringHandler.java @@ -1,6 +1,7 @@ package org.perlonjava.backend.bytecode; import org.perlonjava.app.cli.CompilerOptions; +import org.perlonjava.app.scriptengine.PerlLanguageProvider; import org.perlonjava.backend.jvm.EmitterContext; import org.perlonjava.backend.jvm.JavaClassInfo; import org.perlonjava.frontend.astnode.Node; @@ -109,174 +110,182 @@ public static RuntimeList evalStringList(String perlCode, // Step 1: Clear $@ at start of eval GlobalVariable.getGlobalVariable("main::@").set(""); - // Step 2: Parse the string to AST - Lexer lexer = new Lexer(perlCode); - List tokens = lexer.tokenize(); - - // Create minimal EmitterContext for parsing - // IMPORTANT: Inherit strict/feature/warning flags from parent scope - // This matches Perl's eval STRING semantics where eval inherits lexical pragmas - // Generate a unique eval filename so ByteCodeSourceMapper entries from - // different evals don't collide (each eval's token indices start from 0, - // so sharing a single filename would mix package-at-location data). - String evalFileName = RuntimeCode.getNextEvalFilename(); - - CompilerOptions opts = new CompilerOptions(); - opts.fileName = evalFileName; - ScopedSymbolTable symbolTable = new ScopedSymbolTable(); - - // Add standard variables that are always available in eval context. - // This matches PerlLanguageProvider and evalStringWithInterpreter which - // ensure @_ is visible in the symbol table. Without this, named subs - // parsed inside this eval (e.g., eval q{sub foo { shift }}) would get - // an empty filteredSnapshot and fail strict vars checks for @_. - symbolTable.enterScope(); - symbolTable.addVariable("this", "", null); - symbolTable.addVariable("@_", "our", null); - symbolTable.addVariable("wantarray", "", null); - - // Inherit lexical pragma flags from parent if available - if (currentCode != null) { - int strictOpts = (siteStrictOptions >= 0) ? siteStrictOptions : currentCode.strictOptions; - int featFlags = (siteFeatureFlags >= 0) ? siteFeatureFlags : currentCode.featureFlags; - symbolTable.strictOptionsStack.pop(); - symbolTable.strictOptionsStack.push(strictOpts); - symbolTable.featureFlagsStack.pop(); - symbolTable.featureFlagsStack.push(featFlags); - symbolTable.warningFlagsStack.pop(); - symbolTable.warningFlagsStack.push((java.util.BitSet) currentCode.warningFlags.clone()); - } + // Steps 2-4: Parse and compile under the global compile lock. + // The parser and emitter have shared mutable static state that is not thread-safe. + InterpretedCode evalCode; + PerlLanguageProvider.COMPILE_LOCK.lock(); + try { + // Step 2: Parse the string to AST + Lexer lexer = new Lexer(perlCode); + List tokens = lexer.tokenize(); + + // Create minimal EmitterContext for parsing + // IMPORTANT: Inherit strict/feature/warning flags from parent scope + // This matches Perl's eval STRING semantics where eval inherits lexical pragmas + // Generate a unique eval filename so ByteCodeSourceMapper entries from + // different evals don't collide (each eval's token indices start from 0, + // so sharing a single filename would mix package-at-location data). + String evalFileName = RuntimeCode.getNextEvalFilename(); + + CompilerOptions opts = new CompilerOptions(); + opts.fileName = evalFileName; + ScopedSymbolTable symbolTable = new ScopedSymbolTable(); + + // Add standard variables that are always available in eval context. + // This matches PerlLanguageProvider and evalStringWithInterpreter which + // ensure @_ is visible in the symbol table. Without this, named subs + // parsed inside this eval (e.g., eval q{sub foo { shift }}) would get + // an empty filteredSnapshot and fail strict vars checks for @_. + symbolTable.enterScope(); + symbolTable.addVariable("this", "", null); + symbolTable.addVariable("@_", "our", null); + symbolTable.addVariable("wantarray", "", null); + + // Inherit lexical pragma flags from parent if available + if (currentCode != null) { + int strictOpts = (siteStrictOptions >= 0) ? siteStrictOptions : currentCode.strictOptions; + int featFlags = (siteFeatureFlags >= 0) ? siteFeatureFlags : currentCode.featureFlags; + symbolTable.strictOptionsStack.pop(); + symbolTable.strictOptionsStack.push(strictOpts); + symbolTable.featureFlagsStack.pop(); + symbolTable.featureFlagsStack.push(featFlags); + symbolTable.warningFlagsStack.pop(); + symbolTable.warningFlagsStack.push((java.util.BitSet) currentCode.warningFlags.clone()); + } - // Use runtime package (maintained by PUSH_PACKAGE/SET_PACKAGE opcodes). - // This correctly reflects the current package scope when eval STRING runs - // inside dynamic package blocks like: package Foo { eval("__PACKAGE__") } - // For INIT/END blocks, the runtime package is set by the block's own - // PUSH_PACKAGE opcode before execution begins. - String compilePackage = InterpreterState.currentPackage.get().toString(); - symbolTable.setCurrentPackage(compilePackage, false); - - evalTrace("EvalStringHandler compilePackage=" + compilePackage + " fileName=" + opts.fileName); - - ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); - EmitterContext ctx = new EmitterContext( - new JavaClassInfo(), - symbolTable, - null, // mv - null, // cw - callContext, - false, // isBoxed - errorUtil, - opts, - null // unitcheckBlocks - ); - - Parser parser = new Parser(ctx, tokens); - Node ast = parser.parse(); - - // Step 3: Build captured variables and adjusted registry for eval context - // Collect all parent scope variables (except reserved registers 0-2) - RuntimeBase[] capturedVars = new RuntimeBase[0]; - Map adjustedRegistry = null; - - // Use per-eval-site registry if available, otherwise fall back to global registry - Map registry = siteRegistry != null ? siteRegistry - : (currentCode != null ? currentCode.variableRegistry : null); - - if (registry != null && registers != null) { - - List> sortedVars = new ArrayList<>( - registry.entrySet() + // Use runtime package (maintained by PUSH_PACKAGE/SET_PACKAGE opcodes). + // This correctly reflects the current package scope when eval STRING runs + // inside dynamic package blocks like: package Foo { eval("__PACKAGE__") } + // For INIT/END blocks, the runtime package is set by the block's own + // PUSH_PACKAGE opcode before execution begins. + String compilePackage = InterpreterState.currentPackage.get().toString(); + symbolTable.setCurrentPackage(compilePackage, false); + + evalTrace("EvalStringHandler compilePackage=" + compilePackage + " fileName=" + opts.fileName); + + ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); + EmitterContext ctx = new EmitterContext( + new JavaClassInfo(), + symbolTable, + null, // mv + null, // cw + callContext, + false, // isBoxed + errorUtil, + opts, + null // unitcheckBlocks ); - sortedVars.sort(Map.Entry.comparingByValue()); - - // Build capturedVars array and adjusted registry - // Captured variables will be placed at registers 3+ in eval'd code - List capturedList = new ArrayList<>(); - adjustedRegistry = new HashMap<>(); - - // Always include reserved registers in adjusted registry - adjustedRegistry.put("this", 0); - adjustedRegistry.put("@_", 1); - adjustedRegistry.put("wantarray", 2); - - int captureIndex = 0; - for (Map.Entry entry : sortedVars) { - String varName = entry.getKey(); - int parentRegIndex = entry.getValue(); - - // Skip reserved registers (they're handled separately in interpreter) - if (parentRegIndex < 3) { - continue; - } - if (parentRegIndex < registers.length) { - RuntimeBase value = registers[parentRegIndex]; - - // Skip non-Perl values (like Iterator objects from for loops) - // Only capture actual Perl variables: Scalar, Array, Hash, Code - if (value == null) { - // Null is fine - capture it - } else if (value instanceof RuntimeScalar scalar) { - // Check if the scalar contains an Iterator (used by for loops) - if (scalar.value instanceof java.util.Iterator) { - // Skip - this is a for loop iterator, not a user variable - continue; - } - } else if (!(value instanceof RuntimeArray || - value instanceof RuntimeHash || - value instanceof RuntimeCode)) { - // Skip this register - it contains an internal object + Parser parser = new Parser(ctx, tokens); + Node ast = parser.parse(); + + // Step 3: Build captured variables and adjusted registry for eval context + // Collect all parent scope variables (except reserved registers 0-2) + RuntimeBase[] capturedVars = new RuntimeBase[0]; + Map adjustedRegistry = null; + + // Use per-eval-site registry if available, otherwise fall back to global registry + Map registry = siteRegistry != null ? siteRegistry + : (currentCode != null ? currentCode.variableRegistry : null); + + if (registry != null && registers != null) { + + List> sortedVars = new ArrayList<>( + registry.entrySet() + ); + sortedVars.sort(Map.Entry.comparingByValue()); + + // Build capturedVars array and adjusted registry + // Captured variables will be placed at registers 3+ in eval'd code + List capturedList = new ArrayList<>(); + adjustedRegistry = new HashMap<>(); + + // Always include reserved registers in adjusted registry + adjustedRegistry.put("this", 0); + adjustedRegistry.put("@_", 1); + adjustedRegistry.put("wantarray", 2); + + int captureIndex = 0; + for (Map.Entry entry : sortedVars) { + String varName = entry.getKey(); + int parentRegIndex = entry.getValue(); + + // Skip reserved registers (they're handled separately in interpreter) + if (parentRegIndex < 3) { continue; } - capturedList.add(value); - // Map to new register index starting at 3 - adjustedRegistry.put(varName, 3 + captureIndex); - captureIndex++; + if (parentRegIndex < registers.length) { + RuntimeBase value = registers[parentRegIndex]; + + // Skip non-Perl values (like Iterator objects from for loops) + // Only capture actual Perl variables: Scalar, Array, Hash, Code + if (value == null) { + // Null is fine - capture it + } else if (value instanceof RuntimeScalar scalar) { + // Check if the scalar contains an Iterator (used by for loops) + if (scalar.value instanceof java.util.Iterator) { + // Skip - this is a for loop iterator, not a user variable + continue; + } + } else if (!(value instanceof RuntimeArray || + value instanceof RuntimeHash || + value instanceof RuntimeCode)) { + // Skip this register - it contains an internal object + continue; + } + + capturedList.add(value); + // Map to new register index starting at 3 + adjustedRegistry.put(varName, 3 + captureIndex); + captureIndex++; + } } - } - capturedVars = capturedList.toArray(new RuntimeBase[0]); - if (EVAL_TRACE) { - evalTrace("EvalStringHandler varRegistry keys=" + registry.keySet()); - evalTrace("EvalStringHandler adjustedRegistry=" + adjustedRegistry); - for (int ci = 0; ci < capturedVars.length; ci++) { - evalTrace("EvalStringHandler captured[" + ci + "]=" + (capturedVars[ci] != null ? capturedVars[ci].getClass().getSimpleName() + ":" + capturedVars[ci] : "null")); + capturedVars = capturedList.toArray(new RuntimeBase[0]); + if (EVAL_TRACE) { + evalTrace("EvalStringHandler varRegistry keys=" + registry.keySet()); + evalTrace("EvalStringHandler adjustedRegistry=" + adjustedRegistry); + for (int ci = 0; ci < capturedVars.length; ci++) { + evalTrace("EvalStringHandler captured[" + ci + "]=" + (capturedVars[ci] != null ? capturedVars[ci].getClass().getSimpleName() + ":" + capturedVars[ci] : "null")); + } } } - } - // Step 4: Compile AST to interpreter bytecode with adjusted variable registry. - // The compile-time package is already propagated via ctx.symbolTable. - BytecodeCompiler compiler = new BytecodeCompiler( - evalFileName, - sourceLine, - errorUtil, - adjustedRegistry // Pass adjusted registry for variable capture - ); - InterpretedCode evalCode = compiler.compile(ast, ctx); // Pass ctx for context propagation - - evalTrace("EvalStringHandler compiled bytecodeLen=" + (evalCode != null ? evalCode.bytecode.length : -1) + - " src=" + (evalCode != null ? evalCode.sourceName : "null")); - if (RuntimeCode.DISASSEMBLE) { - System.out.println(Disassemble.disassemble(evalCode)); - } + // Step 4: Compile AST to interpreter bytecode with adjusted variable registry. + // The compile-time package is already propagated via ctx.symbolTable. + BytecodeCompiler compiler = new BytecodeCompiler( + evalFileName, + sourceLine, + errorUtil, + adjustedRegistry // Pass adjusted registry for variable capture + ); + evalCode = compiler.compile(ast, ctx); // Pass ctx for context propagation - // Step 4.5: Store source lines in debugger symbol table if $^P flags are set - int debugFlags = GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("P")).getInt(); - if (debugFlags != 0) { - String evalFilename = RuntimeCode.getNextEvalFilename(); - RuntimeCode.storeSourceLines(perlCode, evalFilename, ast, tokens); - } + evalTrace("EvalStringHandler compiled bytecodeLen=" + (evalCode != null ? evalCode.bytecode.length : -1) + + " src=" + (evalCode != null ? evalCode.sourceName : "null")); + if (RuntimeCode.DISASSEMBLE) { + System.out.println(Disassemble.disassemble(evalCode)); + } - // Step 5: Attach captured variables to eval'd code - if (capturedVars.length > 0) { - evalCode = evalCode.withCapturedVars(capturedVars); - } else if (currentCode != null && currentCode.capturedVars != null) { - // Fallback: share captured variables from parent scope (nested evals) - evalCode = evalCode.withCapturedVars(currentCode.capturedVars); + // Step 4.5: Store source lines in debugger symbol table if $^P flags are set + int debugFlags = GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("P")).getInt(); + if (debugFlags != 0) { + String evalFilename = RuntimeCode.getNextEvalFilename(); + RuntimeCode.storeSourceLines(perlCode, evalFilename, ast, tokens); + } + + // Step 5: Attach captured variables to eval'd code + if (capturedVars.length > 0) { + evalCode = evalCode.withCapturedVars(capturedVars); + } else if (currentCode != null && currentCode.capturedVars != null) { + // Fallback: share captured variables from parent scope (nested evals) + evalCode = evalCode.withCapturedVars(currentCode.capturedVars); + } + } finally { + PerlLanguageProvider.COMPILE_LOCK.unlock(); } - // Step 6: Execute the compiled code. + // Step 6: Execute the compiled code (outside the lock — execution is thread-safe). // IMPORTANT: Scope InterpreterState.currentPackage around eval execution. // currentPackage is a runtime-only field used by caller() — it does NOT // affect name resolution (which is fully compile-time). However, if the @@ -290,11 +299,11 @@ public static RuntimeList evalStringList(String perlCode, InterpreterState.currentPackage.get().set(savedPkg); RuntimeArray args = new RuntimeArray(); // Empty @_ RuntimeList result; - RuntimeCode.evalDepth++; + RuntimeCode.incrementEvalDepth(); try { result = evalCode.apply(args, callContext); } finally { - RuntimeCode.evalDepth--; + RuntimeCode.decrementEvalDepth(); DynamicVariableManager.popToLocalLevel(pkgLevel); } evalTrace("EvalStringHandler exec ok ctx=" + callContext + @@ -328,63 +337,71 @@ public static RuntimeScalar evalString(String perlCode, // Clear $@ at start GlobalVariable.getGlobalVariable("main::@").set(""); - // Parse the string - Lexer lexer = new Lexer(perlCode); - List tokens = lexer.tokenize(); - - // Generate a unique eval filename (see comment in evalStringList above) - String evalFileName = RuntimeCode.getNextEvalFilename(); - - CompilerOptions opts = new CompilerOptions(); - opts.fileName = evalFileName; - ScopedSymbolTable symbolTable = new ScopedSymbolTable(); - - // Add standard variables that are always available in eval context. - // Without this, subs parsed inside the eval would fail strict vars - // checks for @_ (same setup as the evalStringList overload). - symbolTable.enterScope(); - symbolTable.addVariable("this", "", null); - symbolTable.addVariable("@_", "our", null); - symbolTable.addVariable("wantarray", "", null); - - ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); - EmitterContext ctx = new EmitterContext( - new JavaClassInfo(), - symbolTable, - null, null, - RuntimeContextType.SCALAR, - false, - errorUtil, - opts, - null - ); - - Parser parser = new Parser(ctx, tokens); - Node ast = parser.parse(); - - // Compile to bytecode. - // IMPORTANT: Do NOT call compiler.setCompilePackage() here — same reason as the - // first evalString overload above: it corrupts die/warn location baking. - BytecodeCompiler compiler = new BytecodeCompiler( - evalFileName, - sourceLine, - errorUtil - ); - InterpretedCode evalCode = compiler.compile(ast, ctx); // Pass ctx for context propagation - if (RuntimeCode.DISASSEMBLE) { - System.out.println(Disassemble.disassemble(evalCode)); - } + // Parse and compile under the global compile lock + InterpretedCode evalCode; + PerlLanguageProvider.COMPILE_LOCK.lock(); + try { + // Parse the string + Lexer lexer = new Lexer(perlCode); + List tokens = lexer.tokenize(); + + // Generate a unique eval filename (see comment in evalStringList above) + String evalFileName = RuntimeCode.getNextEvalFilename(); + + CompilerOptions opts = new CompilerOptions(); + opts.fileName = evalFileName; + ScopedSymbolTable symbolTable = new ScopedSymbolTable(); + + // Add standard variables that are always available in eval context. + // Without this, subs parsed inside the eval would fail strict vars + // checks for @_ (same setup as the evalStringList overload). + symbolTable.enterScope(); + symbolTable.addVariable("this", "", null); + symbolTable.addVariable("@_", "our", null); + symbolTable.addVariable("wantarray", "", null); + + ErrorMessageUtil errorUtil = new ErrorMessageUtil(sourceName, tokens); + EmitterContext ctx = new EmitterContext( + new JavaClassInfo(), + symbolTable, + null, null, + RuntimeContextType.SCALAR, + false, + errorUtil, + opts, + null + ); - // Store source lines in debugger symbol table if $^P flags are set - int debugFlags = GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("P")).getInt(); - if (debugFlags != 0) { - String evalFilename = RuntimeCode.getNextEvalFilename(); - RuntimeCode.storeSourceLines(perlCode, evalFilename, ast, tokens); - } + Parser parser = new Parser(ctx, tokens); + Node ast = parser.parse(); + + // Compile to bytecode. + // IMPORTANT: Do NOT call compiler.setCompilePackage() here — same reason as the + // first evalString overload above: it corrupts die/warn location baking. + BytecodeCompiler compiler = new BytecodeCompiler( + evalFileName, + sourceLine, + errorUtil + ); + evalCode = compiler.compile(ast, ctx); // Pass ctx for context propagation + if (RuntimeCode.DISASSEMBLE) { + System.out.println(Disassemble.disassemble(evalCode)); + } - // Attach captured variables - evalCode = evalCode.withCapturedVars(capturedVars); + // Store source lines in debugger symbol table if $^P flags are set + int debugFlags = GlobalVariable.getGlobalVariable(GlobalContext.encodeSpecialVar("P")).getInt(); + if (debugFlags != 0) { + String evalFilename = RuntimeCode.getNextEvalFilename(); + RuntimeCode.storeSourceLines(perlCode, evalFilename, ast, tokens); + } + + // Attach captured variables + evalCode = evalCode.withCapturedVars(capturedVars); + } finally { + PerlLanguageProvider.COMPILE_LOCK.unlock(); + } + // Execute outside the lock — execution is thread-safe // Scope currentPackage around eval — see Step 6 comment in evalStringHelper above. int pkgLevel = DynamicVariableManager.getLocalLevel(); String savedPkg = InterpreterState.currentPackage.get().toString(); @@ -392,11 +409,11 @@ public static RuntimeScalar evalString(String perlCode, InterpreterState.currentPackage.get().set(savedPkg); RuntimeArray args = new RuntimeArray(); RuntimeList result; - RuntimeCode.evalDepth++; + RuntimeCode.incrementEvalDepth(); try { result = evalCode.apply(args, RuntimeContextType.SCALAR); } finally { - RuntimeCode.evalDepth--; + RuntimeCode.decrementEvalDepth(); DynamicVariableManager.popToLocalLevel(pkgLevel); } diff --git a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java index c218df163..05463683e 100644 --- a/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java +++ b/src/main/java/org/perlonjava/backend/bytecode/OpcodeHandlerExtended.java @@ -1,5 +1,6 @@ package org.perlonjava.backend.bytecode; +import org.perlonjava.backend.jvm.JvmClosureTemplate; import org.perlonjava.runtime.operators.*; import org.perlonjava.runtime.perlmodule.Attributes; import org.perlonjava.runtime.regex.RuntimeRegex; @@ -888,15 +889,15 @@ public static int executeMatchRegexNot(int[] bytecode, int pc, RuntimeBase[] reg /** * Execute create closure operation. * Format: CREATE_CLOSURE rd template_idx num_captures reg1 reg2 ... + *

+ * Supports both InterpretedCode templates (interpreter-compiled subs) + * and JvmClosureTemplate (JVM-compiled subs from eval STRING). */ public static int executeCreateClosure(int[] bytecode, int pc, RuntimeBase[] registers, InterpretedCode code) { int rd = bytecode[pc++]; int templateIdx = bytecode[pc++]; int numCaptures = bytecode[pc++]; - // Get the template InterpretedCode from constants - InterpretedCode template = (InterpretedCode) code.constants[templateIdx]; - // Capture the current register values RuntimeBase[] capturedVars = new RuntimeBase[numCaptures]; for (int i = 0; i < numCaptures; i++) { @@ -904,35 +905,43 @@ public static int executeCreateClosure(int[] bytecode, int pc, RuntimeBase[] reg capturedVars[i] = registers[captureReg]; } - // Create a new InterpretedCode with the captured variables - InterpretedCode closureCode = template.withCapturedVars(capturedVars); - - // Track captureCount on captured RuntimeScalar variables. - // This mirrors what RuntimeCode.makeCodeObject() does for JVM-compiled closures. - // Without this, scopeExitCleanup() doesn't know the variable is still alive - // via this closure, and may prematurely clear weak references to its value. - java.util.List capturedScalars = new java.util.ArrayList<>(); - for (RuntimeBase captured : capturedVars) { - if (captured instanceof RuntimeScalar s) { - capturedScalars.add(s); - s.captureCount++; + Object template = code.constants[templateIdx]; + + if (template instanceof JvmClosureTemplate jvmTemplate) { + // JVM-compiled closure: instantiate the generated class with captured variables + registers[rd] = jvmTemplate.instantiate(capturedVars); + } else { + // InterpretedCode closure: create a new copy with captured variables + InterpretedCode interpTemplate = (InterpretedCode) template; + InterpretedCode closureCode = interpTemplate.withCapturedVars(capturedVars); + + // Track captureCount on captured RuntimeScalar variables. + // This mirrors what RuntimeCode.makeCodeObject() does for JVM-compiled closures. + // Without this, scopeExitCleanup() doesn't know the variable is still alive + // via this closure, and may prematurely clear weak references to its value. + java.util.List capturedScalars = new java.util.ArrayList<>(); + for (RuntimeBase captured : capturedVars) { + if (captured instanceof RuntimeScalar s) { + capturedScalars.add(s); + s.captureCount++; + } + } + if (!capturedScalars.isEmpty()) { + closureCode.capturedScalars = capturedScalars.toArray(new RuntimeScalar[0]); + closureCode.refCount = 0; } - } - if (!capturedScalars.isEmpty()) { - closureCode.capturedScalars = capturedScalars.toArray(new RuntimeScalar[0]); - closureCode.refCount = 0; - } - // Wrap in RuntimeScalar and set __SUB__ for self-reference - RuntimeScalar codeRef = new RuntimeScalar(closureCode); - closureCode.__SUB__ = codeRef; - registers[rd] = codeRef; + // Wrap in RuntimeScalar and set __SUB__ for self-reference + RuntimeScalar codeRef = new RuntimeScalar(closureCode); + closureCode.__SUB__ = codeRef; + registers[rd] = codeRef; - // Dispatch MODIFY_CODE_ATTRIBUTES for anonymous subs with non-builtin attributes - // Pass isClosure=true since CREATE_CLOSURE always creates a closure - if (closureCode.attributes != null && !closureCode.attributes.isEmpty() - && closureCode.packageName != null) { - Attributes.runtimeDispatchModifyCodeAttributes(closureCode.packageName, codeRef, true); + // Dispatch MODIFY_CODE_ATTRIBUTES for anonymous subs with non-builtin attributes + // Pass isClosure=true since CREATE_CLOSURE always creates a closure + if (closureCode.attributes != null && !closureCode.attributes.isEmpty() + && closureCode.packageName != null) { + Attributes.runtimeDispatchModifyCodeAttributes(closureCode.packageName, codeRef, true); + } } return pc; } diff --git a/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java b/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java index 8ecdcc620..d28451abc 100644 --- a/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java +++ b/src/main/java/org/perlonjava/backend/jvm/ByteCodeSourceMapper.java @@ -4,6 +4,7 @@ import java.util.ArrayList; import java.util.HashMap; +import java.util.List; import java.util.Map; import java.util.TreeMap; @@ -13,32 +14,45 @@ * resolution for stack traces at runtime. */ public class ByteCodeSourceMapper { - // Maps source files to their debug information - private static final Map sourceFiles = new HashMap<>(); - // Pool of package names to optimize memory usage - private static final ArrayList packageNamePool = new ArrayList<>(); - private static final Map packageNameToId = new HashMap<>(); - - // Pool of file names to optimize memory usage - private static final ArrayList fileNamePool = new ArrayList<>(); - private static final Map fileNameToId = new HashMap<>(); + /** + * Holds all mutable source-mapper state. One instance lives in each PerlRuntime + * for multiplicity thread-safety. + */ + public static class State { + // Maps source files to their debug information + public final Map sourceFiles = new HashMap<>(); + + // Pool of package names to optimize memory usage + public final List packageNamePool = new ArrayList<>(); + public final Map packageNameToId = new HashMap<>(); + + // Pool of file names to optimize memory usage + public final List fileNamePool = new ArrayList<>(); + public final Map fileNameToId = new HashMap<>(); + + // Pool of subroutine names to optimize memory usage + public final List subroutineNamePool = new ArrayList<>(); + public final Map subroutineNameToId = new HashMap<>(); + + public void resetAll() { + sourceFiles.clear(); + packageNamePool.clear(); + packageNameToId.clear(); + fileNamePool.clear(); + fileNameToId.clear(); + subroutineNamePool.clear(); + subroutineNameToId.clear(); + } + } - // Pool of subroutine names to optimize memory usage - private static final ArrayList subroutineNamePool = new ArrayList<>(); - private static final Map subroutineNameToId = new HashMap<>(); + /** Returns the current PerlRuntime's source-mapper state. */ + private static State state() { + return org.perlonjava.runtime.runtimetypes.PerlRuntime.current().sourceMapperState; + } public static void resetAll() { - sourceFiles.clear(); - - packageNamePool.clear(); - packageNameToId.clear(); - - fileNamePool.clear(); - fileNameToId.clear(); - - subroutineNamePool.clear(); - subroutineNameToId.clear(); + state().resetAll(); } /** @@ -48,9 +62,10 @@ public static void resetAll() { * @return The unique identifier for the package */ private static int getOrCreatePackageId(String packageName) { - return packageNameToId.computeIfAbsent(packageName, name -> { - packageNamePool.add(name); - return packageNamePool.size() - 1; + State s = state(); + return s.packageNameToId.computeIfAbsent(packageName, name -> { + s.packageNamePool.add(name); + return s.packageNamePool.size() - 1; }); } @@ -61,9 +76,10 @@ private static int getOrCreatePackageId(String packageName) { * @return The unique identifier for the file */ private static int getOrCreateFileId(String fileName) { - return fileNameToId.computeIfAbsent(fileName, name -> { - fileNamePool.add(name); - return fileNamePool.size() - 1; + State s = state(); + return s.fileNameToId.computeIfAbsent(fileName, name -> { + s.fileNamePool.add(name); + return s.fileNamePool.size() - 1; }); } @@ -74,9 +90,10 @@ private static int getOrCreateFileId(String fileName) { * @return The unique identifier for the subroutine name */ private static int getOrCreateSubroutineId(String subroutineName) { - return subroutineNameToId.computeIfAbsent(subroutineName, name -> { - subroutineNamePool.add(name); - return subroutineNamePool.size() - 1; + State s = state(); + return s.subroutineNameToId.computeIfAbsent(subroutineName, name -> { + s.subroutineNamePool.add(name); + return s.subroutineNamePool.size() - 1; }); } @@ -87,7 +104,7 @@ private static int getOrCreateSubroutineId(String subroutineName) { */ static void setDebugInfoFileName(EmitterContext ctx) { int fileId = getOrCreateFileId(ctx.compilerOptions.fileName); - sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new); + state().sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new); ctx.cw.visitSource(ctx.compilerOptions.fileName, null); } @@ -126,13 +143,14 @@ static void setDebugInfoLineNumber(EmitterContext ctx, int tokenIndex) { * @param tokenIndex The index of the token in the source code */ public static void saveSourceLocation(EmitterContext ctx, int tokenIndex) { + State s = state(); // Use the ORIGINAL filename (compile-time) for the key, not the #line-adjusted one. // This is because JVM stack traces report the original filename from visitSource(). // The #line-adjusted filename is stored separately in LineInfo for caller() reporting. int fileId = getOrCreateFileId(ctx.compilerOptions.fileName); // Get or create the SourceFileInfo object for the file - SourceFileInfo info = sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new); + SourceFileInfo info = s.sourceFiles.computeIfAbsent(fileId, SourceFileInfo::new); // Get current subroutine name (empty string for main code) String subroutineName = ctx.symbolTable.getCurrentSubroutine(); @@ -153,6 +171,12 @@ public static void saveSourceLocation(EmitterContext ctx, int tokenIndex) { LineInfo existingEntry = info.tokenToLineInfo.get(tokenIndex); if (existingEntry != null) { // Entry already exists from parse-time - preserve it entirely + if (System.getenv("DEBUG_CALLER") != null) { + System.err.println("DEBUG saveSourceLocation: SKIP (exists) file=" + ctx.compilerOptions.fileName + + " tokenIndex=" + tokenIndex + " existingLine=" + existingEntry.lineNumber() + + " existingPkg=" + s.packageNamePool.get(existingEntry.packageNameId()) + + " existingSourceFile=" + s.fileNamePool.get(existingEntry.sourceFileNameId())); + } return; } @@ -174,7 +198,7 @@ public static void saveSourceLocation(EmitterContext ctx, int tokenIndex) { // Look for nearby entry (within 50 tokens) that has #line-adjusted filename var nearbyEntry = info.tokenToLineInfo.floorEntry(tokenIndex); if (nearbyEntry != null && (tokenIndex - nearbyEntry.getKey()) < 50) { - String nearbySourceFile = fileNamePool.get(nearbyEntry.getValue().sourceFileNameId()); + String nearbySourceFile = s.fileNamePool.get(nearbyEntry.getValue().sourceFileNameId()); if (!nearbySourceFile.equals(ctx.compilerOptions.fileName)) { // Nearby entry has #line-adjusted filename - inherit it sourceFileName = nearbySourceFile; @@ -208,12 +232,13 @@ public static void saveSourceLocation(EmitterContext ctx, int tokenIndex) { * @return The package name at that location, or null if not found */ public static String getPackageAtLocation(String fileName, int tokenIndex) { - int fileId = fileNameToId.getOrDefault(fileName, -1); + State s = state(); + int fileId = s.fileNameToId.getOrDefault(fileName, -1); if (fileId == -1) { return null; } - SourceFileInfo info = sourceFiles.get(fileId); + SourceFileInfo info = s.sourceFiles.get(fileId); if (info == null) { return null; } @@ -223,7 +248,11 @@ public static String getPackageAtLocation(String fileName, int tokenIndex) { return null; } - String pkg = packageNamePool.get(entry.getValue().packageNameId()); + String pkg = s.packageNamePool.get(entry.getValue().packageNameId()); + if (System.getenv("DEBUG_CALLER") != null) { + System.err.println("DEBUG getPackageAtLocation: fileName=" + fileName + " tokenIndex=" + tokenIndex + + " foundTokenIndex=" + entry.getKey() + " pkg=" + pkg); + } return pkg; } @@ -234,10 +263,11 @@ public static String getPackageAtLocation(String fileName, int tokenIndex) { * @return The corresponding source code location */ public static SourceLocation parseStackTraceElement(StackTraceElement element, HashMap locationToClassName) { - int fileId = fileNameToId.getOrDefault(element.getFileName(), -1); + State s = state(); + int fileId = s.fileNameToId.getOrDefault(element.getFileName(), -1); int tokenIndex = element.getLineNumber(); - SourceFileInfo info = sourceFiles.get(fileId); + SourceFileInfo info = s.sourceFiles.get(fileId); if (info == null) { return new SourceLocation(element.getFileName(), "", tokenIndex, null); } @@ -251,9 +281,9 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H LineInfo lineInfo = entry.getValue(); // Get the #line directive-adjusted source filename for caller() reporting - String sourceFileName = fileNamePool.get(lineInfo.sourceFileNameId()); + String sourceFileName = s.fileNamePool.get(lineInfo.sourceFileNameId()); int lineNumber = lineInfo.lineNumber(); - String packageName = packageNamePool.get(lineInfo.packageNameId()); + String packageName = s.packageNamePool.get(lineInfo.packageNameId()); // FIX: If the found entry's sourceFile equals the original file (no #line applied), // check for nearby entries that have a #line-adjusted filename. @@ -266,7 +296,10 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H boolean foundLineDirective = false; while (lowerEntry != null && (entry.getKey() - lowerEntry.getKey()) < 300) { - String lowerSourceFile = fileNamePool.get(lowerEntry.getValue().sourceFileNameId()); + String lowerSourceFile = s.fileNamePool.get(lowerEntry.getValue().sourceFileNameId()); + if (System.getenv("DEBUG_CALLER") != null) { + System.err.println("DEBUG parseStackTraceElement: checking lowerEntry key=" + lowerEntry.getKey() + " sourceFile=" + lowerSourceFile + " line=" + lowerEntry.getValue().lineNumber() + " entryKey=" + entry.getKey()); + } if (!lowerSourceFile.equals(element.getFileName())) { // Found an entry with #line-adjusted filename // Calculate the offset: the difference between the original line and the #line-adjusted line @@ -293,7 +326,10 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H int estimatedExtraLines = tokenDistFromLineDirective / 6; lineNumber = lowerEntry.getValue().lineNumber() + estimatedExtraLines; - packageName = packageNamePool.get(lowerEntry.getValue().packageNameId()); + packageName = s.packageNamePool.get(lowerEntry.getValue().packageNameId()); + if (System.getenv("DEBUG_CALLER") != null) { + System.err.println("DEBUG parseStackTraceElement: APPLYING lowerEntry sourceFile=" + sourceFileName + " adjustedLine=" + lineNumber + " tokenDist=" + tokenDistFromLineDirective); + } break; } // This lower entry still has the original file, keep looking @@ -305,14 +341,20 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H int currentKey = entry.getKey(); var higherEntry = info.tokenToLineInfo.higherEntry(currentKey); while (higherEntry != null && (higherEntry.getKey() - entry.getKey()) < 50) { - String higherSourceFile = fileNamePool.get(higherEntry.getValue().sourceFileNameId()); + String higherSourceFile = s.fileNamePool.get(higherEntry.getValue().sourceFileNameId()); + if (System.getenv("DEBUG_CALLER") != null) { + System.err.println("DEBUG parseStackTraceElement: checking higherEntry key=" + higherEntry.getKey() + " sourceFile=" + higherSourceFile + " entryKey=" + entry.getKey() + " currentKey=" + currentKey); + } if (!higherSourceFile.equals(element.getFileName())) { // Higher entry has #line-adjusted filename - use it sourceFileName = higherSourceFile; lineNumber = higherEntry.getValue().lineNumber() - (higherEntry.getKey() - entry.getKey()); // Approximate adjustment if (lineNumber < 1) lineNumber = 1; - packageName = packageNamePool.get(higherEntry.getValue().packageNameId()); + packageName = s.packageNamePool.get(higherEntry.getValue().packageNameId()); + if (System.getenv("DEBUG_CALLER") != null) { + System.err.println("DEBUG parseStackTraceElement: APPLYING higherEntry sourceFile=" + sourceFileName + " adjustedLine=" + lineNumber); + } break; } // This higher entry still has the original file, keep looking @@ -324,7 +366,7 @@ public static SourceLocation parseStackTraceElement(StackTraceElement element, H // Retrieve subroutine name - String subroutineName = subroutineNamePool.get(lineInfo.subroutineNameId()); + String subroutineName = s.subroutineNamePool.get(lineInfo.subroutineNameId()); // If subroutine name is empty string (main code), convert to null if (subroutineName != null && subroutineName.isEmpty()) { subroutineName = null; diff --git a/src/main/java/org/perlonjava/backend/jvm/Dereference.java b/src/main/java/org/perlonjava/backend/jvm/Dereference.java index e614ded16..8594d3c99 100644 --- a/src/main/java/org/perlonjava/backend/jvm/Dereference.java +++ b/src/main/java/org/perlonjava/backend/jvm/Dereference.java @@ -15,8 +15,9 @@ import static org.perlonjava.runtime.perlmodule.Strict.HINT_STRICT_REFS; public class Dereference { - // Callsite ID counter for inline method caching (unique across all compilations) - private static int nextMethodCallsiteId = 0; + // Callsite ID counter for inline method caching (unique across all compilations, thread-safe) + private static final java.util.concurrent.atomic.AtomicInteger nextMethodCallsiteId = + new java.util.concurrent.atomic.AtomicInteger(0); /** * Handles the postfix `[]` operator. @@ -965,7 +966,7 @@ static void handleArrowOperator(EmitterVisitor emitterVisitor, BinaryOperatorNod mv.visitVarInsn(Opcodes.ISTORE, callContextSlot); // Allocate a unique callsite ID for inline method caching - int callsiteId = nextMethodCallsiteId++; + int callsiteId = nextMethodCallsiteId.getAndIncrement(); mv.visitLdcInsn(callsiteId); mv.visitVarInsn(Opcodes.ALOAD, objectSlot); mv.visitVarInsn(Opcodes.ALOAD, methodSlot); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitEval.java b/src/main/java/org/perlonjava/backend/jvm/EmitEval.java index aff8c7eec..77b7e6ab0 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitEval.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitEval.java @@ -34,7 +34,7 @@ * for each eval site. This tag links the runtime eval to its compile-time context *

  • Reflection for Instantiation: We use Constructor.newInstance() rather than * direct instantiation because class names are generated at runtime
  • - *
  • Global ClassLoader: All eval classes use GlobalVariable.globalClassLoader + *
  • Global ClassLoader: All eval classes use GlobalVariable.getGlobalClassLoader() * to ensure they can reference each other and share the same namespace
  • * * @@ -135,7 +135,7 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) // Generate unique identifier for this eval site // This counter is incremented globally, ensuring each eval gets a unique tag - int counter = EmitterMethodCreator.classCounter++; + int counter = EmitterMethodCreator.classCounter.getAndIncrement(); // Create compiler options specific to this eval // The filename becomes "(eval N)" for better error messages @@ -169,7 +169,7 @@ static void handleEvalOperator(EmitterVisitor emitterVisitor, OperatorNode node) // Store the context in a static map, indexed by evalTag // This allows the runtime compilation to access the compile-time environment - RuntimeCode.evalContext.put(evalTag, evalCtx); + RuntimeCode.getEvalContext().put(evalTag, evalCtx); // Generate bytecode to evaluate the eval string expression // This pushes the string value onto the stack diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java index eaccb8cf3..db6682c91 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitOperator.java @@ -150,9 +150,9 @@ static void handleReadlineOperator(EmitterVisitor emitterVisitor, BinaryOperator } else { emitterVisitor.ctx.mv.visitInsn(Opcodes.ACONST_NULL); } - emitterVisitor.ctx.mv.visitFieldInsn(Opcodes.PUTSTATIC, + emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/RuntimeIO", - "lastReadlineHandleName", "Ljava/lang/String;"); + "setLastReadlineHandleName", "(Ljava/lang/String;)V", false); // Emit the File Handle emitFileHandle(emitterVisitor.with(RuntimeContextType.SCALAR), node.left); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java b/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java index e5e833e90..2f9212822 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitRegex.java @@ -17,8 +17,9 @@ * transliteration and replacement. */ public class EmitRegex { - // Callsite ID counter for /o modifier support (unique across all JVM compilations) - private static int nextCallsiteId = 100000; // Start at 100000 to avoid collision with interpreter IDs + // Callsite ID counter for /o modifier support (unique across all JVM compilations, thread-safe) + private static final java.util.concurrent.atomic.AtomicInteger nextCallsiteId = + new java.util.concurrent.atomic.AtomicInteger(100000); // Start at 100000 to avoid collision with interpreter IDs /** * Handles the binding regex operation where a variable is bound to a regex operation. @@ -280,7 +281,7 @@ static void handleMatchRegex(EmitterVisitor emitterVisitor, OperatorNode node) { // Create the regex matcher (use 3-argument version for /o or m?PAT?) if (needsCallsiteCache) { - int callsiteId = nextCallsiteId++; + int callsiteId = nextCallsiteId.getAndIncrement(); emitterVisitor.ctx.mv.visitLdcInsn(callsiteId); emitterVisitor.ctx.mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/regex/RuntimeRegex", "getQuotedRegex", diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java index 0c1e14fbd..a94e25611 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitSubroutine.java @@ -224,7 +224,7 @@ public static void emitSubroutine(EmitterContext ctx, SubroutineNode node) { String newClassNameDot = subCtx.javaClassInfo.javaClassName.replace('/', '.'); if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Generated class name: " + newClassNameDot + " internal " + subCtx.javaClassInfo.javaClassName); if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Generated class env: " + Arrays.toString(newEnv)); - RuntimeCode.anonSubs.put(subCtx.javaClassInfo.javaClassName, generatedClass); // Cache the class + RuntimeCode.getAnonSubs().put(subCtx.javaClassInfo.javaClassName, generatedClass); // Cache the class // Transfer pad constants (cached string literals referenced via \) from compile time // to a registry so makeCodeObject() can attach them to the RuntimeCode at runtime. @@ -291,14 +291,15 @@ public static void emitSubroutine(EmitterContext ctx, SubroutineNode node) { // Store the InterpretedCode in the interpretedSubs map with a unique key String fallbackKey = "interpreted_" + System.identityHashCode(fallback.interpretedCode); - RuntimeCode.interpretedSubs.put(fallbackKey, fallback.interpretedCode); + RuntimeCode.getInterpretedSubs().put(fallbackKey, fallback.interpretedCode); // Generate bytecode to retrieve and configure the InterpretedCode // 1. Load the InterpretedCode from the map - mv.visitFieldInsn(Opcodes.GETSTATIC, + mv.visitMethodInsn(Opcodes.INVOKESTATIC, "org/perlonjava/runtime/runtimetypes/RuntimeCode", - "interpretedSubs", - "Ljava/util/HashMap;"); + "getInterpretedSubs", + "()Ljava/util/HashMap;", + false); mv.visitLdcInsn(fallbackKey); mv.visitMethodInsn(Opcodes.INVOKEVIRTUAL, "java/util/HashMap", diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java index 0ecce00c2..4b728e259 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitVariable.java @@ -1448,7 +1448,7 @@ static void handleMyOperator(EmitterVisitor emitterVisitor, OperatorNode node) { String className = EmitterMethodCreator.getVariableClassName(sigil); if (operator.equals("my")) { - Integer beginId = RuntimeCode.evalBeginIds.get(sigilNode); + Integer beginId = RuntimeCode.getEvalBeginIds().get(sigilNode); if (beginId == null) { ctx.mv.visitTypeInsn(Opcodes.NEW, className); ctx.mv.visitInsn(Opcodes.DUP); diff --git a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java index b1470199b..944a9eb56 100644 --- a/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java +++ b/src/main/java/org/perlonjava/backend/jvm/EmitterMethodCreator.java @@ -14,6 +14,7 @@ import org.perlonjava.backend.bytecode.Disassemble; import org.perlonjava.backend.bytecode.InterpretedCode; import org.perlonjava.frontend.analysis.EmitterVisitor; +import org.perlonjava.frontend.analysis.RegexUsageDetector; import org.perlonjava.frontend.analysis.TempLocalCountVisitor; import org.perlonjava.frontend.astnode.BlockNode; import org.perlonjava.frontend.astnode.CompilerFlagNode; @@ -24,6 +25,7 @@ import java.io.PrintWriter; import java.lang.annotation.Annotation; import java.lang.reflect.*; +import java.util.concurrent.atomic.AtomicInteger; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; @@ -47,13 +49,13 @@ public class EmitterMethodCreator implements Opcodes { private static final boolean SHOW_FALLBACK = System.getenv("JPERL_SHOW_FALLBACK") != null; // Number of local variables to skip when processing a closure (this, @_, wantarray) - public static int skipVariables = 3; - // Counter for generating unique class names - public static int classCounter = 0; + public static final int skipVariables = 3; + // Counter for generating unique class names (thread-safe) + public static final AtomicInteger classCounter = new AtomicInteger(0); // Generate a unique internal class name public static String generateClassName() { - return "org/perlonjava/anon" + classCounter++; + return "org/perlonjava/anon" + classCounter.getAndIncrement(); } private static String insnToString(AbstractInsnNode n) { @@ -628,8 +630,14 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean // Store dynamicIndex so goto &sub can access it for cleanup before tail call ctx.javaClassInfo.dynamicLevelSlot = dynamicIndex; - mv.visitMethodInsn(Opcodes.INVOKESTATIC, - "org/perlonjava/runtime/runtimetypes/RegexState", "save", "()V", false); + // Only save/restore regex state if the subroutine body contains regex + // operations (or eval STRING which may introduce them at runtime). + // Subroutines without regex don't modify regex state, and callees + // that use regex do their own save/restore. + if (RegexUsageDetector.containsRegexOperation(ast)) { + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RegexState", "save", "()V", false); + } // Store the computed RuntimeList return value in a dedicated local slot. // This keeps the operand stack empty at join labels (endCatch), avoiding @@ -1235,7 +1243,7 @@ private static byte[] getBytecodeInternal(EmitterContext ctx, Node ast, boolean PrintWriter verifyPw = new PrintWriter(System.err); String thisClassNameDot = className.replace('/', '.'); final byte[] verifyClassData = classData; - ClassLoader verifyLoader = new ClassLoader(GlobalVariable.globalClassLoader) { + ClassLoader verifyLoader = new ClassLoader(GlobalVariable.getGlobalClassLoader()) { @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.equals(thisClassNameDot)) { @@ -1277,7 +1285,7 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE PrintWriter verifyPw = new PrintWriter(System.err); String thisClassNameDot = className.replace('/', '.'); final byte[] verifyClassData = classData; - ClassLoader verifyLoader = new ClassLoader(GlobalVariable.globalClassLoader) { + ClassLoader verifyLoader = new ClassLoader(GlobalVariable.getGlobalClassLoader()) { @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.equals(thisClassNameDot)) { @@ -1327,7 +1335,7 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE PrintWriter verifyPw = new PrintWriter(System.err); String thisClassNameDot = className.replace('/', '.'); final byte[] verifyClassData = classData; - ClassLoader verifyLoader = new ClassLoader(GlobalVariable.globalClassLoader) { + ClassLoader verifyLoader = new ClassLoader(GlobalVariable.getGlobalClassLoader()) { @Override protected Class loadClass(String name, boolean resolve) throws ClassNotFoundException { if (name.equals(thisClassNameDot)) { @@ -1481,7 +1489,7 @@ protected Class loadClass(String name, boolean resolve) throws ClassNotFoundE */ public static Class loadBytecode(EmitterContext ctx, byte[] classData) { // Use the global class loader to ensure all generated classes are in the same namespace - CustomClassLoader loader = GlobalVariable.globalClassLoader; + CustomClassLoader loader = GlobalVariable.getGlobalClassLoader(); // Create a "Java" class name with dots instead of slashes String javaClassNameDot = ctx.javaClassInfo.javaClassName.replace('/', '.'); @@ -1822,27 +1830,21 @@ private static void applyCompilerFlagNodes(EmitterContext ctx, Node ast) { /** * Emits bytecode to increment RuntimeCode.evalDepth (for $^S support). - * Stack effect: net 0 (pushes 2, pops 2). + * Calls RuntimeCode.incrementEvalDepth() static method. + * Stack effect: net 0. */ private static void emitEvalDepthIncrement(MethodVisitor mv) { - mv.visitFieldInsn(Opcodes.GETSTATIC, - "org/perlonjava/runtime/runtimetypes/RuntimeCode", "evalDepth", "I"); - mv.visitInsn(Opcodes.ICONST_1); - mv.visitInsn(Opcodes.IADD); - mv.visitFieldInsn(Opcodes.PUTSTATIC, - "org/perlonjava/runtime/runtimetypes/RuntimeCode", "evalDepth", "I"); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", "incrementEvalDepth", "()V", false); } /** * Emits bytecode to decrement RuntimeCode.evalDepth (for $^S support). - * Stack effect: net 0 (pushes 2, pops 2). + * Calls RuntimeCode.decrementEvalDepth() static method. + * Stack effect: net 0. */ private static void emitEvalDepthDecrement(MethodVisitor mv) { - mv.visitFieldInsn(Opcodes.GETSTATIC, - "org/perlonjava/runtime/runtimetypes/RuntimeCode", "evalDepth", "I"); - mv.visitInsn(Opcodes.ICONST_1); - mv.visitInsn(Opcodes.ISUB); - mv.visitFieldInsn(Opcodes.PUTSTATIC, - "org/perlonjava/runtime/runtimetypes/RuntimeCode", "evalDepth", "I"); + mv.visitMethodInsn(Opcodes.INVOKESTATIC, + "org/perlonjava/runtime/runtimetypes/RuntimeCode", "decrementEvalDepth", "()V", false); } } diff --git a/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java b/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java new file mode 100644 index 000000000..bdb3c134b --- /dev/null +++ b/src/main/java/org/perlonjava/backend/jvm/JvmClosureTemplate.java @@ -0,0 +1,131 @@ +package org.perlonjava.backend.jvm; + +import org.perlonjava.runtime.runtimetypes.PerlSubroutine; +import org.perlonjava.runtime.runtimetypes.RuntimeBase; +import org.perlonjava.runtime.runtimetypes.RuntimeCode; +import org.perlonjava.runtime.runtimetypes.RuntimeScalar; + +import java.lang.reflect.Constructor; +import java.lang.reflect.Field; + +/** + * Template for creating JVM-compiled closures from interpreter bytecode. + *

    + * When an anonymous sub inside eval STRING is compiled to a JVM class + * (instead of InterpretedCode), this template holds the generated class + * and knows how to instantiate it with captured variables at runtime. + *

    + * The CREATE_CLOSURE interpreter opcode checks for this type in the + * constant pool and delegates to {@link #instantiate(RuntimeBase[])} + * to create the closure. + */ +public class JvmClosureTemplate { + + /** + * The JVM-compiled class implementing PerlSubroutine + */ + public final Class generatedClass; + + /** + * Cached constructor for the generated class. + * Takes captured variables as typed parameters (RuntimeScalar, RuntimeArray, RuntimeHash). + */ + public final Constructor constructor; + + /** + * Perl prototype string (e.g., "$$@"), or null + */ + public final String prototype; + + /** + * Package where the sub was compiled (CvSTASH) + */ + public final String packageName; + + /** + * Creates a JvmClosureTemplate for a generated class. + * + * @param generatedClass the JVM class implementing PerlSubroutine + * @param prototype Perl prototype string, or null + * @param packageName package where the sub was compiled + */ + public JvmClosureTemplate(Class generatedClass, String prototype, String packageName) { + this.generatedClass = generatedClass; + this.prototype = prototype; + this.packageName = packageName; + + // Cache the constructor - there should be exactly one with closure parameters + Constructor[] constructors = generatedClass.getDeclaredConstructors(); + // Find the constructor that takes the most parameters (the closure one) + Constructor best = null; + for (Constructor c : constructors) { + if (best == null || c.getParameterCount() > best.getParameterCount()) { + best = c; + } + } + this.constructor = best; + } + + /** + * Instantiate the JVM-compiled closure with captured variables. + *

    + * This is called at runtime by the interpreter's CREATE_CLOSURE opcode + * when it encounters a JvmClosureTemplate in the constant pool. + *

    + * The cost of reflection here is amortized: this is called once per + * closure creation (each time the sub {} expression is evaluated), + * not per call to the closure. + * + * @param capturedVars the captured variable values from the interpreter's registers + * @return a RuntimeScalar wrapping the RuntimeCode for this closure + */ + public RuntimeScalar instantiate(RuntimeBase[] capturedVars) { + try { + // Convert RuntimeBase[] to Object[] for reflection + Object[] args = new Object[capturedVars.length]; + System.arraycopy(capturedVars, 0, args, 0, capturedVars.length); + + // Instantiate the JVM class with captured variables as constructor args + PerlSubroutine instance = (PerlSubroutine) constructor.newInstance(args); + + // Create RuntimeCode wrapping and set __SUB__ + RuntimeCode code = new RuntimeCode(instance, prototype); + if (packageName != null) { + code.packageName = packageName; + } + RuntimeScalar codeRef = new RuntimeScalar(code); + + // Set __SUB__ on the generated class instance for self-reference + Field subField = generatedClass.getDeclaredField("__SUB__"); + subField.set(instance, codeRef); + + return codeRef; + } catch (Exception e) { + throw new RuntimeException("Failed to instantiate JVM closure: " + e.getMessage(), e); + } + } + + /** + * Instantiate the JVM-compiled sub with no captured variables. + * + * @return a RuntimeScalar wrapping the RuntimeCode + */ + public RuntimeScalar instantiateNoClosure() { + try { + PerlSubroutine instance = (PerlSubroutine) generatedClass.getDeclaredConstructor().newInstance(); + + RuntimeCode code = new RuntimeCode(instance, prototype); + if (packageName != null) { + code.packageName = packageName; + } + RuntimeScalar codeRef = new RuntimeScalar(code); + + Field subField = generatedClass.getDeclaredField("__SUB__"); + subField.set(instance, codeRef); + + return codeRef; + } catch (Exception e) { + throw new RuntimeException("Failed to instantiate JVM sub: " + e.getMessage(), e); + } + } +} diff --git a/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java b/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java index 2f019ac46..fc2fe3d9e 100644 --- a/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java +++ b/src/main/java/org/perlonjava/backend/jvm/astrefactor/LargeBlockRefactorer.java @@ -24,9 +24,6 @@ */ public class LargeBlockRefactorer { - // Reusable visitor for control flow detection - private static final ControlFlowDetectorVisitor controlFlowDetector = new ControlFlowDetectorVisitor(); - private static long estimateTotalBytecodeSizeCapped(List nodes, long capInclusive) { long total = 0; for (Node node : nodes) { @@ -103,9 +100,10 @@ private static boolean isSpecialContext(BlockNode node) { private static boolean tryWholeBlockRefactoring(EmitterVisitor emitterVisitor, BlockNode node) { // Check for unsafe control flow using ControlFlowDetectorVisitor // This properly handles loop depth - unlabeled next/last/redo inside loops are safe - controlFlowDetector.reset(); - controlFlowDetector.scan(node); - if (controlFlowDetector.hasUnsafeControlFlow()) { + // Create a new instance per call to avoid thread-safety issues with shared mutable state + ControlFlowDetectorVisitor detector = new ControlFlowDetectorVisitor(); + detector.scan(node); + if (detector.hasUnsafeControlFlow()) { return false; } diff --git a/src/main/java/org/perlonjava/core/Configuration.java b/src/main/java/org/perlonjava/core/Configuration.java index ae081404f..27b2a98b9 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 = "ffc466124"; + public static final String gitCommitId = "d7b8bc7ee"; /** * 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-04-10"; + public static final String gitCommitDate = "2026-04-11"; /** * Build timestamp in Perl 5 "Compiled at" format (e.g., "Apr 7 2026 11:20:00"). @@ -48,7 +48,7 @@ public final class Configuration { * Parsed by App::perlbrew and other tools via: perl -V | grep "Compiled at" * DO NOT EDIT MANUALLY - this value is replaced at build time. */ - public static final String buildTimestamp = "Apr 10 2026 22:16:43"; + public static final String buildTimestamp = "Apr 11 2026 08:21:54"; // Prevent instantiation private Configuration() { diff --git a/src/main/java/org/perlonjava/frontend/analysis/ConstantFoldingVisitor.java b/src/main/java/org/perlonjava/frontend/analysis/ConstantFoldingVisitor.java index 1b8c44e41..7b7868380 100644 --- a/src/main/java/org/perlonjava/frontend/analysis/ConstantFoldingVisitor.java +++ b/src/main/java/org/perlonjava/frontend/analysis/ConstantFoldingVisitor.java @@ -142,7 +142,7 @@ private static Boolean resolveConstantSubBoolean(String name, String currentPack String fullName = NameNormalizer.normalizeVariableName(name, currentPackage); // Use direct map lookup to avoid side effects of getGlobalCodeRef(), // which auto-vivifies empty CODE entries and pins references - RuntimeScalar codeRef = GlobalVariable.globalCodeRefs.get(fullName); + RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRefsMap().get(fullName); if (codeRef != null && codeRef.value instanceof RuntimeCode code) { if (code.constantValue != null) { RuntimeList constList = code.constantValue; @@ -538,7 +538,7 @@ private Node resolveConstantSubValue(String name, int tokenIndex) { String fullName = NameNormalizer.normalizeVariableName(name, currentPackage); // Use direct map lookup to avoid side effects of getGlobalCodeRef(), // which auto-vivifies empty CODE entries and pins references - RuntimeScalar codeRef = GlobalVariable.globalCodeRefs.get(fullName); + RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRefsMap().get(fullName); if (codeRef != null && codeRef.value instanceof RuntimeCode code) { if (code.constantValue != null) { RuntimeList constList = code.constantValue; diff --git a/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java b/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java index dd7814604..e9227b528 100644 --- a/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java +++ b/src/main/java/org/perlonjava/frontend/analysis/RegexUsageDetector.java @@ -21,10 +21,11 @@ public class RegexUsageDetector { /** - * Unary operators that perform regex matching/substitution. + * Unary operators that perform regex matching/substitution, + * or that may dynamically introduce regex operations (eval STRING). */ private static final java.util.Set REGEX_OPERATORS = - java.util.Set.of("matchRegex", "replaceRegex"); + java.util.Set.of("matchRegex", "replaceRegex", "eval"); /** * Binary operators that perform regex matching (=~, !~) or use regex internally (split). */ diff --git a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java index 1167eaa03..53c740bdd 100644 --- a/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/OperatorParser.java @@ -494,7 +494,7 @@ static OperatorNode parseVariableDeclaration(Parser parser, String operator, int if (operator.equals("state")) { // Give the variable a persistent id (See: PersistentVariable.java) if (operandNode.id == 0) { - operandNode.id = EmitterMethodCreator.classCounter++; + operandNode.id = EmitterMethodCreator.classCounter.getAndIncrement(); } } @@ -1338,12 +1338,12 @@ private static void callModifyVariableAttributes(Parser parser, String packageNa RuntimeArray.push(canArgs, new RuntimeScalar(packageName)); RuntimeArray.push(canArgs, new RuntimeScalar(modifyMethod)); - InheritanceResolver.autoloadEnabled = false; + InheritanceResolver.setAutoloadEnabled(false); RuntimeList codeList; try { codeList = Universal.can(canArgs, RuntimeContextType.SCALAR); } finally { - InheritanceResolver.autoloadEnabled = true; + InheritanceResolver.setAutoloadEnabled(true); } boolean hasHandler = codeList.size() == 1 && codeList.getFirst().getBoolean(); diff --git a/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java b/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java index cd31f8070..651c575e3 100644 --- a/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java +++ b/src/main/java/org/perlonjava/frontend/parser/ParsePrimary.java @@ -168,7 +168,7 @@ private static Node parseIdentifier(Parser parser, int startIndex, LexerToken to // Check for local package override (only if explicitly imported/declared) String fullName = parser.ctx.symbolTable.getCurrentPackage() + "::" + operator; - if (GlobalVariable.isSubs.getOrDefault(fullName, false)) { + if (GlobalVariable.getIsSubsMap().getOrDefault(fullName, false)) { // Example: 'use subs "hex"; sub hex { 456 } print hex("123"), "\n"' // Or: 'use Time::HiRes "time"; print time, "\n"' (sub imported at BEGIN time) parser.tokenIndex = startIndex; // backtrack to reparse as subroutine diff --git a/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java b/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java index 2f6ce3e20..452a2f3b5 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SpecialBlockParser.java @@ -198,10 +198,10 @@ static RuntimeList runSpecialBlock(Parser parser, String blockPhase, Node block, new IdentifierNode(packageName, tokenIndex), tokenIndex)); } else { OperatorNode ast = entry.ast(); - isFromOuterScope = RuntimeCode.evalBeginIds.containsKey(ast); - int beginId = RuntimeCode.evalBeginIds.computeIfAbsent( + isFromOuterScope = RuntimeCode.getEvalBeginIds().containsKey(ast); + int beginId = RuntimeCode.getEvalBeginIds().computeIfAbsent( ast, - k -> EmitterMethodCreator.classCounter++); + k -> EmitterMethodCreator.classCounter.getAndIncrement()); packageName = PersistentVariable.beginPackage(beginId); // Emit: package BEGIN_PKG nodes.add( @@ -231,13 +231,13 @@ static RuntimeList runSpecialBlock(Parser parser, String blockPhase, Node block, // Put in the appropriate global map based on variable type if (runtimeValue instanceof RuntimeArray) { - GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue); + GlobalVariable.getGlobalArraysMap().put(fullName, (RuntimeArray) runtimeValue); if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("BEGIN block: Aliased array " + fullName); } else if (runtimeValue instanceof RuntimeHash) { - GlobalVariable.globalHashes.put(fullName, (RuntimeHash) runtimeValue); + GlobalVariable.getGlobalHashesMap().put(fullName, (RuntimeHash) runtimeValue); if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("BEGIN block: Aliased hash " + fullName); } else if (runtimeValue instanceof RuntimeScalar) { - GlobalVariable.globalVariables.put(fullName, (RuntimeScalar) runtimeValue); + GlobalVariable.getGlobalVariablesMap().put(fullName, (RuntimeScalar) runtimeValue); if (CompilerOptions.DEBUG_ENABLED) parser.ctx.logDebug("BEGIN block: Aliased scalar " + fullName); } } diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java index 60ddc0cd6..5b430c7e8 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementParser.java @@ -34,7 +34,7 @@ import static org.perlonjava.runtime.runtimetypes.WarningFlags.getLastScopeId; import static org.perlonjava.runtime.runtimetypes.WarningFlags.clearLastScopeId; import static org.perlonjava.runtime.perlmodule.Warnings.useWarnings; -import static org.perlonjava.runtime.runtimetypes.GlobalVariable.packageExistsCache; + import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.scalarUndef; /** @@ -759,11 +759,11 @@ public static Node parseUseDeclaration(Parser parser, LexerToken token) { RuntimeArray.push(canArgs, new RuntimeScalar(importMethod)); RuntimeList codeList = null; - InheritanceResolver.autoloadEnabled = false; + InheritanceResolver.setAutoloadEnabled(false); try { codeList = Universal.can(canArgs, RuntimeContextType.SCALAR); } finally { - InheritanceResolver.autoloadEnabled = true; + InheritanceResolver.setAutoloadEnabled(true); } if (CompilerOptions.DEBUG_ENABLED) ctx.logDebug("Use can(" + packageName + ", " + importMethod + "): " + codeList); @@ -845,7 +845,7 @@ public static Node parsePackageDeclaration(Parser parser, LexerToken token) { } // Remember that this package exists - packageExistsCache.put(packageName, true); + GlobalVariable.getPackageExistsCacheMap().put(packageName, true); boolean isClass = token.text.equals("class"); diff --git a/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java b/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java index 2b281678f..8961249d5 100644 --- a/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java +++ b/src/main/java/org/perlonjava/frontend/parser/StatementResolver.java @@ -271,7 +271,7 @@ public static Node parseStatement(Parser parser, String label) { // For state variables, assign a unique ID for persistent tracking if (declaration.equals("state")) { - innerVarNode.id = EmitterMethodCreator.classCounter++; + innerVarNode.id = EmitterMethodCreator.classCounter.getAndIncrement(); } // Now create the outer declaration node (state/my $hiddenVarName) diff --git a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java index cd4c45fd5..161febe31 100644 --- a/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java +++ b/src/main/java/org/perlonjava/frontend/parser/SubroutineParser.java @@ -211,11 +211,11 @@ static Node parseSubroutineCall(Parser parser, boolean isMethod) { // Check packageExistsCache which is populated when 'package' statement is parsed // Note: packageExistsCache uses the package name as-is for packages, // and fully qualified names for sub names (e.g., "main::error" not "error") - Boolean isPackage = GlobalVariable.packageExistsCache.get(packageName); + Boolean isPackage = GlobalVariable.getPackageExistsCacheMap().get(packageName); // Also check if this is a known sub in the current package (qualified lookup) if (isPackage == null && !packageName.contains("::")) { String qualifiedName = parser.ctx.symbolTable.getCurrentPackage() + "::" + packageName; - Boolean qualifiedResult = GlobalVariable.packageExistsCache.get(qualifiedName); + Boolean qualifiedResult = GlobalVariable.getPackageExistsCacheMap().get(qualifiedName); if (qualifiedResult != null && !qualifiedResult) { isPackage = false; } @@ -543,9 +543,9 @@ public static Node parseSubroutineDefinition(Parser parser, boolean wantName, St // as indirect method call `error->parse()`). if (subName != null && !subName.contains("::")) { String qualifiedSubName = parser.ctx.symbolTable.getCurrentPackage() + "::" + subName; - GlobalVariable.packageExistsCache.put(qualifiedSubName, false); + GlobalVariable.getPackageExistsCacheMap().put(qualifiedSubName, false); } else if (subName != null) { - GlobalVariable.packageExistsCache.put(subName, false); + GlobalVariable.getPackageExistsCacheMap().put(subName, false); } } @@ -1146,9 +1146,9 @@ public static ListNode handleNamedSubWithFilter(Parser parser, String subName, S entry.perlPackage()); } else { OperatorNode ast = entry.ast(); - int beginId = RuntimeCode.evalBeginIds.computeIfAbsent( + int beginId = RuntimeCode.getEvalBeginIds().computeIfAbsent( ast, - k -> EmitterMethodCreator.classCounter++); + k -> EmitterMethodCreator.classCounter.getAndIncrement()); variableName = NameNormalizer.normalizeVariableName( entry.name().substring(1), PersistentVariable.beginPackage(beginId)); @@ -1381,12 +1381,12 @@ private static void callModifyCodeAttributes(String packageName, RuntimeScalar c RuntimeArray.push(canArgs, new RuntimeScalar(packageName)); RuntimeArray.push(canArgs, new RuntimeScalar("MODIFY_CODE_ATTRIBUTES")); - InheritanceResolver.autoloadEnabled = false; + InheritanceResolver.setAutoloadEnabled(false); RuntimeList codeList; try { codeList = Universal.can(canArgs, RuntimeContextType.SCALAR); } finally { - InheritanceResolver.autoloadEnabled = true; + InheritanceResolver.setAutoloadEnabled(true); } boolean hasHandler = codeList.size() == 1 && codeList.getFirst().getBoolean(); diff --git a/src/main/java/org/perlonjava/runtime/CoreSubroutineGenerator.java b/src/main/java/org/perlonjava/runtime/CoreSubroutineGenerator.java index aae32cf96..c69d2d247 100644 --- a/src/main/java/org/perlonjava/runtime/CoreSubroutineGenerator.java +++ b/src/main/java/org/perlonjava/runtime/CoreSubroutineGenerator.java @@ -96,7 +96,7 @@ private static boolean installBarewordOnly(String operatorName, String prototype } /** - * Install a RuntimeCode wrapper into GlobalVariable.globalCodeRefs. + * Install a RuntimeCode wrapper into GlobalVariable.getGlobalCodeRefsMap(). */ private static boolean installWrapper(String fullName, String operatorName, String prototype, PerlSubroutine sub) { diff --git a/src/main/java/org/perlonjava/runtime/HintHashRegistry.java b/src/main/java/org/perlonjava/runtime/HintHashRegistry.java index df21571dc..8d5106b1f 100644 --- a/src/main/java/org/perlonjava/runtime/HintHashRegistry.java +++ b/src/main/java/org/perlonjava/runtime/HintHashRegistry.java @@ -2,6 +2,7 @@ import org.perlonjava.runtime.runtimetypes.GlobalContext; import org.perlonjava.runtime.runtimetypes.GlobalVariable; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import org.perlonjava.runtime.runtimetypes.RuntimeHash; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; @@ -22,6 +23,9 @@ * 2. Per-call-site tracking using snapshot IDs: registerSnapshot() captures %^H * at compile time, and setCallSiteHintHashId()/pushCallerHintHash()/ * getCallerHintHashAtFrame() bridge compile-time state to runtime caller()[10]. + * + * Per-call-site stacks are per-PerlRuntime instance fields (accessed via + * PerlRuntime.current()) instead of separate ThreadLocals. */ public class HintHashRegistry { @@ -38,15 +42,6 @@ public class HintHashRegistry { new ConcurrentHashMap<>(); private static final AtomicInteger nextSnapshotId = new AtomicInteger(0); - // ThreadLocal tracking the current call site's snapshot ID. - // Updated at runtime from emitted bytecode. - private static final ThreadLocal callSiteSnapshotId = - ThreadLocal.withInitial(() -> 0); - - // ThreadLocal stack saving caller's snapshot ID across subroutine calls. - private static final ThreadLocal> callerSnapshotIdStack = - ThreadLocal.withInitial(ArrayDeque::new); - // ---- Compile-time %^H scoping ---- /** @@ -110,7 +105,7 @@ public static int snapshotCurrentHintHash() { * @param id the snapshot ID (0 = empty/no hints) */ public static void setCallSiteHintHashId(int id) { - callSiteSnapshotId.set(id); + PerlRuntime.current().hintCallSiteSnapshotId = id; } /** @@ -120,11 +115,11 @@ public static void setCallSiteHintHashId(int id) { * Called by RuntimeCode.apply() before entering a subroutine. */ public static void pushCallerHintHash() { - int currentId = callSiteSnapshotId.get(); - callerSnapshotIdStack.get().push(currentId); + PerlRuntime rt = PerlRuntime.current(); + rt.hintCallerSnapshotIdStack.push(rt.hintCallSiteSnapshotId); // Reset callsite for the callee - it should not inherit the caller's hints. // The callee's own CompilerFlagNodes will set the correct ID if needed. - callSiteSnapshotId.set(0); + rt.hintCallSiteSnapshotId = 0; } /** @@ -133,12 +128,10 @@ public static void pushCallerHintHash() { * Called by RuntimeCode.apply() after a subroutine returns. */ public static void popCallerHintHash() { - Deque stack = callerSnapshotIdStack.get(); + PerlRuntime rt = PerlRuntime.current(); + Deque stack = rt.hintCallerSnapshotIdStack; if (!stack.isEmpty()) { - int restoredId = stack.pop(); - // Restore the callsite ID so eval STRING and subsequent code - // see the correct hint hash, not one clobbered by the callee. - callSiteSnapshotId.set(restoredId); + rt.hintCallSiteSnapshotId = stack.pop(); } } @@ -150,7 +143,7 @@ public static void popCallerHintHash() { * @return The hint hash map, or null if not available */ public static Map getCallerHintHashAtFrame(int frame) { - Deque stack = callerSnapshotIdStack.get(); + Deque stack = PerlRuntime.current().hintCallerSnapshotIdStack; if (stack.isEmpty()) { return null; } @@ -172,7 +165,7 @@ public static Map getCallerHintHashAtFrame(int frame) { * @return the hint hash map, or null if empty/not set */ public static Map getCurrentCallSiteHintHash() { - int id = callSiteSnapshotId.get(); + int id = PerlRuntime.current().hintCallSiteSnapshotId; if (id == 0) return null; return snapshotRegistry.get(id); } @@ -185,7 +178,8 @@ public static void clear() { compileTimeStack.clear(); snapshotRegistry.clear(); nextSnapshotId.set(0); - callSiteSnapshotId.set(0); - callerSnapshotIdStack.get().clear(); + PerlRuntime rt = PerlRuntime.current(); + rt.hintCallSiteSnapshotId = 0; + rt.hintCallerSnapshotIdStack.clear(); } } diff --git a/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java b/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java index 7e6a4898f..02c259e19 100644 --- a/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java +++ b/src/main/java/org/perlonjava/runtime/WarningBitsRegistry.java @@ -1,11 +1,12 @@ package org.perlonjava.runtime; -import java.util.ArrayDeque; import java.util.Deque; +import java.util.Map; import java.util.concurrent.ConcurrentHashMap; import org.perlonjava.runtime.runtimetypes.GlobalContext; import org.perlonjava.runtime.runtimetypes.GlobalVariable; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import org.perlonjava.runtime.runtimetypes.RuntimeHash; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; @@ -20,54 +21,17 @@ * * At runtime, caller() looks up warning bits by class name. * - * Additionally, a ThreadLocal stack tracks the "current" warning bits - * for runtime code that needs to check FATAL warnings. + * Warning/hints stacks are per-PerlRuntime instance fields (accessed + * via PerlRuntime.current()) instead of separate ThreadLocals, reducing + * the number of ThreadLocal lookups per subroutine call. */ public class WarningBitsRegistry { // Map from fully-qualified class name to warning bits string + // This is shared across runtimes (immutable after registration) private static final ConcurrentHashMap registry = new ConcurrentHashMap<>(); - // ThreadLocal stack of warning bits for the current execution context - // This allows runtime code to find warning bits even at top-level (no subroutine frame) - private static final ThreadLocal> currentBitsStack = - ThreadLocal.withInitial(ArrayDeque::new); - - // ThreadLocal tracking the warning bits at the current call site. - // Updated at runtime when 'use warnings' / 'no warnings' pragmas are encountered. - // This provides per-statement warning bits (like Perl 5's per-COP bits). - private static final ThreadLocal callSiteBits = - ThreadLocal.withInitial(() -> null); - - // ThreadLocal stack saving caller's call-site bits across subroutine calls. - // Each apply() pushes the current callSiteBits before calling the subroutine, - // and pops it when the subroutine returns. This allows caller()[9] to return - // the correct per-call-site warning bits. - private static final ThreadLocal> callerBitsStack = - ThreadLocal.withInitial(ArrayDeque::new); - - // ThreadLocal tracking the compile-time $^H (hints) at the current call site. - // Updated at runtime when pragmas (use strict, etc.) are encountered. - // This provides per-statement hints for caller()[8]. - private static final ThreadLocal callSiteHints = - ThreadLocal.withInitial(() -> 0); - - // ThreadLocal stack saving caller's $^H hints across subroutine calls. - // Mirrors callerBitsStack but for $^H instead of warning bits. - private static final ThreadLocal> callerHintsStack = - ThreadLocal.withInitial(ArrayDeque::new); - - // ThreadLocal tracking the compile-time %^H (hints hash) at the current call site. - // Updated at runtime when pragmas modify %^H. - // This provides per-statement hints hash for caller()[10]. - private static final ThreadLocal> callSiteHintHash = - ThreadLocal.withInitial(java.util.HashMap::new); - - // ThreadLocal stack saving caller's %^H across subroutine calls. - private static final ThreadLocal>> callerHintHashStack = - ThreadLocal.withInitial(ArrayDeque::new); - /** * Registers the warning bits for a class. * Called at class load time (static initializer) for JVM backend, @@ -104,7 +68,7 @@ public static String get(String className) { */ public static void pushCurrent(String bits) { if (bits != null) { - currentBitsStack.get().push(bits); + PerlRuntime.current().warningCurrentBitsStack.push(bits); } } @@ -113,7 +77,7 @@ public static void pushCurrent(String bits) { * Called when exiting a subroutine or code block. */ public static void popCurrent() { - Deque stack = currentBitsStack.get(); + Deque stack = PerlRuntime.current().warningCurrentBitsStack; if (!stack.isEmpty()) { stack.pop(); } @@ -126,7 +90,7 @@ public static void popCurrent() { * @return The current warning bits string, or null if stack is empty */ public static String getCurrent() { - Deque stack = currentBitsStack.get(); + Deque stack = PerlRuntime.current().warningCurrentBitsStack; return stack.isEmpty() ? null : stack.peek(); } @@ -136,13 +100,14 @@ public static String getCurrent() { */ public static void clear() { registry.clear(); - currentBitsStack.get().clear(); - callSiteBits.remove(); - callerBitsStack.get().clear(); - callSiteHints.remove(); - callerHintsStack.get().clear(); - callSiteHintHash.get().clear(); - callerHintHashStack.get().clear(); + PerlRuntime rt = PerlRuntime.current(); + rt.warningCurrentBitsStack.clear(); + rt.warningCallSiteBits = null; + rt.warningCallerBitsStack.clear(); + rt.warningCallSiteHints = 0; + rt.warningCallerHintsStack.clear(); + rt.warningCallSiteHintHash.clear(); + rt.warningCallerHintHashStack.clear(); } /** @@ -153,7 +118,7 @@ public static void clear() { * @param bits The warning bits string for the current call site */ public static void setCallSiteBits(String bits) { - callSiteBits.set(bits); + PerlRuntime.current().warningCallSiteBits = bits; } /** @@ -162,7 +127,7 @@ public static void setCallSiteBits(String bits) { * @return The current call-site warning bits, or null if not set */ public static String getCallSiteBits() { - return callSiteBits.get(); + return PerlRuntime.current().warningCallSiteBits; } /** @@ -171,8 +136,9 @@ public static String getCallSiteBits() { * This preserves the caller's warning bits so caller()[9] can retrieve them. */ public static void pushCallerBits() { - String bits = callSiteBits.get(); - callerBitsStack.get().push(bits != null ? bits : ""); + PerlRuntime rt = PerlRuntime.current(); + String bits = rt.warningCallSiteBits; + rt.warningCallerBitsStack.push(bits != null ? bits : ""); } /** @@ -180,7 +146,7 @@ public static void pushCallerBits() { * Called by RuntimeCode.apply() after a subroutine returns. */ public static void popCallerBits() { - Deque stack = callerBitsStack.get(); + Deque stack = PerlRuntime.current().warningCallerBitsStack; if (!stack.isEmpty()) { stack.pop(); } @@ -195,7 +161,7 @@ public static void popCallerBits() { * @return The warning bits string, or null if not available */ public static String getCallerBitsAtFrame(int frame) { - Deque stack = callerBitsStack.get(); + Deque stack = PerlRuntime.current().warningCallerBitsStack; if (stack.isEmpty()) { return null; } @@ -214,7 +180,7 @@ public static String getCallerBitsAtFrame(int frame) { * Returns the number of registered classes. * Useful for debugging and testing. * - * @return The number of registered class → bits mappings + * @return The number of registered class -> bits mappings */ public static int size() { return registry.size(); @@ -229,7 +195,7 @@ public static int size() { * @param hints The $^H bitmask */ public static void setCallSiteHints(int hints) { - callSiteHints.set(hints); + PerlRuntime.current().warningCallSiteHints = hints; } /** @@ -238,7 +204,7 @@ public static void setCallSiteHints(int hints) { * @return The current call-site $^H value */ public static int getCallSiteHints() { - return callSiteHints.get(); + return PerlRuntime.current().warningCallSiteHints; } /** @@ -246,7 +212,8 @@ public static int getCallSiteHints() { * Called by RuntimeCode.apply() before entering a subroutine. */ public static void pushCallerHints() { - callerHintsStack.get().push(callSiteHints.get()); + PerlRuntime rt = PerlRuntime.current(); + rt.warningCallerHintsStack.push(rt.warningCallSiteHints); } /** @@ -254,7 +221,7 @@ public static void pushCallerHints() { * Called by RuntimeCode.apply() after a subroutine returns. */ public static void popCallerHints() { - Deque stack = callerHintsStack.get(); + Deque stack = PerlRuntime.current().warningCallerHintsStack; if (!stack.isEmpty()) { stack.pop(); } @@ -269,7 +236,7 @@ public static void popCallerHints() { * @return The $^H value, or -1 if not available */ public static int getCallerHintsAtFrame(int frame) { - Deque stack = callerHintsStack.get(); + Deque stack = PerlRuntime.current().warningCallerHintsStack; if (stack.isEmpty()) { return -1; } @@ -292,7 +259,7 @@ public static int getCallerHintsAtFrame(int frame) { * @param hintHash A snapshot of the %^H hash elements */ public static void setCallSiteHintHash(java.util.Map hintHash) { - callSiteHintHash.set(hintHash != null ? new java.util.HashMap<>(hintHash) : new java.util.HashMap<>()); + PerlRuntime.current().warningCallSiteHintHash = hintHash != null ? new java.util.HashMap<>(hintHash) : new java.util.HashMap<>(); } /** @@ -309,7 +276,8 @@ public static void snapshotCurrentHintHash() { * Called by RuntimeCode.apply() before entering a subroutine. */ public static void pushCallerHintHash() { - callerHintHashStack.get().push(new java.util.HashMap<>(callSiteHintHash.get())); + PerlRuntime rt = PerlRuntime.current(); + rt.warningCallerHintHashStack.push(new java.util.HashMap<>(rt.warningCallSiteHintHash)); } /** @@ -317,7 +285,7 @@ public static void pushCallerHintHash() { * Called by RuntimeCode.apply() after a subroutine returns. */ public static void popCallerHintHash() { - Deque> stack = callerHintHashStack.get(); + Deque> stack = PerlRuntime.current().warningCallerHintHashStack; if (!stack.isEmpty()) { stack.pop(); } @@ -332,7 +300,7 @@ public static void popCallerHintHash() { * @return A copy of the %^H hash elements, or null if not available */ public static java.util.Map getCallerHintHashAtFrame(int frame) { - Deque> stack = callerHintHashStack.get(); + Deque> stack = PerlRuntime.current().warningCallerHintHashStack; if (stack.isEmpty()) { return null; } diff --git a/src/main/java/org/perlonjava/runtime/io/DirectoryIO.java b/src/main/java/org/perlonjava/runtime/io/DirectoryIO.java index ca99a0812..6c0d842ae 100644 --- a/src/main/java/org/perlonjava/runtime/io/DirectoryIO.java +++ b/src/main/java/org/perlonjava/runtime/io/DirectoryIO.java @@ -1,6 +1,7 @@ package org.perlonjava.runtime.io; import org.perlonjava.runtime.runtimetypes.*; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import java.nio.file.DirectoryStream; import java.nio.file.Path; @@ -41,7 +42,7 @@ public DirectoryIO(DirectoryStream directoryStream, String directoryPath) // Resolve and store absolute path Path path = Paths.get(directoryPath); if (!path.isAbsolute()) { - path = Paths.get(System.getProperty("user.dir"), directoryPath); + path = Paths.get(PerlRuntime.getCwd(), directoryPath); } this.absoluteDirectoryPath = path.toAbsolutePath().normalize(); } diff --git a/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java b/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java index 38fcd7720..43550f1e9 100644 --- a/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/PipeInputChannel.java @@ -4,6 +4,7 @@ import org.perlonjava.runtime.runtimetypes.RuntimeHash; import org.perlonjava.runtime.runtimetypes.RuntimeIO; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import org.perlonjava.runtime.runtimetypes.RuntimeScalarCache; import java.io.*; @@ -91,7 +92,7 @@ public PipeInputChannel(List commandArgs) throws IOException { */ private void setupProcess(ProcessBuilder processBuilder) throws IOException { // Set working directory to current directory - String userDir = System.getProperty("user.dir"); + String userDir = PerlRuntime.getCwd(); processBuilder.directory(new File(userDir)); // Copy %ENV to the subprocess environment @@ -106,9 +107,17 @@ private void setupProcess(ProcessBuilder processBuilder) throws IOException { // Create reader for stderr only errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + // Capture the parent thread's PerlRuntime so the background thread + // can access per-runtime STDERR handle for proper output routing + PerlRuntime parentRuntime = PerlRuntime.currentOrNull(); + // Start a thread to consume stderr and route through Perl STDERR handle // This ensures Perl-level redirections are honored (e.g., open STDERR, ">", $file) Thread errorThread = new Thread(() -> { + // Bind this thread to the same PerlRuntime as the parent + if (parentRuntime != null) { + PerlRuntime.setCurrent(parentRuntime); + } try (BufferedReader err = errorReader) { String line; while ((line = err.readLine()) != null) { diff --git a/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java b/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java index f6976261b..265edee6f 100644 --- a/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java +++ b/src/main/java/org/perlonjava/runtime/io/PipeOutputChannel.java @@ -4,6 +4,7 @@ import org.perlonjava.runtime.runtimetypes.RuntimeHash; import org.perlonjava.runtime.runtimetypes.RuntimeIO; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import org.perlonjava.runtime.runtimetypes.RuntimeScalarCache; import java.io.*; @@ -150,7 +151,7 @@ private void startProcessDirect(List commandArgs) throws IOException { */ private void setupProcess(ProcessBuilder processBuilder) throws IOException { // Set working directory to current directory - String userDir = System.getProperty("user.dir"); + String userDir = PerlRuntime.getCwd(); processBuilder.directory(new File(userDir)); // Copy %ENV to the subprocess environment @@ -166,9 +167,17 @@ private void setupProcess(ProcessBuilder processBuilder) throws IOException { outputReader = new BufferedReader(new InputStreamReader(process.getInputStream())); errorReader = new BufferedReader(new InputStreamReader(process.getErrorStream())); + // Capture the parent thread's PerlRuntime so background threads + // can access per-runtime STDOUT/STDERR handles for proper output routing + PerlRuntime parentRuntime = PerlRuntime.currentOrNull(); + // Start threads to consume stdout and stderr and route through Perl handles // This ensures Perl-level redirections are honored Thread outputThread = new Thread(() -> { + // Bind this thread to the same PerlRuntime as the parent + if (parentRuntime != null) { + PerlRuntime.setCurrent(parentRuntime); + } try (BufferedReader out = outputReader) { String line; while ((line = out.readLine()) != null) { @@ -191,6 +200,10 @@ private void setupProcess(ProcessBuilder processBuilder) throws IOException { outputThread.start(); Thread errorThread = new Thread(() -> { + // Bind this thread to the same PerlRuntime as the parent + if (parentRuntime != null) { + PerlRuntime.setCurrent(parentRuntime); + } try (BufferedReader err = errorReader) { String line; while ((line = err.readLine()) != null) { diff --git a/src/main/java/org/perlonjava/runtime/mro/C3.java b/src/main/java/org/perlonjava/runtime/mro/C3.java index 00ca9f93a..eaeba2685 100644 --- a/src/main/java/org/perlonjava/runtime/mro/C3.java +++ b/src/main/java/org/perlonjava/runtime/mro/C3.java @@ -13,7 +13,7 @@ public class C3 { */ public static List linearizeC3(String className) { String cacheKey = className + "::C3"; - List result = InheritanceResolver.linearizedClassesCache.get(cacheKey); + List result = InheritanceResolver.getLinearizedClassesCache().get(cacheKey); if (result == null) { Map> isaMap = new HashMap<>(); InheritanceResolver.populateIsaMap(className, isaMap); @@ -34,7 +34,7 @@ public static List linearizeC3(String className) { } } - InheritanceResolver.linearizedClassesCache.put(cacheKey, result); + InheritanceResolver.getLinearizedClassesCache().put(cacheKey, result); } return result; } diff --git a/src/main/java/org/perlonjava/runtime/mro/DFS.java b/src/main/java/org/perlonjava/runtime/mro/DFS.java index b5f8c4cc8..924fd526e 100644 --- a/src/main/java/org/perlonjava/runtime/mro/DFS.java +++ b/src/main/java/org/perlonjava/runtime/mro/DFS.java @@ -18,7 +18,7 @@ public static List linearizeDFS(String className) { // Check cache first String cacheKey = className + "::DFS"; - List cached = InheritanceResolver.linearizedClassesCache.get(cacheKey); + List cached = InheritanceResolver.getLinearizedClassesCache().get(cacheKey); if (cached != null) { if (DEBUG_DFS) { System.out.println("DEBUG DFS: Using cached result for " + className + ": " + cached); @@ -65,7 +65,7 @@ public static List linearizeDFS(String className) { } // Cache the result (store a copy to prevent external modifications) - InheritanceResolver.linearizedClassesCache.put(cacheKey, new ArrayList<>(result)); + InheritanceResolver.getLinearizedClassesCache().put(cacheKey, new ArrayList<>(result)); return result; } diff --git a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java index 81d79db45..b3c63357d 100644 --- a/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java +++ b/src/main/java/org/perlonjava/runtime/mro/InheritanceResolver.java @@ -10,20 +10,50 @@ * for method resolution and linearized class hierarchies to improve performance. */ public class InheritanceResolver { - // Cache for linearized class hierarchies - static final Map> linearizedClassesCache = new HashMap<>(); private static final boolean TRACE_METHOD_RESOLUTION = false; // Set to true for debugging - // Per-package MRO settings - private static final Map packageMRO = new HashMap<>(); - // Method resolution cache - private static final Map methodCache = new HashMap<>(); - // Cache for OverloadContext instances by blessing ID - private static final Map overloadContextCache = new HashMap<>(); - // Track ISA array states for change detection - private static final Map> isaStateCache = new HashMap<>(); - public static boolean autoloadEnabled = true; - // Default MRO algorithm - private static MROAlgorithm currentMRO = MROAlgorithm.DFS; + + // ---- Accessors delegating to PerlRuntime.current() ---- + + /** Returns the linearized classes cache from the current PerlRuntime. */ + static Map> getLinearizedClassesCache() { + return PerlRuntime.current().linearizedClassesCache; + } + + /** Returns the method cache from the current PerlRuntime. */ + private static Map getMethodCache() { + return PerlRuntime.current().methodCache; + } + + /** Returns the overload context cache from the current PerlRuntime. */ + private static Map getOverloadContextCache() { + return PerlRuntime.current().overloadContextCache; + } + + /** Returns the ISA state cache from the current PerlRuntime. */ + private static Map> getIsaStateCache() { + return PerlRuntime.current().isaStateCache; + } + + /** Returns the per-package MRO map from the current PerlRuntime. */ + private static Map getPackageMROMap() { + return PerlRuntime.current().packageMRO; + } + + // ---- autoloadEnabled getter/setter ---- + + public static boolean isAutoloadEnabled() { + return PerlRuntime.current().autoloadEnabled; + } + + public static void setAutoloadEnabled(boolean enabled) { + PerlRuntime.current().autoloadEnabled = enabled; + } + + // ---- currentMRO getter (used internally) ---- + + private static MROAlgorithm getCurrentMRO() { + return PerlRuntime.current().currentMRO; + } /** * Sets the default MRO algorithm. @@ -31,7 +61,7 @@ public class InheritanceResolver { * @param algorithm The MRO algorithm to use as default. */ public static void setDefaultMRO(MROAlgorithm algorithm) { - currentMRO = algorithm; + PerlRuntime.current().currentMRO = algorithm; invalidateCache(); } @@ -42,7 +72,7 @@ public static void setDefaultMRO(MROAlgorithm algorithm) { * @param algorithm The MRO algorithm to use for this package. */ public static void setPackageMRO(String packageName, MROAlgorithm algorithm) { - packageMRO.put(packageName, algorithm); + getPackageMROMap().put(packageName, algorithm); invalidateCache(); } @@ -53,7 +83,7 @@ public static void setPackageMRO(String packageName, MROAlgorithm algorithm) { * @return The MRO algorithm for the package, or the default if not set. */ public static MROAlgorithm getPackageMRO(String packageName) { - return packageMRO.getOrDefault(packageName, currentMRO); + return getPackageMROMap().getOrDefault(packageName, getCurrentMRO()); } /** @@ -63,19 +93,21 @@ public static MROAlgorithm getPackageMRO(String packageName) { * @return A list of class names in the order of method resolution. */ public static List linearizeHierarchy(String className) { + PerlRuntime rt = PerlRuntime.current(); // Check if ISA has changed and invalidate cache if needed - if (hasIsaChanged(className)) { - invalidateCacheForClass(className); + if (hasIsaChanged(className, rt)) { + invalidateCacheForClass(className, rt); } + Map> cache = rt.linearizedClassesCache; // Check cache first - List cached = linearizedClassesCache.get(className); + List cached = cache.get(className); if (cached != null) { // Return a copy of the cached list to prevent modification of the cached version return new ArrayList<>(cached); } - MROAlgorithm mro = getPackageMRO(className); + MROAlgorithm mro = rt.packageMRO.getOrDefault(className, rt.currentMRO); List result; switch (mro) { @@ -90,7 +122,7 @@ public static List linearizeHierarchy(String className) { } // Cache the result (store a copy to prevent external modifications) - linearizedClassesCache.put(className, new ArrayList<>(result)); + cache.put(className, new ArrayList<>(result)); return result; } @@ -98,6 +130,10 @@ public static List linearizeHierarchy(String className) { * Checks if the @ISA array for a class has changed since last cached. */ private static boolean hasIsaChanged(String className) { + return hasIsaChanged(className, PerlRuntime.current()); + } + + private static boolean hasIsaChanged(String className, PerlRuntime rt) { RuntimeArray isaArray = GlobalVariable.getGlobalArray(className + "::ISA"); // Build current ISA list @@ -109,11 +145,12 @@ private static boolean hasIsaChanged(String className) { } } - List cachedIsa = isaStateCache.get(className); + Map> isCache = rt.isaStateCache; + List cachedIsa = isCache.get(className); // If ISA changed, update cache and return true if (!currentIsa.equals(cachedIsa)) { - isaStateCache.put(className, currentIsa); + isCache.put(className, currentIsa); return true; } @@ -124,12 +161,19 @@ private static boolean hasIsaChanged(String className) { * Invalidate cache for a specific class and its dependents. */ private static void invalidateCacheForClass(String className) { + invalidateCacheForClass(className, PerlRuntime.current()); + } + + private static void invalidateCacheForClass(String className, PerlRuntime rt) { + Map> linCache = rt.linearizedClassesCache; + Map mCache = rt.methodCache; + // Remove exact class and subclasses from linearization cache - linearizedClassesCache.remove(className); - linearizedClassesCache.entrySet().removeIf(entry -> entry.getKey().startsWith(className + "::")); + linCache.remove(className); + linCache.entrySet().removeIf(entry -> entry.getKey().startsWith(className + "::")); // Remove from method cache (entries for this class and subclasses) - methodCache.entrySet().removeIf(entry -> + mCache.entrySet().removeIf(entry -> entry.getKey().startsWith(className + "::") || entry.getKey().contains("::" + className + "::")); // Could also notify dependents here if we had that information @@ -140,10 +184,11 @@ private static void invalidateCacheForClass(String className) { * This should be called whenever the class hierarchy or method definitions change. */ public static void invalidateCache() { - methodCache.clear(); - linearizedClassesCache.clear(); - overloadContextCache.clear(); - isaStateCache.clear(); + PerlRuntime rt = PerlRuntime.current(); + rt.methodCache.clear(); + rt.linearizedClassesCache.clear(); + rt.overloadContextCache.clear(); + rt.isaStateCache.clear(); // Also clear the inline method cache in RuntimeCode RuntimeCode.clearInlineMethodCache(); // Clear DESTROY-related caches (destroyClasses BitSet and destroyMethodCache) @@ -157,7 +202,7 @@ public static void invalidateCache() { * @return The cached OverloadContext, or null if not found. */ public static OverloadContext getCachedOverloadContext(int blessId) { - return overloadContextCache.get(blessId); + return getOverloadContextCache().get(blessId); } /** @@ -167,7 +212,7 @@ public static OverloadContext getCachedOverloadContext(int blessId) { * @param context The OverloadContext to cache (can be null to indicate no overloading). */ public static void cacheOverloadContext(int blessId, OverloadContext context) { - overloadContextCache.put(blessId, context); + getOverloadContextCache().put(blessId, context); } /** @@ -177,7 +222,7 @@ public static void cacheOverloadContext(int blessId, OverloadContext context) { * @return The cached RuntimeScalar representing the method, or null if not found. */ public static RuntimeScalar getCachedMethod(String normalizedMethodName) { - return methodCache.get(normalizedMethodName); + return getMethodCache().get(normalizedMethodName); } /** @@ -187,7 +232,7 @@ public static RuntimeScalar getCachedMethod(String normalizedMethodName) { * @param method The RuntimeScalar representing the method to cache. */ public static void cacheMethod(String normalizedMethodName, RuntimeScalar method) { - methodCache.put(normalizedMethodName, method); + getMethodCache().put(normalizedMethodName, method); } /** @@ -269,6 +314,8 @@ private static void populateIsaMapHelper(String className, * @return RuntimeScalar representing the found method, or null if not found */ public static RuntimeScalar findMethodInHierarchy(String methodName, String perlClassName, String cacheKey, int startFromIndex) { + PerlRuntime rt = PerlRuntime.current(); + if (TRACE_METHOD_RESOLUTION) { System.err.println("TRACE InheritanceResolver.findMethodInHierarchy:"); System.err.println(" methodName: '" + methodName + "'"); @@ -288,17 +335,18 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl } // Check if ISA changed for this class - if so, invalidate relevant caches - if (hasIsaChanged(perlClassName)) { - invalidateCacheForClass(perlClassName); + if (hasIsaChanged(perlClassName, rt)) { + invalidateCacheForClass(perlClassName, rt); } // Check the method cache - handles both found and not-found cases - if (methodCache.containsKey(cacheKey)) { + Map mCache = rt.methodCache; + if (mCache.containsKey(cacheKey)) { if (TRACE_METHOD_RESOLUTION) { - System.err.println(" Found in cache: " + (methodCache.get(cacheKey) != null ? "YES" : "NULL")); + System.err.println(" Found in cache: " + (mCache.get(cacheKey) != null ? "YES" : "NULL")); System.err.flush(); } - return methodCache.get(cacheKey); + return mCache.get(cacheKey); } // Get the linearized inheritance hierarchy using the appropriate MRO @@ -328,7 +376,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl if (!codeRef.getDefinedBoolean()) { continue; } - cacheMethod(cacheKey, codeRef); + mCache.put(cacheKey, codeRef); if (TRACE_METHOD_RESOLUTION) { System.err.println(" FOUND method!"); System.err.flush(); @@ -340,7 +388,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl // Second pass — method not found anywhere, check AUTOLOAD in class hierarchy. // This matches Perl semantics: AUTOLOAD is only tried after the full MRO // search (including UNIVERSAL) fails to find the method. - if (autoloadEnabled && !methodName.startsWith("(")) { + if (rt.autoloadEnabled && !methodName.startsWith("(")) { for (int i = startFromIndex; i < linearizedClasses.size(); i++) { String className = linearizedClasses.get(i); String effectiveClassName = GlobalVariable.resolveStashAlias(className); @@ -359,7 +407,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl } else { autoloadCode.autoloadVariableName = autoloadName; } - cacheMethod(cacheKey, autoload); + mCache.put(cacheKey, autoload); return autoload; } } @@ -367,7 +415,7 @@ public static RuntimeScalar findMethodInHierarchy(String methodName, String perl } // Cache the fact that method was not found (using null) - methodCache.put(cacheKey, null); + mCache.put(cacheKey, null); return null; } diff --git a/src/main/java/org/perlonjava/runtime/operators/Directory.java b/src/main/java/org/perlonjava/runtime/operators/Directory.java index abe071f92..80e56d6d3 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Directory.java +++ b/src/main/java/org/perlonjava/runtime/operators/Directory.java @@ -93,7 +93,8 @@ public static RuntimeScalar chdir(RuntimeScalar runtimeScalar) { if (absoluteDir.exists() && absoluteDir.isDirectory()) { // Normalize the path to remove redundant . and .. components - System.setProperty("user.dir", absoluteDir.toPath().normalize().toString()); + // Update per-runtime CWD (not the JVM-global user.dir property) + PerlRuntime.current().cwd = absoluteDir.toPath().normalize().toString(); return scalarTrue; } else { // Set errno to ENOENT (No such file or directory) diff --git a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java index 1f751551b..b08b96ef3 100644 --- a/src/main/java/org/perlonjava/runtime/operators/IOOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/IOOperator.java @@ -43,7 +43,7 @@ public class IOOperator { public static RuntimeScalar select(RuntimeList runtimeList, int ctx) { if (runtimeList.isEmpty()) { // select (returns current filehandle) - return new RuntimeScalar(RuntimeIO.selectedHandle); + return new RuntimeScalar(RuntimeIO.getSelectedHandle()); } if (runtimeList.size() == 4) { // select RBITS,WBITS,EBITS,TIMEOUT (syscall) @@ -82,9 +82,9 @@ public static RuntimeScalar select(RuntimeList runtimeList, int ctx) { } } // select FILEHANDLE (returns/sets current filehandle) - RuntimeScalar fh = new RuntimeScalar(RuntimeIO.selectedHandle); - RuntimeIO.selectedHandle = runtimeList.getFirst().getRuntimeIO(); - RuntimeIO.lastAccesseddHandle = RuntimeIO.selectedHandle; + RuntimeScalar fh = new RuntimeScalar(RuntimeIO.getSelectedHandle()); + RuntimeIO.setSelectedHandle(runtimeList.getFirst().getRuntimeIO()); + RuntimeIO.setLastAccessedHandle(RuntimeIO.getSelectedHandle()); return fh; } @@ -405,7 +405,7 @@ public static RuntimeScalar seek(RuntimeScalar fileHandle, RuntimeList runtimeLi whence = runtimeList.elements.get(1).scalar().getInt(); } - RuntimeIO.lastAccesseddHandle = runtimeIO; + RuntimeIO.setLastAccessedHandle(runtimeIO); return runtimeIO.ioHandle.seek(position, whence); } else { return RuntimeIO.handleIOError("No file handle available for seek"); @@ -443,7 +443,7 @@ public static RuntimeScalar tell(RuntimeScalar fileHandle) { // fall back to the last accessed handle like Perl does. if (fh == null) { if (argless) { - RuntimeIO last = RuntimeIO.lastAccesseddHandle; + RuntimeIO last = RuntimeIO.getLastAccessedHandle(); if (last != null) { return last.tell(); } @@ -455,7 +455,7 @@ public static RuntimeScalar tell(RuntimeScalar fileHandle) { } // Update the last accessed filehandle - RuntimeIO.lastAccesseddHandle = fh; + RuntimeIO.setLastAccessedHandle(fh); if (fh instanceof TieHandle tieHandle) { return TieHandle.tiedTell(tieHandle); @@ -934,7 +934,7 @@ public static RuntimeScalar eof(RuntimeScalar fileHandle) { // Handle undefined or invalid filehandle if (fh == null) { if (argless) { - RuntimeIO last = RuntimeIO.lastAccesseddHandle; + RuntimeIO last = RuntimeIO.getLastAccessedHandle(); if (last != null) { return last.eof(); } @@ -964,7 +964,7 @@ public static RuntimeScalar eof(RuntimeList runtimeList, RuntimeScalar fileHandl // Handle undefined or invalid filehandle if (fh == null) { if (argless) { - RuntimeIO last = RuntimeIO.lastAccesseddHandle; + RuntimeIO last = RuntimeIO.getLastAccessedHandle(); if (last != null) { return last.eof(); } @@ -1497,7 +1497,7 @@ public static boolean applyFilePermissions(Path path, int mode) { */ public static RuntimeScalar write(int ctx, RuntimeBase... args) { String formatName; - RuntimeIO fh = RuntimeIO.stdout; // Default output handle + RuntimeIO fh = RuntimeIO.getStdout(); // Default output handle if (args.length == 0) { // No arguments: write() - use STDOUT format to STDOUT handle @@ -1515,11 +1515,11 @@ public static RuntimeScalar write(int ctx, RuntimeBase... args) { if (argFh != null) { // Argument is a filehandle - determine format name from handle fh = argFh; - if (fh == RuntimeIO.stdout) { + if (fh == RuntimeIO.getStdout()) { formatName = "STDOUT"; - } else if (fh == RuntimeIO.stderr) { + } else if (fh == RuntimeIO.getStderr()) { formatName = "STDERR"; - } else if (fh == RuntimeIO.stdin) { + } else if (fh == RuntimeIO.getStdin()) { formatName = "STDIN"; } else { formatName = "STDOUT"; // Default fallback @@ -2634,11 +2634,11 @@ private static RuntimeIO findFileHandleByDescriptor(int fd) { // Handle standard file descriptors switch (fd) { case 0: // STDIN - return RuntimeIO.stdin; + return RuntimeIO.getStdin(); case 1: // STDOUT - return RuntimeIO.stdout; + return RuntimeIO.getStdout(); case 2: // STDERR - return RuntimeIO.stderr; + return RuntimeIO.getStderr(); default: // Check the RuntimeIO fileno registry (used by all file/pipe/socket handles) RuntimeIO fromRegistry = RuntimeIO.getByFileno(fd); @@ -2714,9 +2714,9 @@ public static RuntimeIO openFileHandleDup(String fileName, String mode) { if (sourceHandle == null || sourceHandle.ioHandle == null) { // Last resort: try static fields for standard handles switch (fileName.toUpperCase()) { - case "STDIN": sourceHandle = RuntimeIO.stdin; break; - case "STDOUT": sourceHandle = RuntimeIO.stdout; break; - case "STDERR": sourceHandle = RuntimeIO.stderr; break; + case "STDIN": sourceHandle = RuntimeIO.getStdin(); break; + case "STDOUT": sourceHandle = RuntimeIO.getStdout(); break; + case "STDERR": sourceHandle = RuntimeIO.getStderr(); break; default: throw new PerlCompilerException("Unsupported filehandle duplication: " + fileName); } diff --git a/src/main/java/org/perlonjava/runtime/operators/Readline.java b/src/main/java/org/perlonjava/runtime/operators/Readline.java index a4b02a21a..1f48a23dc 100644 --- a/src/main/java/org/perlonjava/runtime/operators/Readline.java +++ b/src/main/java/org/perlonjava/runtime/operators/Readline.java @@ -53,7 +53,7 @@ public static RuntimeScalar readline(RuntimeIO runtimeIO) { } // Set this as the last accessed handle for $. (INPUT_LINE_NUMBER) special variable - RuntimeIO.lastAccesseddHandle = runtimeIO; + RuntimeIO.setLastAccessedHandle(runtimeIO); // Get the input record separator (equivalent to Perl's $/) RuntimeScalar rsScalar = getGlobalVariable("main::/"); diff --git a/src/main/java/org/perlonjava/runtime/operators/ScalarGlobOperator.java b/src/main/java/org/perlonjava/runtime/operators/ScalarGlobOperator.java index 24e0c3173..b6f5551c9 100644 --- a/src/main/java/org/perlonjava/runtime/operators/ScalarGlobOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/ScalarGlobOperator.java @@ -1,6 +1,7 @@ package org.perlonjava.runtime.operators; import org.perlonjava.runtime.runtimetypes.*; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import java.io.File; import java.util.*; @@ -225,7 +226,7 @@ private static void globRecursive(ScalarGlobOperator scalarGlobOperator, String startSegment = 1; } } else { - startDir = new File(System.getProperty("user.dir")); + startDir = new File(PerlRuntime.getCwd()); prefix = ""; startSegment = 0; } @@ -346,7 +347,7 @@ private PathComponents extractPathComponents(String normalizedPattern, boolean i filePattern = normalizedPattern.substring(lastSep + 1); } else { // No directory separator - use current directory - baseDir = new File(System.getProperty("user.dir")); + baseDir = new File(PerlRuntime.getCwd()); } return new PathComponents(baseDir, filePattern, hasDirectory, directoryPart); diff --git a/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java b/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java index e9f1c7f4d..4a9817542 100644 --- a/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java +++ b/src/main/java/org/perlonjava/runtime/operators/SystemOperator.java @@ -187,7 +187,7 @@ private static CommandResult executeCommand(String command, boolean captureOutpu } ProcessBuilder processBuilder = new ProcessBuilder(shellCommand); - String userDir = System.getProperty("user.dir"); + String userDir = PerlRuntime.getCwd(); processBuilder.directory(new File(userDir)); // Copy %ENV to the subprocess environment @@ -287,7 +287,7 @@ private static CommandResult executeCommandDirect(List commandArgs) { flushAllHandles(); ProcessBuilder processBuilder = new ProcessBuilder(commandArgs); - String userDir = System.getProperty("user.dir"); + String userDir = PerlRuntime.getCwd(); processBuilder.directory(new File(userDir)); // Copy %ENV to the subprocess environment @@ -339,7 +339,7 @@ private static CommandResult executeCommandDirectCapture(List commandArg flushAllHandles(); ProcessBuilder processBuilder = new ProcessBuilder(commandArgs); - String userDir = System.getProperty("user.dir"); + String userDir = PerlRuntime.getCwd(); processBuilder.directory(new File(userDir)); // Copy %ENV to the subprocess environment @@ -655,7 +655,7 @@ private static RuntimeScalar completeForkOpen(List flattenedArgs, boolea // Run command and capture output ProcessBuilder processBuilder = new ProcessBuilder(command); - processBuilder.directory(new File(System.getProperty("user.dir"))); + processBuilder.directory(new File(PerlRuntime.getCwd())); copyPerlEnvToProcessBuilder(processBuilder); processBuilder.redirectErrorStream(false); // Keep stderr separate @@ -716,7 +716,7 @@ private static int execCommand(String command) throws IOException, InterruptedEx } ProcessBuilder processBuilder = new ProcessBuilder(shellCommand); - String userDir = System.getProperty("user.dir"); + String userDir = PerlRuntime.getCwd(); processBuilder.directory(new File(userDir)); // Copy %ENV to the subprocess environment @@ -739,7 +739,7 @@ private static int execCommand(String command) throws IOException, InterruptedEx */ private static int execCommandDirect(List commandArgs) throws IOException, InterruptedException { ProcessBuilder processBuilder = new ProcessBuilder(commandArgs); - String userDir = System.getProperty("user.dir"); + String userDir = PerlRuntime.getCwd(); processBuilder.directory(new File(userDir)); // Copy %ENV to the subprocess environment diff --git a/src/main/java/org/perlonjava/runtime/operators/TieOperators.java b/src/main/java/org/perlonjava/runtime/operators/TieOperators.java index 62acc9bd0..8f8694311 100644 --- a/src/main/java/org/perlonjava/runtime/operators/TieOperators.java +++ b/src/main/java/org/perlonjava/runtime/operators/TieOperators.java @@ -100,8 +100,8 @@ public static RuntimeScalar tie(int ctx, RuntimeBase... scalars) { glob.IO.value = tieHandle; // Update selectedHandle so that `print` without explicit filehandle // goes through the tied handle (e.g., Test2::Plugin::IOEvents) - if (previousValue == RuntimeIO.selectedHandle) { - RuntimeIO.selectedHandle = tieHandle; + if (previousValue == RuntimeIO.getSelectedHandle()) { + RuntimeIO.setSelectedHandle(tieHandle); } } default -> { @@ -178,8 +178,8 @@ public static RuntimeScalar untie(int ctx, RuntimeBase... scalars) { IO.type = 0; // XXX there is no type defined for IO handles IO.value = previousValue; // Restore selectedHandle if it pointed to the tied handle - if (currentTieHandle == RuntimeIO.selectedHandle) { - RuntimeIO.selectedHandle = previousValue; + if (currentTieHandle == RuntimeIO.getSelectedHandle()) { + RuntimeIO.setSelectedHandle(previousValue); } currentTieHandle.releaseTiedObject(); } diff --git a/src/main/java/org/perlonjava/runtime/operators/WarnDie.java b/src/main/java/org/perlonjava/runtime/operators/WarnDie.java index 7e80418e3..1ce8b3a49 100644 --- a/src/main/java/org/perlonjava/runtime/operators/WarnDie.java +++ b/src/main/java/org/perlonjava/runtime/operators/WarnDie.java @@ -81,7 +81,7 @@ public static RuntimeScalar catchEval(Throwable e) { // By the time we reach catchEval(), evalDepth has already been decremented // by the eval catch block, but the handler should see $^S=1 since we are // conceptually still inside eval (Perl 5 calls the handler before unwinding). - RuntimeCode.evalDepth++; + RuntimeCode.incrementEvalDepth(); try { RuntimeCode.apply(sigHandler, args, RuntimeContextType.SCALAR); } catch (Throwable handlerException) { @@ -99,7 +99,7 @@ public static RuntimeScalar catchEval(Throwable e) { err.set(new RuntimeScalar(ErrorMessageUtil.stringifyException(handlerException))); } } finally { - RuntimeCode.evalDepth--; + RuntimeCode.decrementEvalDepth(); // Restore $SIG{__DIE__} DynamicVariableManager.popToLocalLevel(level); } @@ -505,8 +505,8 @@ public static RuntimeScalar exit(RuntimeScalar runtimeScalar) { * @return String with filehandle context (including leading ", "), or null if no context */ public static String getFilehandleContext() { - if (RuntimeIO.lastAccesseddHandle != null && RuntimeIO.lastAccesseddHandle.currentLineNumber > 0) { - String handleName = findFilehandleName(RuntimeIO.lastAccesseddHandle); + if (RuntimeIO.getLastAccessedHandle() != null && RuntimeIO.getLastAccessedHandle().currentLineNumber > 0) { + String handleName = findFilehandleName(RuntimeIO.getLastAccessedHandle()); if (handleName != null) { // Perl 5 uses "line" only when $/ is exactly "\n". // Everything else (undef, "", custom separator, ref) uses "chunk". @@ -519,7 +519,7 @@ public static String getFilehandleContext() { } catch (Exception ignored) { // Default to "chunk" if we can't read $/ } - return ", <" + handleName + "> " + unit + " " + RuntimeIO.lastAccesseddHandle.currentLineNumber; + return ", <" + handleName + "> " + unit + " " + RuntimeIO.getLastAccessedHandle().currentLineNumber; } } return null; @@ -543,6 +543,6 @@ private static String findFilehandleName(RuntimeIO handle) { return name; } // Fall back to the variable name set during the last readline (e.g., "$f") - return RuntimeIO.lastReadlineHandleName; + return RuntimeIO.getLastReadlineHandleName(); } } \ No newline at end of file diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Attributes.java b/src/main/java/org/perlonjava/runtime/perlmodule/Attributes.java index 57c105eaa..60e088941 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Attributes.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Attributes.java @@ -369,12 +369,12 @@ public static void runtimeDispatchModifyCodeAttributes(String packageName, Runti RuntimeArray.push(canArgs, new RuntimeScalar(packageName)); RuntimeArray.push(canArgs, new RuntimeScalar("MODIFY_CODE_ATTRIBUTES")); - InheritanceResolver.autoloadEnabled = false; + InheritanceResolver.setAutoloadEnabled(false); RuntimeList codeList; try { codeList = Universal.can(canArgs, RuntimeContextType.SCALAR); } finally { - InheritanceResolver.autoloadEnabled = true; + InheritanceResolver.setAutoloadEnabled(true); } boolean hasHandler = codeList.size() == 1 && codeList.getFirst().getBoolean(); @@ -468,12 +468,12 @@ public static void runtimeDispatchModifyVariableAttributes( RuntimeArray.push(canArgs, new RuntimeScalar(packageName)); RuntimeArray.push(canArgs, new RuntimeScalar(modifyMethod)); - InheritanceResolver.autoloadEnabled = false; + InheritanceResolver.setAutoloadEnabled(false); RuntimeList codeList; try { codeList = Universal.can(canArgs, RuntimeContextType.SCALAR); } finally { - InheritanceResolver.autoloadEnabled = true; + InheritanceResolver.setAutoloadEnabled(true); } boolean hasHandler = codeList.size() == 1 && codeList.getFirst().getBoolean(); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java index 82863be92..cb875c479 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/FileSpec.java @@ -1,5 +1,6 @@ package org.perlonjava.runtime.perlmodule; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import org.perlonjava.runtime.runtimetypes.GlobalVariable; import org.perlonjava.runtime.runtimetypes.RuntimeArray; import org.perlonjava.runtime.runtimetypes.RuntimeHash; @@ -511,14 +512,14 @@ public static RuntimeList abs2rel(RuntimeArray args, int ctx) { throw new IllegalStateException("Bad number of arguments for abs2rel() method"); } String path = args.get(1).toString(); - String base = args.size() == 3 ? args.get(2).toString() : System.getProperty("user.dir"); + String base = args.size() == 3 ? args.get(2).toString() : PerlRuntime.getCwd(); // Ensure both paths are absolute before relativizing (like Perl does) - // Note: We use user.dir explicitly because Java's Path.toAbsolutePath() - // doesn't respect System.setProperty("user.dir", ...) set by chdir() + // Note: We use PerlRuntime.getCwd() explicitly because Java's Path.toAbsolutePath() + // doesn't respect per-runtime cwd set by chdir() Path pathObj = Paths.get(path); Path baseObj = Paths.get(base); - String userDir = System.getProperty("user.dir"); + String userDir = PerlRuntime.getCwd(); if (!pathObj.isAbsolute()) { pathObj = Paths.get(userDir).resolve(pathObj).normalize(); @@ -543,7 +544,7 @@ public static RuntimeList rel2abs(RuntimeArray args, int ctx) { throw new IllegalStateException("Bad number of arguments for rel2abs() method"); } String path = args.get(1).toString(); - String base = args.size() == 3 ? args.get(2).toString() : System.getProperty("user.dir"); + String base = args.size() == 3 ? args.get(2).toString() : PerlRuntime.getCwd(); // PerlOnJava: jar: paths are already absolute, return as-is if (path.startsWith("jar:")) { @@ -559,7 +560,7 @@ public static RuntimeList rel2abs(RuntimeArray args, int ctx) { // If base is relative, resolve it against current working directory first Path basePath = Paths.get(base); if (!basePath.isAbsolute()) { - basePath = Paths.get(System.getProperty("user.dir")).resolve(basePath); + basePath = Paths.get(PerlRuntime.getCwd()).resolve(basePath); } // For relative paths, resolve against the base directory diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/IPCOpen3.java b/src/main/java/org/perlonjava/runtime/perlmodule/IPCOpen3.java index b55237d3d..dd8a96e21 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/IPCOpen3.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/IPCOpen3.java @@ -5,6 +5,7 @@ import org.perlonjava.runtime.nativ.NativeUtils; import org.perlonjava.runtime.operators.WaitpidOperator; import org.perlonjava.runtime.runtimetypes.*; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import java.io.File; import java.io.InputStream; @@ -121,7 +122,7 @@ public static RuntimeList _open3(RuntimeArray args, int ctx) { } ProcessBuilder processBuilder = new ProcessBuilder(command); - String userDir = System.getProperty("user.dir"); + String userDir = PerlRuntime.getCwd(); processBuilder.directory(new File(userDir)); // Copy %ENV to the subprocess @@ -349,7 +350,7 @@ public static RuntimeList _open2(RuntimeArray args, int ctx) { } ProcessBuilder processBuilder = new ProcessBuilder(command); - String userDir = System.getProperty("user.dir"); + String userDir = PerlRuntime.getCwd(); processBuilder.directory(new File(userDir)); // Copy %ENV to the subprocess diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java b/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java index 094f1d2e7..538c1735e 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Internals.java @@ -216,7 +216,7 @@ public static RuntimeList isInitializedStateVariable(RuntimeArray args, int ctx) * @return RuntimeScalar with the current working directory path */ public static RuntimeList getcwd(RuntimeArray args, int ctx) { - return new RuntimeScalar(System.getProperty("user.dir")).getList(); + return new RuntimeScalar(PerlRuntime.getCwd()).getList(); } /** @@ -233,7 +233,7 @@ public static RuntimeList abs_path(RuntimeArray args, int ctx) { try { java.io.File file = new java.io.File(path); if (!file.isAbsolute()) { - file = new java.io.File(System.getProperty("user.dir"), path); + file = new java.io.File(PerlRuntime.getCwd(), path); } if (!file.exists()) { return new RuntimeScalar().getList(); // return undef diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java b/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java index b516c2d0d..a536b24c8 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Mro.java @@ -12,15 +12,11 @@ * The Mro class provides Perl's mro (Method Resolution Order) module functionality. * It allows switching between different MRO algorithms (DFS and C3) and provides * utilities for introspecting the inheritance hierarchy. + * + * All mutable state is per-PerlRuntime for multiplicity thread-safety. */ public class Mro extends PerlModuleBase { - // Package generation counters - private static final Map packageGenerations = new HashMap<>(); - - // Reverse ISA cache (which classes inherit from a given class) - private static final Map> isaRevCache = new HashMap<>(); - /** * Constructor for Mro. * Initializes the module with the name "mro". @@ -233,27 +229,28 @@ public static RuntimeList get_mro(RuntimeArray args, int ctx) { * Builds the reverse ISA cache by dynamically scanning all packages with @ISA arrays. */ private static void buildIsaRevCache() { - isaRevCache.clear(); + Map> cache = PerlRuntime.current().mroIsaRevCache; + cache.clear(); // Dynamically scan all @ISA arrays from global variables Map allIsaArrays = GlobalVariable.getAllIsaArrays(); for (String key : allIsaArrays.keySet()) { // Key format: "ClassName::ISA" → extract class name String className = key.substring(0, key.length() - 5); // remove "::ISA" - buildIsaRevForClass(className); + buildIsaRevForClass(className, cache); } } /** * Build reverse ISA relationships for a specific class. */ - private static void buildIsaRevForClass(String className) { + private static void buildIsaRevForClass(String className, Map> cache) { if (GlobalVariable.existsGlobalArray(className + "::ISA")) { RuntimeArray isaArray = GlobalVariable.getGlobalArray(className + "::ISA"); for (RuntimeBase parent : isaArray.elements) { String parentName = parent.toString(); if (parentName != null && !parentName.isEmpty()) { - isaRevCache.computeIfAbsent(parentName, k -> new HashSet<>()).add(className); + cache.computeIfAbsent(parentName, k -> new HashSet<>()).add(className); } } } @@ -272,14 +269,15 @@ public static RuntimeList get_isarev(RuntimeArray args, int ctx) { } String className = args.get(0).toString(); + Map> cache = PerlRuntime.current().mroIsaRevCache; // Build reverse ISA cache if empty - if (isaRevCache.isEmpty()) { + if (cache.isEmpty()) { buildIsaRevCache(); } RuntimeArray result = new RuntimeArray(); - Set inheritors = isaRevCache.getOrDefault(className, new HashSet<>()); + Set inheritors = cache.getOrDefault(className, new HashSet<>()); // Add all classes that inherit from this one, including indirectly Set allInheritors = new HashSet<>(); @@ -301,7 +299,8 @@ private static void collectAllInheritors(String className, Set result, S } visited.add(className); - Set directInheritors = isaRevCache.getOrDefault(className, new HashSet<>()); + Map> cache = PerlRuntime.current().mroIsaRevCache; + Set directInheritors = cache.getOrDefault(className, new HashSet<>()); for (String inheritor : directInheritors) { result.add(inheritor); collectAllInheritors(inheritor, result, visited); @@ -347,11 +346,12 @@ public static RuntimeList is_universal(RuntimeArray args, int ctx) { * @return A RuntimeList. */ public static RuntimeList invalidate_all_method_caches(RuntimeArray args, int ctx) { + PerlRuntime rt = PerlRuntime.current(); InheritanceResolver.invalidateCache(); - isaRevCache.clear(); + rt.mroIsaRevCache.clear(); // Increment all package generations - for (String pkg : new HashSet<>(packageGenerations.keySet())) { + for (String pkg : new HashSet<>(rt.mroPackageGenerations.keySet())) { incrementPackageGeneration(pkg); } @@ -371,16 +371,17 @@ public static RuntimeList method_changed_in(RuntimeArray args, int ctx) { } String className = args.get(0).toString(); + Map> cache = PerlRuntime.current().mroIsaRevCache; // Invalidate the method cache InheritanceResolver.invalidateCache(); // Build isarev if needed and invalidate dependent classes - if (isaRevCache.isEmpty()) { + if (cache.isEmpty()) { buildIsaRevCache(); } - Set dependents = isaRevCache.getOrDefault(className, new HashSet<>()); + Set dependents = cache.getOrDefault(className, new HashSet<>()); dependents.add(className); // Include the class itself // Increment package generation for all dependent classes @@ -391,9 +392,6 @@ public static RuntimeList method_changed_in(RuntimeArray args, int ctx) { return new RuntimeList(); } - // Cached @ISA state per package — used to detect @ISA changes in get_pkg_gen - private static final Map> pkgGenIsaState = new HashMap<>(); - /** * Returns the package generation number. * @@ -407,6 +405,7 @@ public static RuntimeList get_pkg_gen(RuntimeArray args, int ctx) { } String className = args.get(0).toString(); + PerlRuntime rt = PerlRuntime.current(); // Lazily detect @ISA changes and auto-increment pkg_gen if (GlobalVariable.existsGlobalArray(className + "::ISA")) { @@ -418,15 +417,15 @@ public static RuntimeList get_pkg_gen(RuntimeArray args, int ctx) { currentIsa.add(parentName); } } - List cachedIsa = pkgGenIsaState.get(className); + List cachedIsa = rt.mroPkgGenIsaState.get(className); if (cachedIsa != null && !currentIsa.equals(cachedIsa)) { incrementPackageGeneration(className); } - pkgGenIsaState.put(className, currentIsa); + rt.mroPkgGenIsaState.put(className, currentIsa); } // Return current generation, starting from 1 - Integer gen = packageGenerations.getOrDefault(className, 1); + Integer gen = rt.mroPackageGenerations.getOrDefault(className, 1); return new RuntimeScalar(gen).getList(); } @@ -437,7 +436,8 @@ public static RuntimeList get_pkg_gen(RuntimeArray args, int ctx) { * @param packageName The name of the package. */ public static void incrementPackageGeneration(String packageName) { - Integer current = packageGenerations.getOrDefault(packageName, 1); - packageGenerations.put(packageName, current + 1); + Map generations = PerlRuntime.current().mroPackageGenerations; + Integer current = generations.getOrDefault(packageName, 1); + generations.put(packageName, current + 1); } } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java b/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java index d19decafe..19bfa77ef 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/POSIX.java @@ -391,7 +391,7 @@ public static RuntimeList getegid(RuntimeArray args, int ctx) { } public static RuntimeList getcwd(RuntimeArray args, int ctx) { - return new RuntimeScalar(System.getProperty("user.dir")).getList(); + return new RuntimeScalar(PerlRuntime.getCwd()).getList(); } public static RuntimeList strerror(RuntimeArray args, int ctx) { @@ -514,9 +514,9 @@ public static RuntimeList dup2(RuntimeArray args, int ctx) { // If targeting fd 0/1/2, update the static handles switch (newFd) { - case 0 -> RuntimeIO.stdin = target; - case 1 -> RuntimeIO.stdout = target; - case 2 -> RuntimeIO.stderr = target; + case 0 -> RuntimeIO.setStdin(target); + case 1 -> RuntimeIO.setStdout(target); + case 2 -> RuntimeIO.setStderr(target); } return new RuntimeScalar(newFd == 0 ? "0 but true" : (Object) newFd).getList(); @@ -529,9 +529,9 @@ private static RuntimeIO lookupByFd(int fd) { RuntimeIO rio = RuntimeIO.getByFileno(fd); if (rio != null) return rio; return switch (fd) { - case 0 -> RuntimeIO.stdin; - case 1 -> RuntimeIO.stdout; - case 2 -> RuntimeIO.stderr; + case 0 -> RuntimeIO.getStdin(); + case 1 -> RuntimeIO.getStdout(); + case 2 -> RuntimeIO.getStderr(); default -> null; }; } diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Subs.java b/src/main/java/org/perlonjava/runtime/perlmodule/Subs.java index c676ff5d2..1c226303d 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Subs.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Subs.java @@ -46,7 +46,7 @@ public static RuntimeList importSubs(RuntimeArray args, int ctx) { String variableString = variableObj.toString(); String fullName = caller + "::" + variableString; GlobalVariable.getGlobalCodeRef(fullName); - GlobalVariable.isSubs.put(fullName, true); + GlobalVariable.getIsSubsMap().put(fullName, true); } return new RuntimeList(); @@ -65,7 +65,7 @@ public static RuntimeList markOverridable(RuntimeArray args, int ctx) { String fullName = args.get(0).toString(); String operatorName = args.get(1).toString(); if (ParserTables.OVERRIDABLE_OP.contains(operatorName)) { - GlobalVariable.isSubs.put(fullName, true); + GlobalVariable.getIsSubsMap().put(fullName, true); } } return new RuntimeList(); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/Symbol.java b/src/main/java/org/perlonjava/runtime/perlmodule/Symbol.java index 82d28cfe9..9c2a232d5 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/Symbol.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/Symbol.java @@ -51,7 +51,7 @@ public static void initialize() { */ public static RuntimeList gensym(RuntimeArray args, int ctx) { // Create a unique anonymous glob - String globName = "Symbol::GEN" + EmitterMethodCreator.classCounter++; + String globName = "Symbol::GEN" + EmitterMethodCreator.classCounter.getAndIncrement(); RuntimeGlob glob = new RuntimeGlob(globName); // Return a reference to the glob (not the glob itself) @@ -103,7 +103,7 @@ public static RuntimeList delete_package(RuntimeArray args, int ctx) { */ public static RuntimeList geniosym(RuntimeArray args, int ctx) { // Create a unique anonymous glob (same as gensym) - String globName = "Symbol::GEN" + EmitterMethodCreator.classCounter++; + String globName = "Symbol::GEN" + EmitterMethodCreator.classCounter.getAndIncrement(); RuntimeGlob glob = new RuntimeGlob(globName); // Initialize the IO slot (equivalent to Perl's: select(select $sym)) diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/TermReadKey.java b/src/main/java/org/perlonjava/runtime/perlmodule/TermReadKey.java index 4988c951a..b9f0d3729 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/TermReadKey.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/TermReadKey.java @@ -107,7 +107,7 @@ public static RuntimeList readMode(RuntimeArray args, int ctx) { } // Get filehandle (defaults to STDIN) - RuntimeIO fh = RuntimeIO.stdin; + RuntimeIO fh = RuntimeIO.getStdin(); if (args.size() > 1) { RuntimeScalar fileHandle = args.get(1); fh = RuntimeIO.getRuntimeIO(fileHandle); @@ -123,7 +123,7 @@ public static RuntimeList readMode(RuntimeArray args, int ctx) { */ public static RuntimeList readKey(RuntimeArray args, int ctx) { double timeout = 0; // Default is blocking - RuntimeIO fh = RuntimeIO.stdin; + RuntimeIO fh = RuntimeIO.getStdin(); if (!args.isEmpty() && args.get(0).getDefinedBoolean()) { timeout = args.get(0).getDouble(); @@ -154,7 +154,7 @@ public static RuntimeList readKey(RuntimeArray args, int ctx) { */ public static RuntimeList readLine(RuntimeArray args, int ctx) { double timeout = 0; // Default is blocking - RuntimeIO fh = RuntimeIO.stdin; + RuntimeIO fh = RuntimeIO.getStdin(); if (!args.isEmpty() && args.get(0).getDefinedBoolean()) { timeout = args.get(0).getDouble(); @@ -185,7 +185,7 @@ public static RuntimeList readLine(RuntimeArray args, int ctx) { * Returns (width, height, xpixels, ypixels) */ public static RuntimeList getTerminalSize(RuntimeArray args, int ctx) { - RuntimeIO fh = RuntimeIO.stdout; + RuntimeIO fh = RuntimeIO.getStdout(); if (!args.isEmpty()) { RuntimeScalar fileHandle = args.get(0); fh = RuntimeIO.getRuntimeIO(fileHandle); @@ -219,7 +219,7 @@ public static RuntimeList setTerminalSize(RuntimeArray args, int ctx) { int xpixels = args.get(2).getInt(); int ypixels = args.get(3).getInt(); - RuntimeIO fh = RuntimeIO.stdout; + RuntimeIO fh = RuntimeIO.getStdout(); if (args.size() > 4) { RuntimeScalar fileHandle = args.get(4); fh = RuntimeIO.getRuntimeIO(fileHandle); @@ -235,7 +235,7 @@ public static RuntimeList setTerminalSize(RuntimeArray args, int ctx) { * Returns (input_speed, output_speed) */ public static RuntimeList getSpeed(RuntimeArray args, int ctx) { - RuntimeIO fh = RuntimeIO.stdin; + RuntimeIO fh = RuntimeIO.getStdin(); if (!args.isEmpty()) { RuntimeScalar fileHandle = args.get(0); fh = RuntimeIO.getRuntimeIO(fileHandle); @@ -259,7 +259,7 @@ public static RuntimeList getSpeed(RuntimeArray args, int ctx) { * Returns an array containing key/value pairs suitable for a hash */ public static RuntimeList getControlChars(RuntimeArray args, int ctx) { - RuntimeIO fh = RuntimeIO.stdin; + RuntimeIO fh = RuntimeIO.getStdin(); if (!args.isEmpty()) { RuntimeScalar fileHandle = args.get(0); fh = RuntimeIO.getRuntimeIO(fileHandle); @@ -293,7 +293,7 @@ public static RuntimeList setControlChars(RuntimeArray args, int ctx) { RuntimeArray controlArray = (RuntimeArray) arrayRef.value; - RuntimeIO fh = RuntimeIO.stdin; + RuntimeIO fh = RuntimeIO.getStdin(); if (args.size() > 1) { RuntimeScalar fileHandle = args.get(1); fh = RuntimeIO.getRuntimeIO(fileHandle); diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/TermReadLine.java b/src/main/java/org/perlonjava/runtime/perlmodule/TermReadLine.java index 94e03df5b..eb31badd1 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/TermReadLine.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/TermReadLine.java @@ -119,8 +119,8 @@ public static RuntimeList readLine(RuntimeArray args, int ctx) { try { // Print prompt to STDOUT using RuntimeIO if (!prompt.isEmpty()) { - RuntimeIO.stdout.write(prompt); - RuntimeIO.stdout.flush(); + RuntimeIO.getStdout().write(prompt); + RuntimeIO.getStdout().flush(); } // Flush all file handles to ensure prompt is visible @@ -160,7 +160,7 @@ public static RuntimeList addHistory(RuntimeArray args, int ctx) { */ public static RuntimeList getInputHandle(RuntimeArray args, int ctx) { // Return a Perl glob for STDIN - return new RuntimeList(new RuntimeScalar(RuntimeIO.stdin)); + return new RuntimeList(new RuntimeScalar(RuntimeIO.getStdin())); } /** @@ -168,7 +168,7 @@ public static RuntimeList getInputHandle(RuntimeArray args, int ctx) { */ public static RuntimeList getOutputHandle(RuntimeArray args, int ctx) { // Return a Perl glob for STDOUT - return new RuntimeList(new RuntimeScalar(RuntimeIO.stdout)); + return new RuntimeList(new RuntimeScalar(RuntimeIO.getStdout())); } /** diff --git a/src/main/java/org/perlonjava/runtime/perlmodule/XMLParserExpat.java b/src/main/java/org/perlonjava/runtime/perlmodule/XMLParserExpat.java index 5a4ae3e5d..84dcef8fa 100644 --- a/src/main/java/org/perlonjava/runtime/perlmodule/XMLParserExpat.java +++ b/src/main/java/org/perlonjava/runtime/perlmodule/XMLParserExpat.java @@ -3,6 +3,7 @@ import org.perlonjava.runtime.operators.Readline; import org.perlonjava.runtime.operators.ReferenceOperators; import org.perlonjava.runtime.runtimetypes.*; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import static org.perlonjava.runtime.runtimetypes.RuntimeScalarCache.*; @@ -1062,7 +1063,7 @@ private static void doParse(ParserState state, InputStream input) throws Excepti } // Set systemId to the current working directory so SAX resolves relative URIs correctly. // This also allows unresolveSysId to strip this prefix and recover relative paths. - String cwd = System.getProperty("user.dir"); + String cwd = PerlRuntime.getCwd(); String baseUri = new java.io.File(cwd, "dummy").toURI().toString(); baseUri = baseUri.substring(0, baseUri.lastIndexOf('/') + 1); inputSource.setSystemId(baseUri); @@ -2132,7 +2133,7 @@ private static String unresolveSysId(String systemId, ParserState state) { // Try to strip file:// + CWD prefix to recover relative or absolute file paths if (systemId.startsWith("file:")) { try { - String cwd = System.getProperty("user.dir"); + String cwd = PerlRuntime.getCwd(); String filePath; if (systemId.startsWith("file:///")) { filePath = systemId.substring(7); // file:///path -> /path diff --git a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java index f8503ce77..0a73d96b9 100644 --- a/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java +++ b/src/main/java/org/perlonjava/runtime/regex/RuntimeRegex.java @@ -5,10 +5,7 @@ import org.perlonjava.runtime.perlmodule.Utf8; import org.perlonjava.runtime.runtimetypes.*; -import java.util.Iterator; -import java.util.LinkedHashMap; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -36,38 +33,87 @@ public class RuntimeRegex extends RuntimeBase implements RuntimeScalarReference private static final int DOTALL = Pattern.DOTALL; // Maximum size for the regex cache private static final int MAX_REGEX_CACHE_SIZE = 1000; - // Cache to store compiled regex patterns - private static final Map regexCache = new LinkedHashMap(MAX_REGEX_CACHE_SIZE, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - return size() > MAX_REGEX_CACHE_SIZE; - } - }; - // Cache for /o modifier - maps callsite ID to compiled regex (only first compilation is used) - private static final Map optimizedRegexCache = new LinkedHashMap<>(); - // Global matcher used for regex operations - public static Matcher globalMatcher; // Provides Perl regex variables like %+, %- - public static String globalMatchString; // Provides Perl regex variables like $& - // Store match information to avoid IllegalStateException from Matcher - public static String lastMatchedString = null; - public static int lastMatchStart = -1; - public static int lastMatchEnd = -1; - // Store match information from last successful pattern (persists across failed matches) - public static String lastSuccessfulMatchedString = null; - public static int lastSuccessfulMatchStart = -1; - public static int lastSuccessfulMatchEnd = -1; - public static String lastSuccessfulMatchString = null; - // ${^LAST_SUCCESSFUL_PATTERN} - public static RuntimeRegex lastSuccessfulPattern = null; - public static boolean lastMatchUsedPFlag = false; - // Tracks if the last match used \K, so matcherStart/matcherEnd/matcherSize adjust group offsets - public static boolean lastMatchUsedBackslashK = false; - // Capture groups from the last successful match that had captures. - // In Perl 5, $1/$2/etc persist across non-capturing matches. - public static String[] lastCaptureGroups = null; - // Track whether the last successful match was on a BYTE_STRING input, - // so that captures ($1, $2, $&, etc.) preserve BYTE_STRING type. - public static boolean lastMatchWasByteString = false; + // Cache to store compiled regex patterns (synchronized for multiplicity thread-safety) + private static final Map regexCache = Collections.synchronizedMap( + new LinkedHashMap(MAX_REGEX_CACHE_SIZE, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + return size() > MAX_REGEX_CACHE_SIZE; + } + }); + // Cache for /o modifier is now per-PerlRuntime (regexOptimizedCache field) + + // ---- Regex match state accessors (delegating to PerlRuntime.current()) ---- + + /** Gets the global Matcher from current runtime. */ + public static Matcher getGlobalMatcher() { return PerlRuntime.current().regexGlobalMatcher; } + /** Sets the global Matcher on current runtime. */ + public static void setGlobalMatcher(Matcher m) { PerlRuntime.current().regexGlobalMatcher = m; } + + /** Gets the global match string from current runtime. */ + public static String getGlobalMatchString() { return PerlRuntime.current().regexGlobalMatchString; } + /** Sets the global match string on current runtime. */ + public static void setGlobalMatchString(String s) { PerlRuntime.current().regexGlobalMatchString = s; } + + /** Gets lastMatchedString from current runtime. */ + public static String getLastMatchedString() { return PerlRuntime.current().regexLastMatchedString; } + /** Sets lastMatchedString on current runtime. */ + public static void setLastMatchedString(String s) { PerlRuntime.current().regexLastMatchedString = s; } + + /** Gets lastMatchStart from current runtime. */ + public static int getLastMatchStart() { return PerlRuntime.current().regexLastMatchStart; } + /** Sets lastMatchStart on current runtime. */ + public static void setLastMatchStart(int v) { PerlRuntime.current().regexLastMatchStart = v; } + + /** Gets lastMatchEnd from current runtime. */ + public static int getLastMatchEnd() { return PerlRuntime.current().regexLastMatchEnd; } + /** Sets lastMatchEnd on current runtime. */ + public static void setLastMatchEnd(int v) { PerlRuntime.current().regexLastMatchEnd = v; } + + /** Gets lastSuccessfulMatchedString from current runtime. */ + public static String getLastSuccessfulMatchedString() { return PerlRuntime.current().regexLastSuccessfulMatchedString; } + /** Sets lastSuccessfulMatchedString on current runtime. */ + public static void setLastSuccessfulMatchedString(String s) { PerlRuntime.current().regexLastSuccessfulMatchedString = s; } + + /** Gets lastSuccessfulMatchStart from current runtime. */ + public static int getLastSuccessfulMatchStart() { return PerlRuntime.current().regexLastSuccessfulMatchStart; } + /** Sets lastSuccessfulMatchStart on current runtime. */ + public static void setLastSuccessfulMatchStart(int v) { PerlRuntime.current().regexLastSuccessfulMatchStart = v; } + + /** Gets lastSuccessfulMatchEnd from current runtime. */ + public static int getLastSuccessfulMatchEnd() { return PerlRuntime.current().regexLastSuccessfulMatchEnd; } + /** Sets lastSuccessfulMatchEnd on current runtime. */ + public static void setLastSuccessfulMatchEnd(int v) { PerlRuntime.current().regexLastSuccessfulMatchEnd = v; } + + /** Gets lastSuccessfulMatchString from current runtime. */ + public static String getLastSuccessfulMatchString() { return PerlRuntime.current().regexLastSuccessfulMatchString; } + /** Sets lastSuccessfulMatchString on current runtime. */ + public static void setLastSuccessfulMatchString(String s) { PerlRuntime.current().regexLastSuccessfulMatchString = s; } + + /** Gets lastSuccessfulPattern from current runtime. */ + public static RuntimeRegex getLastSuccessfulPattern() { return PerlRuntime.current().regexLastSuccessfulPattern; } + /** Sets lastSuccessfulPattern on current runtime. */ + public static void setLastSuccessfulPattern(RuntimeRegex p) { PerlRuntime.current().regexLastSuccessfulPattern = p; } + + /** Gets lastMatchUsedPFlag from current runtime. */ + public static boolean getLastMatchUsedPFlag() { return PerlRuntime.current().regexLastMatchUsedPFlag; } + /** Sets lastMatchUsedPFlag on current runtime. */ + public static void setLastMatchUsedPFlag(boolean v) { PerlRuntime.current().regexLastMatchUsedPFlag = v; } + + /** Gets lastMatchUsedBackslashK from current runtime. */ + public static boolean getLastMatchUsedBackslashK() { return PerlRuntime.current().regexLastMatchUsedBackslashK; } + /** Sets lastMatchUsedBackslashK on current runtime. */ + public static void setLastMatchUsedBackslashK(boolean v) { PerlRuntime.current().regexLastMatchUsedBackslashK = v; } + + /** Gets lastCaptureGroups from current runtime. */ + public static String[] getLastCaptureGroups() { return PerlRuntime.current().regexLastCaptureGroups; } + /** Sets lastCaptureGroups on current runtime. */ + public static void setLastCaptureGroups(String[] g) { PerlRuntime.current().regexLastCaptureGroups = g; } + + /** Gets lastMatchWasByteString from current runtime. */ + public static boolean getLastMatchWasByteString() { return PerlRuntime.current().regexLastMatchWasByteString; } + /** Sets lastMatchWasByteString on current runtime. */ + public static void setLastMatchWasByteString(boolean v) { PerlRuntime.current().regexLastMatchWasByteString = v; } // Compiled regex pattern (for byte strings - ASCII-only \w, \d) public Pattern pattern; // Compiled regex pattern for Unicode strings (Unicode \w, \d) @@ -475,15 +521,16 @@ public static RuntimeScalar getQuotedRegex(RuntimeScalar patternString, RuntimeS // to preserve state: /o caches the compiled pattern, m?PAT? preserves the // 'matched' flag that tracks whether the pattern has already matched once) if (modifierStr.contains("o") || modifierStr.contains("?")) { + Map cache = PerlRuntime.current().regexOptimizedCache; // Check if we already have a cached regex for this callsite - RuntimeScalar cached = optimizedRegexCache.get(callsiteId); + RuntimeScalar cached = cache.get(callsiteId); if (cached != null) { return cached; } // Compile the regex and cache it RuntimeScalar result = getQuotedRegex(patternString, modifiers); - optimizedRegexCache.put(callsiteId, result); + cache.put(callsiteId, result); return result; } @@ -617,25 +664,25 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc // Handle empty pattern - reuse last successful pattern or use empty pattern if (regex.patternString == null || regex.patternString.isEmpty()) { - if (lastSuccessfulPattern != null) { + if (PerlRuntime.current().regexLastSuccessfulPattern != null) { // Use the pattern from last successful match // But keep the current flags (especially /g and /i) - Pattern pattern = lastSuccessfulPattern.pattern; + Pattern pattern = PerlRuntime.current().regexLastSuccessfulPattern.pattern; // Re-apply current flags if they differ - if (originalFlags != null && !originalFlags.equals(lastSuccessfulPattern.regexFlags)) { + if (originalFlags != null && !originalFlags.equals(PerlRuntime.current().regexLastSuccessfulPattern.regexFlags)) { // Need to recompile with current flags using preprocessed pattern int newFlags = originalFlags.toPatternFlags(); - String recompilePattern = lastSuccessfulPattern.javaPatternString != null - ? lastSuccessfulPattern.javaPatternString : lastSuccessfulPattern.patternString; + String recompilePattern = PerlRuntime.current().regexLastSuccessfulPattern.javaPatternString != null + ? PerlRuntime.current().regexLastSuccessfulPattern.javaPatternString : PerlRuntime.current().regexLastSuccessfulPattern.patternString; pattern = Pattern.compile(recompilePattern, newFlags); } // Create a temporary regex with the right pattern and current flags RuntimeRegex tempRegex = new RuntimeRegex(); tempRegex.pattern = pattern; - tempRegex.patternUnicode = lastSuccessfulPattern.patternUnicode; - tempRegex.patternString = lastSuccessfulPattern.patternString; - tempRegex.javaPatternString = lastSuccessfulPattern.javaPatternString; - tempRegex.hasPreservesMatch = lastSuccessfulPattern.hasPreservesMatch || (originalFlags != null && originalFlags.preservesMatch()); + tempRegex.patternUnicode = PerlRuntime.current().regexLastSuccessfulPattern.patternUnicode; + tempRegex.patternString = PerlRuntime.current().regexLastSuccessfulPattern.patternString; + tempRegex.javaPatternString = PerlRuntime.current().regexLastSuccessfulPattern.javaPatternString; + tempRegex.hasPreservesMatch = PerlRuntime.current().regexLastSuccessfulPattern.hasPreservesMatch || (originalFlags != null && originalFlags.preservesMatch()); tempRegex.regexFlags = originalFlags; tempRegex.useGAssertion = originalFlags != null && originalFlags.useGAssertion(); regex = tempRegex; @@ -796,52 +843,52 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc } found = true; - lastMatchWasByteString = (string.type == RuntimeScalarType.BYTE_STRING); + PerlRuntime.current().regexLastMatchWasByteString = (string.type == RuntimeScalarType.BYTE_STRING); int captureCount = matcher.groupCount(); // Always initialize $1, $2, @+, @-, $`, $&, $' for every successful match - globalMatcher = matcher; - globalMatchString = inputStr; - lastMatchUsedBackslashK = regex.hasBackslashK; + PerlRuntime.current().regexGlobalMatcher = matcher; + PerlRuntime.current().regexGlobalMatchString = inputStr; + PerlRuntime.current().regexLastMatchUsedBackslashK = regex.hasBackslashK; if (captureCount > 0) { if (regex.hasBackslashK) { // Skip the internal perlK capture group int perlKGroup = getPerlKGroup(matcher); int userGroupCount = captureCount - 1; if (userGroupCount > 0) { - lastCaptureGroups = new String[userGroupCount]; + PerlRuntime.current().regexLastCaptureGroups = new String[userGroupCount]; int destIdx = 0; for (int i = 1; i <= captureCount; i++) { if (i == perlKGroup) continue; - lastCaptureGroups[destIdx++] = matcher.group(i); + PerlRuntime.current().regexLastCaptureGroups[destIdx++] = matcher.group(i); } } else { - lastCaptureGroups = null; + PerlRuntime.current().regexLastCaptureGroups = null; } } else { - lastCaptureGroups = new String[captureCount]; + PerlRuntime.current().regexLastCaptureGroups = new String[captureCount]; for (int i = 0; i < captureCount; i++) { - lastCaptureGroups[i] = matcher.group(i + 1); + PerlRuntime.current().regexLastCaptureGroups[i] = matcher.group(i + 1); } } } else { - lastCaptureGroups = null; + PerlRuntime.current().regexLastCaptureGroups = null; } // For \K, adjust match start/string so $& is only the post-\K portion if (regex.hasBackslashK) { int keepEnd = matcher.end("perlK"); - lastMatchedString = inputStr.substring(keepEnd, matcher.end()); - lastMatchStart = keepEnd; + PerlRuntime.current().regexLastMatchedString = inputStr.substring(keepEnd, matcher.end()); + PerlRuntime.current().regexLastMatchStart = keepEnd; } else { - lastMatchedString = matcher.group(0); - lastMatchStart = matcher.start(); + PerlRuntime.current().regexLastMatchedString = matcher.group(0); + PerlRuntime.current().regexLastMatchStart = matcher.start(); } - lastMatchEnd = matcher.end(); + PerlRuntime.current().regexLastMatchEnd = matcher.end(); if (regex.regexFlags.isGlobalMatch() && captureCount < 1 && ctx == RuntimeContextType.LIST) { // Global match and no captures, in list context return the matched string - String matchedStr = regex.hasBackslashK ? lastMatchedString : matcher.group(0); + String matchedStr = regex.hasBackslashK ? PerlRuntime.current().regexLastMatchedString : matcher.group(0); matchedGroups.add(makeMatchResultScalar(matchedStr)); } else { // save captures in return list if needed @@ -931,24 +978,24 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc if (!found) { // No match: scalar match vars ($`, $&, $') should become undef. - // Keep lastSuccessful* and the previous globalMatcher intact so @-/@+ do not get clobbered + // Keep lastSuccessful* and the previous PerlRuntime.current().regexGlobalMatcher intact so @-/@+ do not get clobbered // by internal regex checks that fail (e.g. in test libraries). - globalMatchString = null; - lastMatchedString = null; - lastMatchStart = -1; - lastMatchEnd = -1; - // Don't clear lastCaptureGroups - Perl preserves $1 across failed matches + PerlRuntime.current().regexGlobalMatchString = null; + PerlRuntime.current().regexLastMatchedString = null; + PerlRuntime.current().regexLastMatchStart = -1; + PerlRuntime.current().regexLastMatchEnd = -1; + // Don't clear PerlRuntime.current().regexLastCaptureGroups - Perl preserves $1 across failed matches } if (found) { regex.matched = true; // Counter for m?PAT? - lastMatchUsedPFlag = regex.hasPreservesMatch; - lastSuccessfulPattern = regex; + PerlRuntime.current().regexLastMatchUsedPFlag = regex.hasPreservesMatch; + PerlRuntime.current().regexLastSuccessfulPattern = regex; // Store last successful match information (persists across failed matches) - lastSuccessfulMatchedString = lastMatchedString; - lastSuccessfulMatchStart = lastMatchStart; - lastSuccessfulMatchEnd = lastMatchEnd; - lastSuccessfulMatchString = globalMatchString; + PerlRuntime.current().regexLastSuccessfulMatchedString = PerlRuntime.current().regexLastMatchedString; + PerlRuntime.current().regexLastSuccessfulMatchStart = PerlRuntime.current().regexLastMatchStart; + PerlRuntime.current().regexLastSuccessfulMatchEnd = PerlRuntime.current().regexLastMatchEnd; + PerlRuntime.current().regexLastSuccessfulMatchString = PerlRuntime.current().regexGlobalMatchString; // Update $^R if this regex has code block captures (performance optimization) if (regex.hasCodeBlockCaptures) { @@ -962,9 +1009,9 @@ private static RuntimeBase matchRegexDirect(RuntimeScalar quotedRegex, RuntimeSc if (regex.regexFlags.isGlobalMatch() && ctx == RuntimeContextType.LIST && posScalar != null) { posScalar.set(scalarUndef); } - // System.err.println("DEBUG: Match completed, globalMatcher is " + (globalMatcher == null ? "null" : "set")); + // System.err.println("DEBUG: Match completed, PerlRuntime.current().regexGlobalMatcher is " + (PerlRuntime.current().regexGlobalMatcher == null ? "null" : "set")); } else { - // System.err.println("DEBUG: No match found, globalMatcher is " + (globalMatcher == null ? "null" : "set")); + // System.err.println("DEBUG: No match found, PerlRuntime.current().regexGlobalMatcher is " + (PerlRuntime.current().regexGlobalMatcher == null ? "null" : "set")); } if (ctx == RuntimeContextType.LIST) { @@ -1058,25 +1105,25 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar // Handle empty pattern - reuse last successful pattern or use empty pattern if (regex.patternString == null || regex.patternString.isEmpty()) { - if (lastSuccessfulPattern != null) { + if (PerlRuntime.current().regexLastSuccessfulPattern != null) { // Use the pattern from last successful match // But keep the current replacement and flags (especially /g and /i) - Pattern pattern = lastSuccessfulPattern.pattern; + Pattern pattern = PerlRuntime.current().regexLastSuccessfulPattern.pattern; // Re-apply current flags if they differ - if (originalFlags != null && !originalFlags.equals(lastSuccessfulPattern.regexFlags)) { + if (originalFlags != null && !originalFlags.equals(PerlRuntime.current().regexLastSuccessfulPattern.regexFlags)) { // Need to recompile with current flags using preprocessed pattern int newFlags = originalFlags.toPatternFlags(); - String recompilePattern = lastSuccessfulPattern.javaPatternString != null - ? lastSuccessfulPattern.javaPatternString : lastSuccessfulPattern.patternString; + String recompilePattern = PerlRuntime.current().regexLastSuccessfulPattern.javaPatternString != null + ? PerlRuntime.current().regexLastSuccessfulPattern.javaPatternString : PerlRuntime.current().regexLastSuccessfulPattern.patternString; pattern = Pattern.compile(recompilePattern, newFlags); } // Create a temporary regex with the right pattern and current flags RuntimeRegex tempRegex = new RuntimeRegex(); tempRegex.pattern = pattern; - tempRegex.patternUnicode = lastSuccessfulPattern.patternUnicode; - tempRegex.patternString = lastSuccessfulPattern.patternString; - tempRegex.javaPatternString = lastSuccessfulPattern.javaPatternString; - tempRegex.hasPreservesMatch = lastSuccessfulPattern.hasPreservesMatch || (originalFlags != null && originalFlags.preservesMatch()); + tempRegex.patternUnicode = PerlRuntime.current().regexLastSuccessfulPattern.patternUnicode; + tempRegex.patternString = PerlRuntime.current().regexLastSuccessfulPattern.patternString; + tempRegex.javaPatternString = PerlRuntime.current().regexLastSuccessfulPattern.javaPatternString; + tempRegex.hasPreservesMatch = PerlRuntime.current().regexLastSuccessfulPattern.hasPreservesMatch || (originalFlags != null && originalFlags.preservesMatch()); tempRegex.regexFlags = originalFlags; tempRegex.useGAssertion = originalFlags != null && originalFlags.useGAssertion(); tempRegex.replacement = replacement; @@ -1131,7 +1178,7 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar // Determine if the replacement is a code that needs to be evaluated boolean replacementIsCode = (replacement.type == RuntimeScalarType.CODE); - // Don't reset globalMatcher here - only reset it if we actually find a match + // Don't reset PerlRuntime.current().regexGlobalMatcher here - only reset it if we actually find a match // This preserves capture variables from previous matches when substitution doesn't match // Track position for manual replacement when \K is used @@ -1141,47 +1188,47 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar try { while (matcher.find()) { found++; - lastMatchWasByteString = (string.type == RuntimeScalarType.BYTE_STRING); + PerlRuntime.current().regexLastMatchWasByteString = (string.type == RuntimeScalarType.BYTE_STRING); // Initialize $1, $2, @+, @- only when we have a match - globalMatcher = matcher; - globalMatchString = inputStr; - lastMatchUsedBackslashK = regex.hasBackslashK; + PerlRuntime.current().regexGlobalMatcher = matcher; + PerlRuntime.current().regexGlobalMatchString = inputStr; + PerlRuntime.current().regexLastMatchUsedBackslashK = regex.hasBackslashK; if (matcher.groupCount() > 0) { if (regex.hasBackslashK) { // Skip the internal perlK capture group when populating $1, $2, etc. int perlKGroup = getPerlKGroup(matcher); int userGroupCount = matcher.groupCount() - 1; if (userGroupCount > 0) { - lastCaptureGroups = new String[userGroupCount]; + PerlRuntime.current().regexLastCaptureGroups = new String[userGroupCount]; int destIdx = 0; for (int i = 1; i <= matcher.groupCount(); i++) { if (i == perlKGroup) continue; - lastCaptureGroups[destIdx++] = matcher.group(i); + PerlRuntime.current().regexLastCaptureGroups[destIdx++] = matcher.group(i); } } else { - lastCaptureGroups = null; + PerlRuntime.current().regexLastCaptureGroups = null; } } else { - lastCaptureGroups = new String[matcher.groupCount()]; + PerlRuntime.current().regexLastCaptureGroups = new String[matcher.groupCount()]; for (int i = 0; i < matcher.groupCount(); i++) { - lastCaptureGroups[i] = matcher.group(i + 1); + PerlRuntime.current().regexLastCaptureGroups[i] = matcher.group(i + 1); } } } else { - lastCaptureGroups = null; + PerlRuntime.current().regexLastCaptureGroups = null; } // For \K, adjust match start so $& is only the post-\K portion if (regex.hasBackslashK) { int keepEnd = matcher.end("perlK"); - lastMatchStart = keepEnd; - lastMatchedString = inputStr.substring(keepEnd, matcher.end()); + PerlRuntime.current().regexLastMatchStart = keepEnd; + PerlRuntime.current().regexLastMatchedString = inputStr.substring(keepEnd, matcher.end()); } else { - lastMatchStart = matcher.start(); - lastMatchedString = matcher.group(0); + PerlRuntime.current().regexLastMatchStart = matcher.start(); + PerlRuntime.current().regexLastMatchedString = matcher.group(0); } - lastMatchEnd = matcher.end(); + PerlRuntime.current().regexLastMatchEnd = matcher.end(); String replacementStr; if (replacementIsCode) { @@ -1229,8 +1276,8 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar boolean wasByteString = (string.type == RuntimeScalarType.BYTE_STRING); // Store as last successful pattern for empty pattern reuse - lastMatchUsedPFlag = regex.hasPreservesMatch; - lastSuccessfulPattern = regex; + PerlRuntime.current().regexLastMatchUsedPFlag = regex.hasPreservesMatch; + PerlRuntime.current().regexLastSuccessfulPattern = regex; if (regex.regexFlags.isNonDestructive()) { // /r modifier: return the modified string @@ -1265,12 +1312,15 @@ public static RuntimeBase replaceRegex(RuntimeScalar quotedRegex, RuntimeScalar */ public static void reset() { // Iterate over the regexCache and reset the `matched` flag for each cached regex - for (Map.Entry entry : regexCache.entrySet()) { - RuntimeRegex regex = entry.getValue(); - regex.matched = false; // Reset the matched field + // Synchronized because Collections.synchronizedMap requires manual sync for iteration + synchronized (regexCache) { + for (Map.Entry entry : regexCache.entrySet()) { + RuntimeRegex regex = entry.getValue(); + regex.matched = false; // Reset the matched field + } } - // Also reset m?PAT? patterns cached per-callsite in optimizedRegexCache - for (Map.Entry entry : optimizedRegexCache.entrySet()) { + // Also reset m?PAT? patterns cached per-callsite in regexOptimizedCache + for (Map.Entry entry : PerlRuntime.current().regexOptimizedCache.entrySet()) { RuntimeScalar scalar = entry.getValue(); if (scalar.value instanceof RuntimeRegex regex) { regex.matched = false; @@ -1284,48 +1334,48 @@ public static void reset() { */ public static void initialize() { // Reset all match state - globalMatcher = null; - globalMatchString = null; + PerlRuntime.current().regexGlobalMatcher = null; + PerlRuntime.current().regexGlobalMatchString = null; // Reset current match information - lastMatchedString = null; - lastMatchStart = -1; - lastMatchEnd = -1; + PerlRuntime.current().regexLastMatchedString = null; + PerlRuntime.current().regexLastMatchStart = -1; + PerlRuntime.current().regexLastMatchEnd = -1; // Reset last successful match information - lastSuccessfulPattern = null; - lastSuccessfulMatchedString = null; - lastSuccessfulMatchStart = -1; - lastSuccessfulMatchEnd = -1; - lastSuccessfulMatchString = null; - lastMatchUsedPFlag = false; - lastCaptureGroups = null; + PerlRuntime.current().regexLastSuccessfulPattern = null; + PerlRuntime.current().regexLastSuccessfulMatchedString = null; + PerlRuntime.current().regexLastSuccessfulMatchStart = -1; + PerlRuntime.current().regexLastSuccessfulMatchEnd = -1; + PerlRuntime.current().regexLastSuccessfulMatchString = null; + PerlRuntime.current().regexLastMatchUsedPFlag = false; + PerlRuntime.current().regexLastCaptureGroups = null; // Reset regex cache matched flags reset(); } public static String matchString() { - if (globalMatcher != null && lastMatchedString != null) { + if (PerlRuntime.current().regexGlobalMatcher != null && PerlRuntime.current().regexLastMatchedString != null) { // Current match data available - return lastMatchedString; + return PerlRuntime.current().regexLastMatchedString; } return null; } public static String preMatchString() { - if (globalMatcher != null && globalMatchString != null && lastMatchStart != -1) { + if (PerlRuntime.current().regexGlobalMatcher != null && PerlRuntime.current().regexGlobalMatchString != null && PerlRuntime.current().regexLastMatchStart != -1) { // Current match data available - String result = globalMatchString.substring(0, lastMatchStart); + String result = PerlRuntime.current().regexGlobalMatchString.substring(0, PerlRuntime.current().regexLastMatchStart); return result; } return null; } public static String postMatchString() { - if (globalMatcher != null && globalMatchString != null && lastMatchEnd != -1) { + if (PerlRuntime.current().regexGlobalMatcher != null && PerlRuntime.current().regexGlobalMatchString != null && PerlRuntime.current().regexLastMatchEnd != -1) { // Current match data available - String result = globalMatchString.substring(lastMatchEnd); + String result = PerlRuntime.current().regexGlobalMatchString.substring(PerlRuntime.current().regexLastMatchEnd); return result; } return null; @@ -1333,24 +1383,24 @@ public static String postMatchString() { public static String captureString(int group) { if (group <= 0) { - return lastMatchedString; + return PerlRuntime.current().regexLastMatchedString; } - if (lastCaptureGroups == null || group > lastCaptureGroups.length) { + if (PerlRuntime.current().regexLastCaptureGroups == null || group > PerlRuntime.current().regexLastCaptureGroups.length) { return null; } - return lastCaptureGroups[group - 1]; + return PerlRuntime.current().regexLastCaptureGroups[group - 1]; } public static String lastCaptureString() { - if (lastCaptureGroups == null || lastCaptureGroups.length == 0) { + if (PerlRuntime.current().regexLastCaptureGroups == null || PerlRuntime.current().regexLastCaptureGroups.length == 0) { return null; } // $+ returns the highest-numbered capture group that actually participated // in the match (i.e., is non-null). Non-participating groups in alternations // have null values from Java's Matcher.group(). - for (int i = lastCaptureGroups.length - 1; i >= 0; i--) { - if (lastCaptureGroups[i] != null) { - return lastCaptureGroups[i]; + for (int i = PerlRuntime.current().regexLastCaptureGroups.length - 1; i >= 0; i--) { + if (PerlRuntime.current().regexLastCaptureGroups[i] != null) { + return PerlRuntime.current().regexLastCaptureGroups[i]; } } return null; @@ -1365,7 +1415,7 @@ public static RuntimeScalar makeMatchResultScalar(String value) { return RuntimeScalarCache.scalarUndef; } RuntimeScalar scalar = new RuntimeScalar(value); - if (lastMatchWasByteString) { + if (PerlRuntime.current().regexLastMatchWasByteString) { scalar.type = RuntimeScalarType.BYTE_STRING; } return scalar; @@ -1373,18 +1423,18 @@ public static RuntimeScalar makeMatchResultScalar(String value) { public static RuntimeScalar matcherStart(int group) { if (group == 0) { - return lastMatchStart >= 0 ? getScalarInt(lastMatchStart) : scalarUndef; + return PerlRuntime.current().regexLastMatchStart >= 0 ? getScalarInt(PerlRuntime.current().regexLastMatchStart) : scalarUndef; } - if (globalMatcher == null) { + if (PerlRuntime.current().regexGlobalMatcher == null) { return scalarUndef; } try { // Adjust group number to skip the internal perlK group int javaGroup = adjustGroupForBackslashK(group); - if (javaGroup < 0 || javaGroup > globalMatcher.groupCount()) { + if (javaGroup < 0 || javaGroup > PerlRuntime.current().regexGlobalMatcher.groupCount()) { return scalarUndef; } - int start = globalMatcher.start(javaGroup); + int start = PerlRuntime.current().regexGlobalMatcher.start(javaGroup); if (start == -1) { return scalarUndef; } @@ -1396,18 +1446,18 @@ public static RuntimeScalar matcherStart(int group) { public static RuntimeScalar matcherEnd(int group) { if (group == 0) { - return lastMatchEnd >= 0 ? getScalarInt(lastMatchEnd) : scalarUndef; + return PerlRuntime.current().regexLastMatchEnd >= 0 ? getScalarInt(PerlRuntime.current().regexLastMatchEnd) : scalarUndef; } - if (globalMatcher == null) { + if (PerlRuntime.current().regexGlobalMatcher == null) { return scalarUndef; } try { // Adjust group number to skip the internal perlK group int javaGroup = adjustGroupForBackslashK(group); - if (javaGroup < 0 || javaGroup > globalMatcher.groupCount()) { + if (javaGroup < 0 || javaGroup > PerlRuntime.current().regexGlobalMatcher.groupCount()) { return scalarUndef; } - int end = globalMatcher.end(javaGroup); + int end = PerlRuntime.current().regexGlobalMatcher.end(javaGroup); if (end == -1) { return scalarUndef; } @@ -1418,12 +1468,12 @@ public static RuntimeScalar matcherEnd(int group) { } public static int matcherSize() { - if (globalMatcher == null) { + if (PerlRuntime.current().regexGlobalMatcher == null) { return 0; } - int size = globalMatcher.groupCount(); + int size = PerlRuntime.current().regexGlobalMatcher.groupCount(); // Subtract the internal perlK group if \K was used - if (lastMatchUsedBackslashK) { + if (PerlRuntime.current().regexLastMatchUsedBackslashK) { size--; } // +1 because groupCount is zero-based, and we include the entire match @@ -1435,10 +1485,10 @@ public static int matcherSize() { * skipping the internal perlK named group when \K is active. */ private static int adjustGroupForBackslashK(int perlGroup) { - if (!lastMatchUsedBackslashK || globalMatcher == null) { + if (!PerlRuntime.current().regexLastMatchUsedBackslashK || PerlRuntime.current().regexGlobalMatcher == null) { return perlGroup; } - int perlKGroup = getPerlKGroup(globalMatcher); + int perlKGroup = getPerlKGroup(PerlRuntime.current().regexGlobalMatcher); if (perlKGroup < 0) return perlGroup; // Perl groups before perlK: same number. At or after: add 1. return perlGroup >= perlKGroup ? perlGroup + 1 : perlGroup; @@ -1747,7 +1797,7 @@ public void dynamicRestoreState() { * @return The constant value for $^R, or null if no code block was matched */ public RuntimeScalar getLastCodeBlockResult() { - Matcher matcher = globalMatcher; + Matcher matcher = PerlRuntime.current().regexGlobalMatcher; if (matcher == null) { return null; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/CallerStack.java b/src/main/java/org/perlonjava/runtime/runtimetypes/CallerStack.java index 7857cd39d..e0cf3d9b7 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/CallerStack.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/CallerStack.java @@ -10,8 +10,10 @@ * for implementing the caller() function during operations like import() and unimport(). */ public class CallerStack { - // Store either CallerInfo (resolved) or LazyCallerInfo (deferred) - private static final List callerStack = new ArrayList<>(); + // State is now held per-PerlRuntime. This accessor delegates to the current runtime. + private static List callerStack() { + return PerlRuntime.current().callerStack; + } /** * Pushes a new CallerInfo object onto the stack, representing a new entry in the calling sequence. @@ -21,7 +23,11 @@ public class CallerStack { * @param line The line number in the file where the call originated. */ public static void push(String packageName, String filename, int line) { - callerStack.add(new CallerInfo(packageName, filename, line)); + List stack = callerStack(); + if (System.getenv("DEBUG_CALLER") != null) { + System.err.println("DEBUG CallerStack.push: pkg=" + packageName + " file=" + filename + " line=" + line + " (stack size now " + (stack.size() + 1) + ")"); + } + stack.add(new CallerInfo(packageName, filename, line)); } /** @@ -33,7 +39,11 @@ public static void push(String packageName, String filename, int line) { * @param resolver A function to compute the CallerInfo when needed. */ public static void pushLazy(String packageName, CallerInfoResolver resolver) { - callerStack.add(new LazyCallerInfo(packageName, resolver)); + List stack = callerStack(); + if (System.getenv("DEBUG_CALLER") != null) { + System.err.println("DEBUG CallerStack.pushLazy: pkg=" + packageName + " (stack size now " + (stack.size() + 1) + ")"); + } + stack.add(new LazyCallerInfo(packageName, resolver)); } /** @@ -44,20 +54,20 @@ public static void pushLazy(String packageName, CallerInfoResolver resolver) { * @return The most recent CallerInfo object, or null if the stack is empty. */ public static CallerInfo peek(int callFrame) { - if (callerStack.isEmpty()) { + List stack = callerStack(); + if (stack.isEmpty()) { return null; } - int index = callerStack.size() - 1 - callFrame; + int index = stack.size() - 1 - callFrame; if (index < 0) { return null; } - Object entry = callerStack.get(index); + Object entry = stack.get(index); if (entry instanceof CallerInfo ci) { return ci; } else if (entry instanceof LazyCallerInfo lazy) { - // Resolve the lazy entry and cache it CallerInfo resolved = lazy.resolve(); - callerStack.set(index, resolved); + stack.set(index, resolved); return resolved; } return null; @@ -70,14 +80,14 @@ public static CallerInfo peek(int callFrame) { * @return The most recent CallerInfo object, or null if the stack is empty. */ public static CallerInfo pop() { - if (callerStack.isEmpty()) { + List stack = callerStack(); + if (stack.isEmpty()) { return null; } - Object entry = callerStack.removeLast(); + Object entry = stack.removeLast(); if (entry instanceof CallerInfo ci) { return ci; } else if (entry instanceof LazyCallerInfo lazy) { - // Don't resolve on pop - caller info not needed return null; } return null; @@ -90,14 +100,15 @@ public static CallerInfo pop() { * @return A list containing all CallerInfo objects in the stack. */ public static List getStack() { + List stack = callerStack(); List result = new ArrayList<>(); - for (int i = 0; i < callerStack.size(); i++) { - Object entry = callerStack.get(i); + for (int i = 0; i < stack.size(); i++) { + Object entry = stack.get(i); if (entry instanceof CallerInfo ci) { result.add(ci); } else if (entry instanceof LazyCallerInfo lazy) { CallerInfo resolved = lazy.resolve(); - callerStack.set(i, resolved); + stack.set(i, resolved); result.add(resolved); } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/DiamondIO.java b/src/main/java/org/perlonjava/runtime/runtimetypes/DiamondIO.java index b38ab3e5b..12df0c29d 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/DiamondIO.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/DiamondIO.java @@ -236,11 +236,11 @@ private static boolean openNextFile() { // Use the resolved path to ensure we write to the correct location currentWriter = RuntimeIO.open(originalPath.toString(), ">"); getGlobalIO("main::ARGVOUT").set(currentWriter); - RuntimeIO.lastAccesseddHandle = currentWriter; + RuntimeIO.setLastAccessedHandle(currentWriter); // CRITICAL: Update selectedHandle so print statements without explicit filehandle // write to the original file during in-place editing - RuntimeIO.selectedHandle = currentWriter; + RuntimeIO.setSelectedHandle(currentWriter); } // Open the renamed file for reading diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java b/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java index 66471b4fc..8796e6c84 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/DynamicVariableManager.java @@ -10,9 +10,10 @@ * to their original states. */ public class DynamicVariableManager { - // A stack to hold the dynamic states of variables. - // Using ArrayDeque instead of Stack for better performance (no synchronization overhead). - private static final Deque variableStack = new ArrayDeque<>(); + // State is now held per-PerlRuntime. This accessor delegates to the current runtime. + private static Deque variableStack() { + return PerlRuntime.current().dynamicVariableStack; + } /** * Returns the current local level, which is the size of the variable stack. @@ -21,7 +22,7 @@ public class DynamicVariableManager { * @return the number of dynamic states in the stack. */ public static int getLocalLevel() { - return variableStack.size(); + return variableStack().size(); } /** @@ -33,14 +34,14 @@ public static int getLocalLevel() { public static RuntimeBase pushLocalVariable(RuntimeBase variable) { // Save the current state of the variable and push it onto the stack. variable.dynamicSaveState(); - variableStack.addLast(variable); + variableStack().addLast(variable); return variable; } public static RuntimeScalar pushLocalVariable(RuntimeScalar variable) { // Save the current state of the variable and push it onto the stack. variable.dynamicSaveState(); - variableStack.addLast(variable); + variableStack().addLast(variable); return variable; } @@ -48,7 +49,7 @@ public static RuntimeGlob pushLocalVariable(RuntimeGlob variable) { // Save the current state of the variable and push it onto the stack. // dynamicSaveState() creates a NEW glob in globalIORefs for the local scope. variable.dynamicSaveState(); - variableStack.addLast(variable); + variableStack().addLast(variable); // Return the NEW glob from globalIORefs (installed by dynamicSaveState), // not the old one. This ensures `local *FH` returns the fresh local glob, // so that \do { local *FH } captures a unique glob per call (Perl 5 parity). @@ -57,7 +58,7 @@ public static RuntimeGlob pushLocalVariable(RuntimeGlob variable) { public static void pushLocalVariable(DynamicState variable) { variable.dynamicSaveState(); - variableStack.addLast(variable); + variableStack().addLast(variable); } /** @@ -72,8 +73,9 @@ public static void pushLocalVariable(DynamicState variable) { * @param targetLocalLevel the target size of the stack after popping variables. */ public static void popToLocalLevel(int targetLocalLevel) { + Deque stack = variableStack(); // Ensure the target level is non-negative and does not exceed the current stack size - if (targetLocalLevel < 0 || targetLocalLevel > variableStack.size()) { + if (targetLocalLevel < 0 || targetLocalLevel > stack.size()) { throw new IllegalArgumentException("Invalid target local level: " + targetLocalLevel); } @@ -81,8 +83,8 @@ public static void popToLocalLevel(int targetLocalLevel) { Throwable pendingException = null; // Pop variables until the stack size matches the target local level - while (variableStack.size() > targetLocalLevel) { - DynamicState variable = variableStack.removeLast(); + while (stack.size() > targetLocalLevel) { + DynamicState variable = stack.removeLast(); try { variable.dynamicRestoreState(); } catch (Throwable t) { diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrnoHash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrnoHash.java index 7945626c6..cfb775d21 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrnoHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrnoHash.java @@ -45,7 +45,7 @@ public class ErrnoHash extends AbstractMap { * Get the current errno value from $!. */ private static int getCurrentErrno() { - RuntimeScalar errnoVar = GlobalVariable.globalVariables.get("main::!"); + RuntimeScalar errnoVar = GlobalVariable.getGlobalVariablesMap().get("main::!"); return errnoVar != null ? errnoVar.getInt() : 0; } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrnoVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrnoVariable.java index 97e7911af..54fd51467 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ErrnoVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ErrnoVariable.java @@ -331,25 +331,29 @@ public void clear() { set(0); } - // Stack to save errno/message during local() - private static final java.util.Stack errnoStack = new java.util.Stack<>(); - private static final java.util.Stack messageStack = new java.util.Stack<>(); + // Errno stacks are now held per-PerlRuntime. + private static java.util.Stack errnoStack() { + return PerlRuntime.current().errnoStack; + } + private static java.util.Stack messageStack() { + return PerlRuntime.current().errnoMessageStack; + } @Override public void dynamicSaveState() { - errnoStack.push(new int[]{errno}); - messageStack.push(message); + errnoStack().push(new int[]{errno}); + messageStack().push(message); super.dynamicSaveState(); } @Override public void dynamicRestoreState() { super.dynamicRestoreState(); - if (!errnoStack.isEmpty()) { - errno = errnoStack.pop()[0]; + if (!errnoStack().isEmpty()) { + errno = errnoStack().pop()[0]; } - if (!messageStack.isEmpty()) { - message = messageStack.pop(); + if (!messageStack().isEmpty()) { + message = messageStack().pop(); } } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java index f25e674a2..fca481886 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalContext.java @@ -48,9 +48,9 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { GlobalVariable.getGlobalVariable(varName); } // $^N - last capture group closed (not yet implemented, but must be read-only) - GlobalVariable.globalVariables.put(encodeSpecialVar("N"), new RuntimeScalarReadOnly()); + GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("N"), new RuntimeScalarReadOnly()); // $^S - current state of the interpreter (undef=compiling, 0=not in eval, 1=in eval) - GlobalVariable.globalVariables.put("main::" + Character.toString('S' - 'A' + 1), + GlobalVariable.getGlobalVariablesMap().put("main::" + Character.toString('S' - 'A' + 1), new ScalarSpecialVariable(ScalarSpecialVariable.Id.EVAL_STATE)); GlobalVariable.getGlobalVariable("main::" + Character.toString('O' - 'A' + 1)).set(SystemUtils.getPerlOsName()); // initialize $^O GlobalVariable.getGlobalVariable("main::" + Character.toString('V' - 'A' + 1)).set(Configuration.getPerlVersionVString()); // initialize $^V @@ -76,60 +76,60 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { GlobalVariable.getGlobalVariable("main::\"").set(" "); // initialize $" to " " GlobalVariable.getGlobalVariable("main::a"); // initialize $a to "undef" GlobalVariable.getGlobalVariable("main::b"); // initialize $b to "undef" - GlobalVariable.globalVariables.put("main::!", new ErrnoVariable()); // initialize $! with dualvar support + GlobalVariable.getGlobalVariablesMap().put("main::!", new ErrnoVariable()); // initialize $! with dualvar support // Initialize $, (output field separator) with special variable class - if (!GlobalVariable.globalVariables.containsKey("main::,")) { + if (!GlobalVariable.getGlobalVariablesMap().containsKey("main::,")) { var ofs = new OutputFieldSeparator(); ofs.set(""); - GlobalVariable.globalVariables.put("main::,", ofs); + GlobalVariable.getGlobalVariablesMap().put("main::,", ofs); } - GlobalVariable.globalVariables.put("main::|", new OutputAutoFlushVariable()); + GlobalVariable.getGlobalVariablesMap().put("main::|", new OutputAutoFlushVariable()); // Only set $\ if it hasn't been set yet - prevents overwriting during re-entrant calls - if (!GlobalVariable.globalVariables.containsKey("main::\\")) { + if (!GlobalVariable.getGlobalVariablesMap().containsKey("main::\\")) { var ors = new OutputRecordSeparator(); ors.set(compilerOptions.outputRecordSeparator); // initialize $\ - GlobalVariable.globalVariables.put("main::\\", ors); + GlobalVariable.getGlobalVariablesMap().put("main::\\", ors); } - GlobalVariable.getGlobalVariable("main::$").set(ProcessHandle.current().pid()); // initialize `$$` to process id + GlobalVariable.getGlobalVariable("main::$").set(PerlRuntime.current().pid); // initialize `$$` to per-runtime unique pid GlobalVariable.getGlobalVariable("main::?"); // Only set $0 if it hasn't been set yet - prevents overwriting during re-entrant calls // (e.g., when require() is called during module initialization) - if (!GlobalVariable.globalVariables.containsKey("main::0")) { + if (!GlobalVariable.getGlobalVariablesMap().containsKey("main::0")) { GlobalVariable.getGlobalVariable("main::0").set(compilerOptions.fileName); } GlobalVariable.getGlobalVariable(GLOBAL_PHASE).set("RUN"); // ${^GLOBAL_PHASE} // ${^TAINT} - set to 1 if -T (taint mode) was specified, 0 otherwise // Only initialize if not already set (to avoid overwriting during re-initialization) String taintVarName = encodeSpecialVar("TAINT"); - if (!GlobalVariable.globalVariables.containsKey(taintVarName) || - (compilerOptions.taintMode && GlobalVariable.globalVariables.get(taintVarName) == RuntimeScalarCache.scalarZero)) { - GlobalVariable.globalVariables.put(taintVarName, + if (!GlobalVariable.getGlobalVariablesMap().containsKey(taintVarName) || + (compilerOptions.taintMode && GlobalVariable.getGlobalVariablesMap().get(taintVarName) == RuntimeScalarCache.scalarZero)) { + GlobalVariable.getGlobalVariablesMap().put(taintVarName, compilerOptions.taintMode ? RuntimeScalarCache.scalarOne : RuntimeScalarCache.scalarZero); } - GlobalVariable.globalVariables.put("main::>", new ScalarSpecialVariable(ScalarSpecialVariable.Id.EFFECTIVE_UID)); // $> - effective UID (lazy) - GlobalVariable.globalVariables.put("main::<", new ScalarSpecialVariable(ScalarSpecialVariable.Id.REAL_UID)); // $< - real UID (lazy) + GlobalVariable.getGlobalVariablesMap().put("main::>", new ScalarSpecialVariable(ScalarSpecialVariable.Id.EFFECTIVE_UID)); // $> - effective UID (lazy) + GlobalVariable.getGlobalVariablesMap().put("main::<", new ScalarSpecialVariable(ScalarSpecialVariable.Id.REAL_UID)); // $< - real UID (lazy) GlobalVariable.getGlobalVariable("main::;").set("\034"); // initialize $; (SUBSEP) to \034 - 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.getGlobalVariablesMap().put("main::(", new ScalarSpecialVariable(ScalarSpecialVariable.Id.REAL_GID)); // $( - real GID (lazy) + GlobalVariable.getGlobalVariablesMap().put("main::)", new ScalarSpecialVariable(ScalarSpecialVariable.Id.EFFECTIVE_GID)); // $) - effective GID (lazy) GlobalVariable.getGlobalVariable("main::="); // TODO GlobalVariable.getGlobalVariable("main::^"); // TODO GlobalVariable.getGlobalVariable("main:::"); // TODO // Only set $/ if it hasn't been set yet - prevents overwriting during re-entrant calls - if (!GlobalVariable.globalVariables.containsKey("main::/")) { - GlobalVariable.globalVariables.put("main::/", new InputRecordSeparator(compilerOptions.inputRecordSeparator)); // initialize $/ + if (!GlobalVariable.getGlobalVariablesMap().containsKey("main::/")) { + GlobalVariable.getGlobalVariablesMap().put("main::/", new InputRecordSeparator(compilerOptions.inputRecordSeparator)); // initialize $/ } - GlobalVariable.globalVariables.put("main::`", new ScalarSpecialVariable(ScalarSpecialVariable.Id.PREMATCH)); - GlobalVariable.globalVariables.put("main::&", new ScalarSpecialVariable(ScalarSpecialVariable.Id.MATCH)); - GlobalVariable.globalVariables.put("main::'", new ScalarSpecialVariable(ScalarSpecialVariable.Id.POSTMATCH)); - GlobalVariable.globalVariables.put("main::.", new ScalarSpecialVariable(ScalarSpecialVariable.Id.INPUT_LINE_NUMBER)); // $. - GlobalVariable.globalVariables.put("main::+", new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_PAREN_MATCH)); - GlobalVariable.globalVariables.put(encodeSpecialVar("LAST_SUCCESSFUL_PATTERN"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_SUCCESSFUL_PATTERN)); - GlobalVariable.globalVariables.put(encodeSpecialVar("LAST_FH"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_FH)); // $^LAST_FH - GlobalVariable.globalVariables.put(encodeSpecialVar("H"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.HINTS)); // $^H - compile-time hints + GlobalVariable.getGlobalVariablesMap().put("main::`", new ScalarSpecialVariable(ScalarSpecialVariable.Id.PREMATCH)); + GlobalVariable.getGlobalVariablesMap().put("main::&", new ScalarSpecialVariable(ScalarSpecialVariable.Id.MATCH)); + GlobalVariable.getGlobalVariablesMap().put("main::'", new ScalarSpecialVariable(ScalarSpecialVariable.Id.POSTMATCH)); + GlobalVariable.getGlobalVariablesMap().put("main::.", new ScalarSpecialVariable(ScalarSpecialVariable.Id.INPUT_LINE_NUMBER)); // $. + GlobalVariable.getGlobalVariablesMap().put("main::+", new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_PAREN_MATCH)); + GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("LAST_SUCCESSFUL_PATTERN"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_SUCCESSFUL_PATTERN)); + GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("LAST_FH"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_FH)); // $^LAST_FH + GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("H"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.HINTS)); // $^H - compile-time hints // $^R is writable, not read-only - initialize as regular variable instead of ScalarSpecialVariable - // GlobalVariable.globalVariables.put(encodeSpecialVar("R"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_REGEXP_CODE_RESULT)); // $^R + // GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("R"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_REGEXP_CODE_RESULT)); // $^R GlobalVariable.getGlobalVariable(encodeSpecialVar("R")); // initialize $^R to "undef" - writable variable GlobalVariable.getGlobalVariable(encodeSpecialVar("A")).set(""); // initialize $^A to "" - format accumulator variable GlobalVariable.getGlobalVariable(encodeSpecialVar("P")).set(0); // initialize $^P to 0 - debugger flags @@ -139,19 +139,19 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { GlobalVariable.getGlobalVariable(encodeSpecialVar("I")).set( compilerOptions.inPlaceExtension != null ? compilerOptions.inPlaceExtension : ""); } - GlobalVariable.globalVariables.put(encodeSpecialVar("LAST_SUCCESSFUL_PATTERN"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_SUCCESSFUL_PATTERN)); - GlobalVariable.globalVariables.put(encodeSpecialVar("LAST_FH"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_FH)); // $^LAST_FH + GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("LAST_SUCCESSFUL_PATTERN"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_SUCCESSFUL_PATTERN)); + GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("LAST_FH"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.LAST_FH)); // $^LAST_FH GlobalVariable.getGlobalVariable(encodeSpecialVar("UNICODE")).set(0); // initialize $^UNICODE to 0 - `-C` unicode flags - GlobalVariable.globalVariables.put(encodeSpecialVar("PREMATCH"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.P_PREMATCH)); - GlobalVariable.globalVariables.put(encodeSpecialVar("MATCH"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.P_MATCH)); - GlobalVariable.globalVariables.put(encodeSpecialVar("POSTMATCH"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.P_POSTMATCH)); + GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("PREMATCH"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.P_PREMATCH)); + GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("MATCH"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.P_MATCH)); + GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("POSTMATCH"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.P_POSTMATCH)); GlobalVariable.getGlobalVariable(encodeSpecialVar("SAFE_LOCALES")); // TODO // Initialize additional magic scalar variables that tests expect to exist at startup GlobalVariable.getGlobalVariable(encodeSpecialVar("UTF8LOCALE")); // ${^UTF8LOCALE} - GlobalVariable.globalVariables.put(encodeSpecialVar("WARNING_BITS"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.WARNING_BITS)); // ${^WARNING_BITS} + GlobalVariable.getGlobalVariablesMap().put(encodeSpecialVar("WARNING_BITS"), new ScalarSpecialVariable(ScalarSpecialVariable.Id.WARNING_BITS)); // ${^WARNING_BITS} GlobalVariable.getGlobalVariable(encodeSpecialVar("UTF8CACHE")).set(0); // ${^UTF8CACHE} GlobalVariable.getGlobalVariable("main::[").set(0); // $[ (array base, deprecated) GlobalVariable.getGlobalVariable("main::~"); // $~ (current format name) @@ -180,7 +180,7 @@ public static void initializeGlobals(CompilerOptions compilerOptions) { // Initialize hashes // %SIG uses a special hash that auto-qualifies handler names for known signals - GlobalVariable.globalHashes.put("main::SIG", new RuntimeSigHash()); + GlobalVariable.getGlobalHashesMap().put("main::SIG", new RuntimeSigHash()); GlobalVariable.getGlobalHash(encodeSpecialVar("H")); GlobalVariable.getGlobalHash("main::+").elements = new HashSpecialVariable(HashSpecialVariable.Id.CAPTURE); // regex %+ GlobalVariable.getGlobalHash("main::-").elements = new HashSpecialVariable(HashSpecialVariable.Id.CAPTURE_ALL); // regex %- diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalDestruction.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalDestruction.java index 40fd6ca79..25863cc65 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalDestruction.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalDestruction.java @@ -20,12 +20,12 @@ public static void runGlobalDestruction() { GlobalVariable.getGlobalVariable(GlobalContext.GLOBAL_PHASE).set("DESTRUCT"); // Walk all global scalars - for (RuntimeScalar val : GlobalVariable.globalVariables.values()) { + for (RuntimeScalar val : GlobalVariable.getGlobalVariablesMap().values()) { destroyIfTracked(val); } // Walk global arrays for blessed ref elements - for (RuntimeArray arr : GlobalVariable.globalArrays.values()) { + for (RuntimeArray arr : GlobalVariable.getGlobalArraysMap().values()) { // Skip tied arrays — iterating them calls FETCHSIZE/FETCH on the // tie object, which may already be destroyed or invalid at global // destruction time (e.g., broken ties from eval+last). @@ -36,7 +36,7 @@ public static void runGlobalDestruction() { } // Walk global hashes for blessed ref values - for (RuntimeHash hash : GlobalVariable.globalHashes.values()) { + for (RuntimeHash hash : GlobalVariable.getGlobalHashesMap().values()) { // Skip tied hashes — iterating them dispatches through FIRSTKEY/ // NEXTKEY/FETCH which may fail if the tie object is already gone. if (hash.type == RuntimeHash.TIED_HASH) continue; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeArray.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeArray.java index 96918b359..02a0a0bb0 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeArray.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeArray.java @@ -16,7 +16,11 @@ * and {@link GlobalRuntimeScalar} for scalars. */ public class GlobalRuntimeArray implements DynamicState { - private static final Stack localizedStack = new Stack<>(); + // Localized stack is now held per-PerlRuntime. + @SuppressWarnings("unchecked") + private static Stack localizedStack() { + return (Stack) (Stack) PerlRuntime.current().globalArrayLocalizedStack; + } private final String fullName; public GlobalRuntimeArray(String fullName) { @@ -40,37 +44,37 @@ public static RuntimeArray makeLocal(String fullName) { @Override public void dynamicSaveState() { // Save the current array reference from the global map - RuntimeArray original = GlobalVariable.globalArrays.get(fullName); - localizedStack.push(new SavedGlobalArrayState(fullName, original)); + RuntimeArray original = GlobalVariable.getGlobalArraysMap().get(fullName); + localizedStack().push(new SavedGlobalArrayState(fullName, original)); // Install a fresh empty array in the global map RuntimeArray newLocal = new RuntimeArray(); - GlobalVariable.globalArrays.put(fullName, newLocal); + GlobalVariable.getGlobalArraysMap().put(fullName, newLocal); // Update glob aliases so they all point to the new local array java.util.List aliasGroup = GlobalVariable.getGlobAliasGroup(fullName); for (String alias : aliasGroup) { if (!alias.equals(fullName)) { - GlobalVariable.globalArrays.put(alias, newLocal); + GlobalVariable.getGlobalArraysMap().put(alias, newLocal); } } } @Override public void dynamicRestoreState() { - if (!localizedStack.isEmpty()) { - SavedGlobalArrayState saved = localizedStack.peek(); + if (!localizedStack().isEmpty()) { + SavedGlobalArrayState saved = localizedStack().peek(); if (saved.fullName.equals(this.fullName)) { - localizedStack.pop(); + localizedStack().pop(); // Restore the original array reference in the global map - GlobalVariable.globalArrays.put(saved.fullName, saved.originalArray); + GlobalVariable.getGlobalArraysMap().put(saved.fullName, saved.originalArray); // Restore glob aliases java.util.List aliasGroup = GlobalVariable.getGlobAliasGroup(saved.fullName); for (String alias : aliasGroup) { if (!alias.equals(saved.fullName)) { - GlobalVariable.globalArrays.put(alias, saved.originalArray); + GlobalVariable.getGlobalArraysMap().put(alias, saved.originalArray); } } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeHash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeHash.java index 40df14826..6d37165a8 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeHash.java @@ -12,7 +12,11 @@ *

    Follows the same pattern as {@link GlobalRuntimeScalar} for scalars. */ public class GlobalRuntimeHash implements DynamicState { - private static final Stack localizedStack = new Stack<>(); + // Localized stack is now held per-PerlRuntime. + @SuppressWarnings("unchecked") + private static Stack localizedStack() { + return (Stack) (Stack) PerlRuntime.current().globalHashLocalizedStack; + } private final String fullName; public GlobalRuntimeHash(String fullName) { @@ -36,37 +40,37 @@ public static RuntimeHash makeLocal(String fullName) { @Override public void dynamicSaveState() { // Save the current hash reference from the global map - RuntimeHash original = GlobalVariable.globalHashes.get(fullName); - localizedStack.push(new SavedGlobalHashState(fullName, original)); + RuntimeHash original = GlobalVariable.getGlobalHashesMap().get(fullName); + localizedStack().push(new SavedGlobalHashState(fullName, original)); // Install a fresh empty hash in the global map RuntimeHash newLocal = new RuntimeHash(); - GlobalVariable.globalHashes.put(fullName, newLocal); + GlobalVariable.getGlobalHashesMap().put(fullName, newLocal); // Update glob aliases so they all point to the new local hash java.util.List aliasGroup = GlobalVariable.getGlobAliasGroup(fullName); for (String alias : aliasGroup) { if (!alias.equals(fullName)) { - GlobalVariable.globalHashes.put(alias, newLocal); + GlobalVariable.getGlobalHashesMap().put(alias, newLocal); } } } @Override public void dynamicRestoreState() { - if (!localizedStack.isEmpty()) { - SavedGlobalHashState saved = localizedStack.peek(); + if (!localizedStack().isEmpty()) { + SavedGlobalHashState saved = localizedStack().peek(); if (saved.fullName.equals(this.fullName)) { - localizedStack.pop(); + localizedStack().pop(); // Restore the original hash reference in the global map - GlobalVariable.globalHashes.put(saved.fullName, saved.originalHash); + GlobalVariable.getGlobalHashesMap().put(saved.fullName, saved.originalHash); // Restore glob aliases java.util.List aliasGroup = GlobalVariable.getGlobAliasGroup(saved.fullName); for (String alias : aliasGroup) { if (!alias.equals(saved.fullName)) { - GlobalVariable.globalHashes.put(alias, saved.originalHash); + GlobalVariable.getGlobalHashesMap().put(alias, saved.originalHash); } } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeScalar.java index e4f505b65..6a129873f 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalRuntimeScalar.java @@ -8,8 +8,11 @@ * global symbol table and restoring it when the context exits. */ public class GlobalRuntimeScalar extends RuntimeScalar { - // Stack to store the previous values when localized - private static final Stack localizedStack = new Stack<>(); + // Localized stack is now held per-PerlRuntime. + @SuppressWarnings("unchecked") + private static Stack localizedStack() { + return (Stack) (Stack) PerlRuntime.current().globalScalarLocalizedStack; + } private final String fullName; public GlobalRuntimeScalar(String fullName) { @@ -44,9 +47,9 @@ public static RuntimeScalar makeLocal(String fullName) { @Override public void dynamicSaveState() { // Save the current global reference - var originalVariable = GlobalVariable.globalVariables.get(fullName); + var originalVariable = GlobalVariable.getGlobalVariablesMap().get(fullName); - localizedStack.push(new SavedGlobalState(fullName, originalVariable)); + localizedStack().push(new SavedGlobalState(fullName, originalVariable)); // Create a new variable for the localized scope. // For output separator variables, create the matching special type so that @@ -64,7 +67,7 @@ public void dynamicSaveState() { } // Replace this variable in the global symbol table with the new one - GlobalVariable.globalVariables.put(fullName, newLocal); + GlobalVariable.getGlobalVariablesMap().put(fullName, newLocal); // Also update all glob aliases to point to the new local variable. // This implements Perl 5 semantics where after `*verbose = *Verbose`, @@ -72,22 +75,23 @@ public void dynamicSaveState() { java.util.List aliasGroup = GlobalVariable.getGlobAliasGroup(fullName); for (String alias : aliasGroup) { if (!alias.equals(fullName)) { - GlobalVariable.globalVariables.put(alias, newLocal); + GlobalVariable.getGlobalVariablesMap().put(alias, newLocal); } } } @Override public void dynamicRestoreState() { - if (!localizedStack.isEmpty()) { - SavedGlobalState saved = localizedStack.peek(); + Stack stack = localizedStack(); + if (!stack.isEmpty()) { + SavedGlobalState saved = stack.peek(); if (saved.fullName.equals(this.fullName)) { - localizedStack.pop(); + stack.pop(); // Decrement refCount of the CURRENT (local) value being displaced. // Do NOT increment the restored value — it already has the correct // refCount from its original counting. - RuntimeScalar currentVar = GlobalVariable.globalVariables.get(saved.fullName); + RuntimeScalar currentVar = GlobalVariable.getGlobalVariablesMap().get(saved.fullName); if (currentVar != null && (currentVar.type & RuntimeScalarType.REFERENCE_BIT) != 0 && currentVar.value instanceof RuntimeBase displacedBase @@ -104,13 +108,13 @@ public void dynamicRestoreState() { } // Restore the original variable in the global symbol table - GlobalVariable.globalVariables.put(saved.fullName, saved.originalVariable); + GlobalVariable.getGlobalVariablesMap().put(saved.fullName, saved.originalVariable); // Also restore all glob aliases to the original shared variable java.util.List aliasGroup = GlobalVariable.getGlobAliasGroup(saved.fullName); for (String alias : aliasGroup) { if (!alias.equals(saved.fullName)) { - GlobalVariable.globalVariables.put(alias, saved.originalVariable); + GlobalVariable.getGlobalVariablesMap().put(alias, saved.originalVariable); } } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java index 6fdcb25ab..dc69a365e 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/GlobalVariable.java @@ -6,7 +6,6 @@ import org.perlonjava.runtime.mro.InheritanceResolver; import java.util.HashMap; -import java.util.HashSet; import java.util.Map; import java.util.Set; import java.util.regex.Matcher; @@ -21,81 +20,94 @@ * the existence of these global entities, initializing them as necessary. */ public class GlobalVariable { - // Global variables and subroutines - public static final Map globalVariables = new HashMap<>(); - public static final Map globalArrays = new HashMap<>(); - public static final Map globalHashes = new HashMap<>(); - // Cache for package existence checks - public static final Map packageExistsCache = new HashMap<>(); - // isSubs: Tracks subroutines declared via 'use subs' pragma (e.g., use subs 'hex') - // Maps fully-qualified names (package::subname) to indicate they should be called - // as user-defined subroutines instead of built-in operators - public static final Map isSubs = new HashMap<>(); - public static final Map globalCodeRefs = new HashMap<>(); - static final Map globalIORefs = new HashMap<>(); - static final Map globalFormatRefs = new HashMap<>(); - - // Pinned code references: RuntimeScalars that were accessed at compile time - // and should survive stash deletion. This matches Perl's behavior where - // compiled bytecode holds direct references to CVs that survive stash deletion. - private static final Map pinnedCodeRefs = new HashMap<>(); - - // Stash aliasing: `*{Dst::} = *{Src::}` effectively makes Dst:: symbol table - // behave like Src:: for method lookup and stash operations. - // We keep this separate from globalCodeRefs/globalVariables so existing references - // to Dst:: symbols can still point to their original objects. - static final Map stashAliases = new HashMap<>(); - - // Glob aliasing: `*a = *b` makes a and b share the same glob. - // Maps glob names to their canonical (target) name. - // When looking up or assigning to glob slots, we resolve through this map. - static final Map globAliases = new HashMap<>(); - - // Flags used by operator override - // globalGlobs: Tracks typeglob assignments (e.g., *CORE::GLOBAL::hex = sub {...}) - // Used to detect when built-in operators have been globally overridden - static final Map globalGlobs = new HashMap<>(); - // Global class loader for all generated classes - not final so we can replace it - public static CustomClassLoader globalClassLoader = - new CustomClassLoader(GlobalVariable.class.getClassLoader()); - - // Regular expression for regex variables like $main::1 - static Pattern regexVariablePattern = Pattern.compile("^main::(\\d+)$"); + // ---- Static accessor methods delegating to PerlRuntime.current() ---- + // These replace the former static fields. External code should use these methods. + + public static Map getGlobalVariablesMap() { + return PerlRuntime.current().globalVariables; + } + + public static Map getGlobalArraysMap() { + return PerlRuntime.current().globalArrays; + } + + public static Map getGlobalHashesMap() { + return PerlRuntime.current().globalHashes; + } + + public static Map getPackageExistsCacheMap() { + return PerlRuntime.current().packageExistsCache; + } + + public static Map getIsSubsMap() { + return PerlRuntime.current().isSubs; + } + + public static Map getGlobalCodeRefsMap() { + return PerlRuntime.current().globalCodeRefs; + } + + public static Map getGlobalIORefsMap() { + return PerlRuntime.current().globalIORefs; + } - // Track explicitly declared global variables (via use vars, our, Exporter glob assignment). - // Separate from globalVariables/globalArrays/globalHashes to distinguish intentional - // declarations from auto-vivification under 'no strict'. Used by strict vars check - // for single-letter variable names like $A-$Z (excluding $a/$b). - private static final Set declaredGlobalVariables = new HashSet<>(); - private static final Set declaredGlobalArrays = new HashSet<>(); - private static final Set declaredGlobalHashes = new HashSet<>(); + public static Map getGlobalFormatRefsMap() { + return PerlRuntime.current().globalFormatRefs; + } + + public static Map getPinnedCodeRefsMap() { + return PerlRuntime.current().pinnedCodeRefs; + } + + public static Map getStashAliasesMap() { + return PerlRuntime.current().stashAliases; + } + + public static Map getGlobAliasesMap() { + return PerlRuntime.current().globAliases; + } + + public static Map getGlobalGlobsMap() { + return PerlRuntime.current().globalGlobs; + } + + public static CustomClassLoader getGlobalClassLoader() { + return PerlRuntime.current().globalClassLoader; + } + + public static void setGlobalClassLoader(CustomClassLoader loader) { + PerlRuntime.current().globalClassLoader = loader; + } + + // Regular expression for regex variables like $main::1 (compile-time constant) + static Pattern regexVariablePattern = Pattern.compile("^main::(\\d+)$"); /** * Marks a global variable as explicitly declared (e.g., via use vars, Exporter import). */ public static void declareGlobalVariable(String key) { - declaredGlobalVariables.add(key); + PerlRuntime.current().declaredGlobalVariables.add(key); } /** * Marks a global array as explicitly declared. */ public static void declareGlobalArray(String key) { - declaredGlobalArrays.add(key); + PerlRuntime.current().declaredGlobalArrays.add(key); } /** * Marks a global hash as explicitly declared. */ public static void declareGlobalHash(String key) { - declaredGlobalHashes.add(key); + PerlRuntime.current().declaredGlobalHashes.add(key); } /** * Checks if a global variable was explicitly declared (not just auto-vivified). */ public static boolean isDeclaredGlobalVariable(String key) { - return declaredGlobalVariables.contains(key) + return PerlRuntime.current().declaredGlobalVariables.contains(key) || key.endsWith("::a") || key.endsWith("::b"); } @@ -103,14 +115,14 @@ public static boolean isDeclaredGlobalVariable(String key) { * Checks if a global array was explicitly declared. */ public static boolean isDeclaredGlobalArray(String key) { - return declaredGlobalArrays.contains(key); + return PerlRuntime.current().declaredGlobalArrays.contains(key); } /** * Checks if a global hash was explicitly declared. */ public static boolean isDeclaredGlobalHash(String key) { - return declaredGlobalHashes.contains(key); + return PerlRuntime.current().declaredGlobalHashes.contains(key); } /** @@ -118,21 +130,22 @@ public static boolean isDeclaredGlobalHash(String key) { * Also destroys and recreates the global class loader to allow GC of old classes. */ public static void resetAllGlobals() { + PerlRuntime rt = PerlRuntime.current(); // Clear all global state - globalVariables.clear(); - globalArrays.clear(); - globalHashes.clear(); - globalCodeRefs.clear(); - pinnedCodeRefs.clear(); - globalIORefs.clear(); - globalFormatRefs.clear(); - globalGlobs.clear(); - isSubs.clear(); - stashAliases.clear(); - globAliases.clear(); - declaredGlobalVariables.clear(); - declaredGlobalArrays.clear(); - declaredGlobalHashes.clear(); + rt.globalVariables.clear(); + rt.globalArrays.clear(); + rt.globalHashes.clear(); + rt.globalCodeRefs.clear(); + rt.pinnedCodeRefs.clear(); + rt.globalIORefs.clear(); + rt.globalFormatRefs.clear(); + rt.globalGlobs.clear(); + rt.isSubs.clear(); + rt.stashAliases.clear(); + rt.globAliases.clear(); + rt.declaredGlobalVariables.clear(); + rt.declaredGlobalArrays.clear(); + rt.declaredGlobalHashes.clear(); clearPackageCache(); RuntimeCode.clearCaches(); @@ -140,9 +153,9 @@ public static void resetAllGlobals() { // Clear special blocks (INIT, END, CHECK, UNITCHECK) to prevent stale code references. // When the classloader is replaced, old INIT blocks may reference evalTags that no longer // exist in the cleared evalContext, causing "ctx is null" errors. - SpecialBlock.initBlocks.elements.clear(); - SpecialBlock.endBlocks.elements.clear(); - SpecialBlock.checkBlocks.elements.clear(); + SpecialBlock.getInitBlocks().elements.clear(); + SpecialBlock.getEndBlocks().elements.clear(); + SpecialBlock.getCheckBlocks().elements.clear(); // Method resolution caches can grow across test scripts. InheritanceResolver.invalidateCache(); @@ -162,23 +175,24 @@ public static void resetAllGlobals() { // Destroy the old classloader and create a new one // This allows the old generated classes to be garbage collected - globalClassLoader = new CustomClassLoader(GlobalVariable.class.getClassLoader()); + rt.globalClassLoader = new CustomClassLoader(GlobalVariable.class.getClassLoader()); } public static void setStashAlias(String dstNamespace, String srcNamespace) { String dst = dstNamespace.endsWith("::") ? dstNamespace : dstNamespace + "::"; String src = srcNamespace.endsWith("::") ? srcNamespace : srcNamespace + "::"; - stashAliases.put(dst, src); + PerlRuntime.current().stashAliases.put(dst, src); } public static void clearStashAlias(String namespace) { String key = namespace.endsWith("::") ? namespace : namespace + "::"; - stashAliases.remove(key); + PerlRuntime.current().stashAliases.remove(key); } public static String resolveStashAlias(String namespace) { + PerlRuntime rt = PerlRuntime.current(); String key = namespace.endsWith("::") ? namespace : namespace + "::"; - String aliased = stashAliases.get(key); + String aliased = rt.stashAliases.get(key); if (aliased == null) { return namespace; } @@ -196,13 +210,14 @@ public static String resolveStashAlias(String namespace) { public static void setGlobAlias(String fromGlob, String toGlob) { // Find the canonical name for toGlob (in case it's already an alias) String canonical = resolveGlobAlias(toGlob); + PerlRuntime rt = PerlRuntime.current(); // Don't create self-loops if (!fromGlob.equals(canonical)) { - globAliases.put(fromGlob, canonical); + rt.globAliases.put(fromGlob, canonical); } // Also ensure toGlob points to the canonical name (unless it would create a self-loop) if (!toGlob.equals(canonical) && !toGlob.equals(fromGlob)) { - globAliases.put(toGlob, canonical); + rt.globAliases.put(toGlob, canonical); } } @@ -211,7 +226,7 @@ public static void setGlobAlias(String fromGlob, String toGlob) { * If the glob is aliased, returns the target name; otherwise returns the input. */ public static String resolveGlobAlias(String globName) { - String aliased = globAliases.get(globName); + String aliased = PerlRuntime.current().globAliases.get(globName); if (aliased != null && !aliased.equals(globName)) { // Follow the chain in case of multiple aliases return resolveGlobAlias(aliased); @@ -227,7 +242,7 @@ public static java.util.List getGlobAliasGroup(String globName) { String canonical = resolveGlobAlias(globName); java.util.List group = new java.util.ArrayList<>(); group.add(canonical); - for (Map.Entry entry : globAliases.entrySet()) { + for (Map.Entry entry : PerlRuntime.current().globAliases.entrySet()) { if (resolveGlobAlias(entry.getKey()).equals(canonical) && !group.contains(entry.getKey())) { group.add(entry.getKey()); } @@ -243,7 +258,8 @@ public static java.util.List getGlobAliasGroup(String globName) { * @return The RuntimeScalar representing the global variable. */ public static RuntimeScalar getGlobalVariable(String key) { - RuntimeScalar var = globalVariables.get(key); + PerlRuntime rt = PerlRuntime.current(); + RuntimeScalar var = rt.globalVariables.get(key); if (var == null) { // Need to initialize global variable Matcher matcher = regexVariablePattern.matcher(key); @@ -259,19 +275,20 @@ public static RuntimeScalar getGlobalVariable(String key) { // Normal "non-magic" global variable var = new RuntimeScalar(); } - globalVariables.put(key, var); + rt.globalVariables.put(key, var); } return var; } public static RuntimeScalar aliasGlobalVariable(String key, String to) { - RuntimeScalar var = globalVariables.get(to); - globalVariables.put(key, var); + PerlRuntime rt = PerlRuntime.current(); + RuntimeScalar var = rt.globalVariables.get(to); + rt.globalVariables.put(key, var); return var; } public static void aliasGlobalVariable(String key, RuntimeScalar var) { - globalVariables.put(key, var); + PerlRuntime.current().globalVariables.put(key, var); } /** @@ -291,7 +308,7 @@ public static void setGlobalVariable(String key, String value) { * @return True if the global variable exists, false otherwise. */ public static boolean existsGlobalVariable(String key) { - return globalVariables.containsKey(key) + return PerlRuntime.current().globalVariables.containsKey(key) || key.endsWith("::a") // $a, $b always exist || key.endsWith("::b"); } @@ -303,7 +320,7 @@ public static boolean existsGlobalVariable(String key) { * @return True if the variable exists and is defined, false otherwise. */ public static boolean isGlobalVariableDefined(String key) { - RuntimeScalar var = globalVariables.get(key); + RuntimeScalar var = PerlRuntime.current().globalVariables.get(key); return var != null && var.getDefinedBoolean(); } @@ -314,7 +331,7 @@ public static boolean isGlobalVariableDefined(String key) { * @return The removed RuntimeScalar, or null if it did not exist. */ public static RuntimeScalar removeGlobalVariable(String key) { - return globalVariables.remove(key); + return PerlRuntime.current().globalVariables.remove(key); } /** @@ -324,10 +341,11 @@ public static RuntimeScalar removeGlobalVariable(String key) { * @return The RuntimeArray representing the global array. */ public static RuntimeArray getGlobalArray(String key) { - RuntimeArray var = globalArrays.get(key); + PerlRuntime rt = PerlRuntime.current(); + RuntimeArray var = rt.globalArrays.get(key); if (var == null) { var = new RuntimeArray(); - globalArrays.put(key, var); + rt.globalArrays.put(key, var); } return var; } @@ -339,7 +357,7 @@ public static RuntimeArray getGlobalArray(String key) { * @return True if the global array exists, false otherwise. */ public static boolean existsGlobalArray(String key) { - return globalArrays.containsKey(key); + return PerlRuntime.current().globalArrays.containsKey(key); } /** @@ -349,7 +367,7 @@ public static boolean existsGlobalArray(String key) { * @return The removed RuntimeArray, or null if it did not exist. */ public static RuntimeArray removeGlobalArray(String key) { - return globalArrays.remove(key); + return PerlRuntime.current().globalArrays.remove(key); } /** @@ -359,7 +377,8 @@ public static RuntimeArray removeGlobalArray(String key) { * @return The RuntimeHash representing the global hash. */ public static RuntimeHash getGlobalHash(String key) { - RuntimeHash var = globalHashes.get(key); + PerlRuntime rt = PerlRuntime.current(); + RuntimeHash var = rt.globalHashes.get(key); if (var == null) { // Check if this is a package stash (ends with ::) if (key.endsWith("::")) { @@ -367,7 +386,7 @@ public static RuntimeHash getGlobalHash(String key) { } else { var = new RuntimeHash(); } - globalHashes.put(key, var); + rt.globalHashes.put(key, var); } return var; } @@ -379,7 +398,7 @@ public static RuntimeHash getGlobalHash(String key) { * @return True if the global hash exists, false otherwise. */ public static boolean existsGlobalHash(String key) { - return globalHashes.containsKey(key); + return PerlRuntime.current().globalHashes.containsKey(key); } /** @@ -389,7 +408,7 @@ public static boolean existsGlobalHash(String key) { * @return The removed RuntimeHash, or null if it did not exist. */ public static RuntimeHash removeGlobalHash(String key) { - return globalHashes.remove(key); + return PerlRuntime.current().globalHashes.remove(key); } /** @@ -404,17 +423,18 @@ public static RuntimeScalar getGlobalCodeRef(String key) { if (key == null) { return new RuntimeScalar(); } + PerlRuntime rt = PerlRuntime.current(); // First check if we have a pinned reference that survives stash deletion - RuntimeScalar pinned = pinnedCodeRefs.get(key); + RuntimeScalar pinned = rt.pinnedCodeRefs.get(key); if (pinned != null) { // Return the pinned ref so compiled code keeps working, but do NOT - // re-add to globalCodeRefs. If it was deleted from the stash (e.g., by + // re-add to rt.globalCodeRefs. If it was deleted from the stash (e.g., by // namespace::clean), that deletion should be respected for method // resolution via can() and the inheritance hierarchy. return pinned; } - RuntimeScalar var = globalCodeRefs.get(key); + RuntimeScalar var = rt.globalCodeRefs.get(key); if (var == null) { var = new RuntimeScalar(); var.type = RuntimeScalarType.CODE; // value is null @@ -435,11 +455,11 @@ public static RuntimeScalar getGlobalCodeRef(String key) { // It will be set specifically for \&{string} patterns in createCodeReference var.value = runtimeCode; - globalCodeRefs.put(key, var); + rt.globalCodeRefs.put(key, var); } // Pin the RuntimeScalar so it survives stash deletion - pinnedCodeRefs.put(key, var); + rt.pinnedCodeRefs.put(key, var); return var; } @@ -447,7 +467,7 @@ public static RuntimeScalar getGlobalCodeRef(String key) { /** * Retrieves a global code reference for the purpose of DEFINING code. * Unlike getGlobalCodeRef(), this also ensures the entry is visible in - * globalCodeRefs for method resolution via can() and the inheritance hierarchy. + * PerlRuntime.current().globalCodeRefs for method resolution via can() and the inheritance hierarchy. * Use this when assigning code to a glob (e.g., *Foo::bar = sub { ... }). * * @param key The key of the global code reference. @@ -455,9 +475,10 @@ public static RuntimeScalar getGlobalCodeRef(String key) { */ public static RuntimeScalar defineGlobalCodeRef(String key) { RuntimeScalar ref = getGlobalCodeRef(key); - // Ensure it's in globalCodeRefs so method resolution finds it - if (!globalCodeRefs.containsKey(key)) { - globalCodeRefs.put(key, ref); + PerlRuntime rt = PerlRuntime.current(); + // Ensure it's in rt.globalCodeRefs so method resolution finds it + if (!rt.globalCodeRefs.containsKey(key)) { + rt.globalCodeRefs.put(key, ref); } return ref; } @@ -469,7 +490,7 @@ public static RuntimeScalar defineGlobalCodeRef(String key) { * @return True if the global code reference exists, false otherwise. */ public static boolean existsGlobalCodeRef(String key) { - return globalCodeRefs.containsKey(key); + return PerlRuntime.current().globalCodeRefs.containsKey(key); } /** @@ -481,8 +502,9 @@ public static boolean existsGlobalCodeRef(String key) { * @param codeRef The new RuntimeScalar to pin (typically a new empty one). */ static void replacePinnedCodeRef(String key, RuntimeScalar codeRef) { - if (pinnedCodeRefs.containsKey(key)) { - pinnedCodeRefs.put(key, codeRef); + Map pinned = PerlRuntime.current().pinnedCodeRefs; + if (pinned.containsKey(key)) { + pinned.put(key, codeRef); } } @@ -494,7 +516,7 @@ static void replacePinnedCodeRef(String key, RuntimeScalar codeRef) { * @return True if the code reference exists and is defined, false otherwise. */ public static boolean isGlobalCodeRefDefined(String key) { - RuntimeScalar var = globalCodeRefs.get(key); + RuntimeScalar var = PerlRuntime.current().globalCodeRefs.get(key); if (var != null && var.type == RuntimeScalarType.CODE && var.value instanceof RuntimeCode runtimeCode) { return runtimeCode.defined(); } @@ -502,7 +524,7 @@ public static boolean isGlobalCodeRefDefined(String key) { } public static RuntimeScalar existsGlobalCodeRefAsScalar(String key) { - RuntimeScalar var = globalCodeRefs.get(key); + RuntimeScalar var = PerlRuntime.current().globalCodeRefs.get(key); if (var != null && var.type == RuntimeScalarType.CODE && var.value instanceof RuntimeCode runtimeCode) { // Use the RuntimeCode.defined() method to check if the subroutine actually exists // This checks methodHandle, constantValue, and compilerSupplier @@ -547,7 +569,7 @@ public static RuntimeScalar definedGlobalCodeRefAsScalar(String key) { } } - RuntimeScalar var = globalCodeRefs.get(key); + RuntimeScalar var = PerlRuntime.current().globalCodeRefs.get(key); if (var != null && var.type == RuntimeScalarType.CODE && var.value instanceof RuntimeCode runtimeCode) { return runtimeCode.defined() ? scalarTrue : scalarFalse; } @@ -579,7 +601,7 @@ public static RuntimeScalar definedGlobalCodeRefAsScalar(RuntimeScalar key, Stri public static RuntimeScalar deleteGlobalCodeRefAsScalar(String key) { - RuntimeScalar deleted = globalCodeRefs.remove(key); + RuntimeScalar deleted = PerlRuntime.current().globalCodeRefs.remove(key); return deleted != null ? deleted : scalarFalse; } @@ -610,7 +632,7 @@ public static RuntimeScalar deleteGlobalCodeRefAsScalar(RuntimeScalar key, Strin * @param prefix The namespace prefix (e.g., "Foo::") to clear. */ public static void clearPinnedCodeRefsForNamespace(String prefix) { - pinnedCodeRefs.keySet().removeIf(k -> k.startsWith(prefix)); + PerlRuntime.current().pinnedCodeRefs.keySet().removeIf(k -> k.startsWith(prefix)); } /** @@ -618,7 +640,7 @@ public static void clearPinnedCodeRefsForNamespace(String prefix) { * Should be called when new packages are loaded or code refs are modified. */ public static void clearPackageCache() { - packageExistsCache.clear(); + PerlRuntime.current().packageExistsCache.clear(); } /** @@ -628,8 +650,9 @@ public static void clearPackageCache() { * @return true if any methods exist in the class namespace */ public static boolean isPackageLoaded(String className) { + PerlRuntime rt = PerlRuntime.current(); // Check cache first - Boolean cached = packageExistsCache.get(className); + Boolean cached = rt.packageExistsCache.get(className); if (cached != null) { return cached; } @@ -641,11 +664,11 @@ public static boolean isPackageLoaded(String className) { // A key like "Foo::Bar::baz" belongs to package "Foo::Bar", not "Foo". // After stripping the prefix, the remaining part must NOT contain "::" // to be a direct member of this package. - boolean exists = globalCodeRefs.keySet().stream() + boolean exists = rt.globalCodeRefs.keySet().stream() .anyMatch(key -> key.startsWith(prefix) && !key.substring(prefix.length()).contains("::")); // Cache the result - packageExistsCache.put(className, exists); + rt.packageExistsCache.put(className, exists); return exists; } @@ -669,7 +692,7 @@ public static String resolveStashHashRedirect(String fullName) { int lastDoubleColon = fullName.lastIndexOf("::"); if (lastDoubleColon >= 0) { String pkgPart = fullName.substring(0, lastDoubleColon + 2); - RuntimeHash stashHash = globalHashes.get(pkgPart); + RuntimeHash stashHash = PerlRuntime.current().globalHashes.get(pkgPart); if (stashHash instanceof RuntimeStash stash && !stash.namespace.equals(pkgPart)) { String shortName = fullName.substring(lastDoubleColon + 2); return stash.namespace + shortName; @@ -690,10 +713,11 @@ public static String resolveStashHashRedirect(String fullName) { */ public static RuntimeGlob getGlobalIO(String key) { String resolvedKey = resolveStashHashRedirect(key); - RuntimeGlob glob = globalIORefs.get(resolvedKey); + PerlRuntime rt = PerlRuntime.current(); + RuntimeGlob glob = rt.globalIORefs.get(resolvedKey); if (glob == null) { glob = new RuntimeGlob(resolvedKey); - globalIORefs.put(resolvedKey, glob); + rt.globalIORefs.put(resolvedKey, glob); } return glob; } @@ -723,7 +747,7 @@ public static RuntimeScalar getGlobalIOCopy(String key) { * @return True if the global IO reference exists, false otherwise. */ public static boolean existsGlobalIO(String key) { - return globalIORefs.containsKey(key); + return PerlRuntime.current().globalIORefs.containsKey(key); } /** @@ -734,7 +758,7 @@ public static boolean existsGlobalIO(String key) { * @return True if the IO reference exists and has a real IO handle, false otherwise. */ public static boolean isGlobalIODefined(String key) { - RuntimeGlob glob = globalIORefs.get(key); + RuntimeGlob glob = PerlRuntime.current().globalIORefs.get(key); if (glob != null && glob.type == RuntimeScalarType.GLOB) { // Check the IO slot, not glob.value - IO is stored in glob.IO return glob.IO != null && glob.IO.getDefinedBoolean(); @@ -751,7 +775,7 @@ public static boolean isGlobalIODefined(String key) { * @return The RuntimeGlob if it exists in the stash, null otherwise. */ public static RuntimeGlob getExistingGlobalIO(String key) { - return globalIORefs.get(key); + return PerlRuntime.current().globalIORefs.get(key); } /** @@ -780,39 +804,41 @@ public static RuntimeScalar definedGlob(RuntimeScalar scalar, String packageName return RuntimeScalarCache.scalarTrue; } + PerlRuntime rt = PerlRuntime.current(); + // Check if glob was explicitly assigned - if (globalGlobs.getOrDefault(varName, false)) { + if (rt.globalGlobs.getOrDefault(varName, false)) { return RuntimeScalarCache.scalarTrue; } // Check scalar slot - slot existence makes glob defined (not value definedness) // In Perl, `defined *FOO` is true if $FOO exists, even if $FOO is undef - if (globalVariables.containsKey(varName)) { + if (rt.globalVariables.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } // Check array slot - exists = defined (even if empty) - if (globalArrays.containsKey(varName)) { + if (rt.globalArrays.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } // Check hash slot - exists = defined (even if empty) - if (globalHashes.containsKey(varName)) { + if (rt.globalHashes.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } // Check code slot - slot existence makes glob defined - if (globalCodeRefs.containsKey(varName)) { + if (rt.globalCodeRefs.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } - // Check IO slot (via globalIORefs) - if (globalIORefs.containsKey(varName)) { + // Check IO slot (via rt.globalIORefs) + if (rt.globalIORefs.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } // Check format slot - if (globalFormatRefs.containsKey(varName)) { + if (rt.globalFormatRefs.containsKey(varName)) { return RuntimeScalarCache.scalarTrue; } @@ -826,10 +852,11 @@ public static RuntimeScalar definedGlob(RuntimeScalar scalar, String packageName * @return The RuntimeFormat representing the global format reference. */ public static RuntimeFormat getGlobalFormatRef(String key) { - RuntimeFormat format = globalFormatRefs.get(key); + PerlRuntime rt = PerlRuntime.current(); + RuntimeFormat format = rt.globalFormatRefs.get(key); if (format == null) { format = new RuntimeFormat(key); - globalFormatRefs.put(key, format); + rt.globalFormatRefs.put(key, format); } return format; } @@ -842,7 +869,7 @@ public static RuntimeFormat getGlobalFormatRef(String key) { * @param format The RuntimeFormat object to set. */ public static void setGlobalFormatRef(String key, RuntimeFormat format) { - globalFormatRefs.put(key, format); + PerlRuntime.current().globalFormatRefs.put(key, format); } /** @@ -852,11 +879,11 @@ public static void setGlobalFormatRef(String key, RuntimeFormat format) { * @return True if the global format reference exists, false otherwise. */ public static boolean existsGlobalFormat(String key) { - return globalFormatRefs.containsKey(key); + return PerlRuntime.current().globalFormatRefs.containsKey(key); } public static RuntimeScalar existsGlobalFormatAsScalar(String key) { - return globalFormatRefs.containsKey(key) ? scalarTrue : scalarFalse; + return PerlRuntime.current().globalFormatRefs.containsKey(key) ? scalarTrue : scalarFalse; } public static RuntimeScalar existsGlobalFormatAsScalar(RuntimeScalar key) { @@ -870,13 +897,14 @@ public static RuntimeScalar existsGlobalFormatAsScalar(RuntimeScalar key) { * @return True if the format reference exists and is defined, false otherwise. */ public static boolean isGlobalFormatDefined(String key) { - RuntimeFormat format = globalFormatRefs.get(key); + RuntimeFormat format = PerlRuntime.current().globalFormatRefs.get(key); return format != null && format.isFormatDefined(); } public static RuntimeScalar definedGlobalFormatAsScalar(String key) { - return globalFormatRefs.containsKey(key) ? - (globalFormatRefs.get(key).isFormatDefined() ? scalarTrue : scalarFalse) : scalarFalse; + PerlRuntime rt = PerlRuntime.current(); + return rt.globalFormatRefs.containsKey(key) ? + (rt.globalFormatRefs.get(key).isFormatDefined() ? scalarTrue : scalarFalse) : scalarFalse; } public static RuntimeScalar definedGlobalFormatAsScalar(RuntimeScalar key) { @@ -890,8 +918,9 @@ public static RuntimeScalar definedGlobalFormatAsScalar(RuntimeScalar key) { * @param currentPackage The current package name with "::" suffix */ public static void resetGlobalVariables(Set resetChars, String currentPackage) { + PerlRuntime rt = PerlRuntime.current(); // Reset scalar variables - for (Map.Entry entry : globalVariables.entrySet()) { + for (Map.Entry entry : rt.globalVariables.entrySet()) { String key = entry.getKey(); if (key.startsWith(currentPackage) && shouldResetVariable(key, currentPackage, resetChars)) { @@ -901,7 +930,7 @@ public static void resetGlobalVariables(Set resetChars, String curren } // Reset array variables - for (Map.Entry entry : globalArrays.entrySet()) { + for (Map.Entry entry : rt.globalArrays.entrySet()) { String key = entry.getKey(); if (key.startsWith(currentPackage) && shouldResetVariable(key, currentPackage, resetChars)) { @@ -911,7 +940,7 @@ public static void resetGlobalVariables(Set resetChars, String curren } // Reset hash variables - for (Map.Entry entry : globalHashes.entrySet()) { + for (Map.Entry entry : rt.globalHashes.entrySet()) { String key = entry.getKey(); if (key.startsWith(currentPackage) && shouldResetVariable(key, currentPackage, resetChars)) { @@ -963,7 +992,7 @@ private static boolean shouldResetVariable(String fullKey, String packagePrefix, */ public static Map getAllIsaArrays() { Map result = new HashMap<>(); - for (Map.Entry entry : globalArrays.entrySet()) { + for (Map.Entry entry : PerlRuntime.current().globalArrays.entrySet()) { if (entry.getKey().endsWith("::ISA")) { result.put(entry.getKey(), entry.getValue()); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java index f4f69d721..ddd9c92ee 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/HashSpecialVariable.java @@ -74,7 +74,7 @@ public static RuntimeHash getStash(String namespace) { public Set> entrySet() { Set> entries = new HashSet<>(); if (this.mode == Id.CAPTURE_ALL || this.mode == Id.CAPTURE) { - Matcher matcher = RuntimeRegex.globalMatcher; + Matcher matcher = RuntimeRegex.getGlobalMatcher(); if (matcher != null) { Map namedGroups = matcher.pattern().namedGroups(); for (String name : namedGroups.keySet()) { @@ -106,12 +106,12 @@ public Set> entrySet() { // System.out.println("EntrySet "); // Collect all keys from GlobalVariable Set allKeys = new HashSet<>(); - allKeys.addAll(GlobalVariable.globalVariables.keySet()); - allKeys.addAll(GlobalVariable.globalArrays.keySet()); - allKeys.addAll(GlobalVariable.globalHashes.keySet()); - allKeys.addAll(GlobalVariable.globalCodeRefs.keySet()); - allKeys.addAll(GlobalVariable.globalIORefs.keySet()); - allKeys.addAll(GlobalVariable.globalFormatRefs.keySet()); + allKeys.addAll(GlobalVariable.getGlobalVariablesMap().keySet()); + allKeys.addAll(GlobalVariable.getGlobalArraysMap().keySet()); + allKeys.addAll(GlobalVariable.getGlobalHashesMap().keySet()); + allKeys.addAll(GlobalVariable.getGlobalCodeRefsMap().keySet()); + allKeys.addAll(GlobalVariable.getGlobalIORefsMap().keySet()); + allKeys.addAll(GlobalVariable.getGlobalFormatRefsMap().keySet()); // Process each key to extract the namespace part Set uniqueKeys = new HashSet<>(); // Set to track unique keys @@ -182,7 +182,7 @@ public Set> entrySet() { @Override public RuntimeScalar get(Object key) { if (this.mode == Id.CAPTURE_ALL || this.mode == Id.CAPTURE) { - Matcher matcher = RuntimeRegex.globalMatcher; + Matcher matcher = RuntimeRegex.getGlobalMatcher(); if (matcher != null && key instanceof String name) { // Encode the Perl name to Java regex name (underscore encoding) String encodedName = CaptureNameEncoder.encodeGroupName(name); @@ -210,12 +210,12 @@ public RuntimeScalar get(Object key) { } else if (this.mode == Id.STASH) { String prefix = namespace + key; // System.out.println("Get Key " + prefix); - if (containsNamespace(GlobalVariable.globalVariables, prefix) || - containsNamespace(GlobalVariable.globalArrays, prefix) || - containsNamespace(GlobalVariable.globalHashes, prefix) || - containsNamespace(GlobalVariable.globalCodeRefs, prefix) || - containsNamespace(GlobalVariable.globalIORefs, prefix) || - containsNamespace(GlobalVariable.globalFormatRefs, prefix)) { + if (containsNamespace(GlobalVariable.getGlobalVariablesMap(), prefix) || + containsNamespace(GlobalVariable.getGlobalArraysMap(), prefix) || + containsNamespace(GlobalVariable.getGlobalHashesMap(), prefix) || + containsNamespace(GlobalVariable.getGlobalCodeRefsMap(), prefix) || + containsNamespace(GlobalVariable.getGlobalIORefsMap(), prefix) || + containsNamespace(GlobalVariable.getGlobalFormatRefsMap(), prefix)) { return new RuntimeStashEntry(prefix, true); } return new RuntimeStashEntry(prefix, false); @@ -227,7 +227,7 @@ public RuntimeScalar get(Object key) { public boolean containsKey(Object key) { if (this.mode == Id.CAPTURE_ALL) { // For %-, all named groups exist (even non-participating ones) - Matcher matcher = RuntimeRegex.globalMatcher; + Matcher matcher = RuntimeRegex.getGlobalMatcher(); if (matcher != null && key instanceof String name) { String encodedName = CaptureNameEncoder.encodeGroupName(name); return matcher.pattern().namedGroups().containsKey(encodedName); @@ -236,7 +236,7 @@ public boolean containsKey(Object key) { } if (this.mode == Id.CAPTURE) { // For %+, only groups that actually captured - Matcher matcher = RuntimeRegex.globalMatcher; + Matcher matcher = RuntimeRegex.getGlobalMatcher(); if (matcher != null && key instanceof String name) { String encodedName = CaptureNameEncoder.encodeGroupName(name); return matcher.pattern().namedGroups().containsKey(encodedName) && matcher.group(encodedName) != null; @@ -271,12 +271,12 @@ public RuntimeScalar remove(Object key) { String fullKey = namespace + key; // Check if the glob exists - boolean exists = containsNamespace(GlobalVariable.globalVariables, fullKey) || - containsNamespace(GlobalVariable.globalArrays, fullKey) || - containsNamespace(GlobalVariable.globalHashes, fullKey) || - containsNamespace(GlobalVariable.globalCodeRefs, fullKey) || - containsNamespace(GlobalVariable.globalIORefs, fullKey) || - containsNamespace(GlobalVariable.globalFormatRefs, fullKey); + boolean exists = containsNamespace(GlobalVariable.getGlobalVariablesMap(), fullKey) || + containsNamespace(GlobalVariable.getGlobalArraysMap(), fullKey) || + containsNamespace(GlobalVariable.getGlobalHashesMap(), fullKey) || + containsNamespace(GlobalVariable.getGlobalCodeRefsMap(), fullKey) || + containsNamespace(GlobalVariable.getGlobalIORefsMap(), fullKey) || + containsNamespace(GlobalVariable.getGlobalFormatRefsMap(), fullKey); if (!exists) { return scalarUndef; @@ -285,12 +285,12 @@ public RuntimeScalar remove(Object key) { // Get references to all the slots before deleting // Only remove from globalCodeRefs, NOT pinnedCodeRefs, to allow compiled code // to continue calling the subroutine (Perl caches CVs at compile time) - RuntimeScalar code = GlobalVariable.globalCodeRefs.remove(fullKey); - RuntimeScalar scalar = GlobalVariable.globalVariables.remove(fullKey); - RuntimeArray array = GlobalVariable.globalArrays.remove(fullKey); - RuntimeHash hash = GlobalVariable.globalHashes.remove(fullKey); - RuntimeGlob io = GlobalVariable.globalIORefs.remove(fullKey); - RuntimeScalar format = GlobalVariable.globalFormatRefs.remove(fullKey); + RuntimeScalar code = GlobalVariable.getGlobalCodeRefsMap().remove(fullKey); + RuntimeScalar scalar = GlobalVariable.getGlobalVariablesMap().remove(fullKey); + RuntimeArray array = GlobalVariable.getGlobalArraysMap().remove(fullKey); + RuntimeHash hash = GlobalVariable.getGlobalHashesMap().remove(fullKey); + RuntimeGlob io = GlobalVariable.getGlobalIORefsMap().remove(fullKey); + RuntimeScalar format = GlobalVariable.getGlobalFormatRefsMap().remove(fullKey); // Any stash mutation can affect method lookup; clear method resolution caches. InheritanceResolver.invalidateCache(); @@ -308,12 +308,12 @@ public void clear() { if (this.mode == Id.STASH) { String prefix = namespace; - GlobalVariable.globalVariables.keySet().removeIf(k -> k.startsWith(prefix)); - GlobalVariable.globalArrays.keySet().removeIf(k -> k.startsWith(prefix)); - GlobalVariable.globalHashes.keySet().removeIf(k -> k.startsWith(prefix)); - GlobalVariable.globalCodeRefs.keySet().removeIf(k -> k.startsWith(prefix)); - GlobalVariable.globalIORefs.keySet().removeIf(k -> k.startsWith(prefix)); - GlobalVariable.globalFormatRefs.keySet().removeIf(k -> k.startsWith(prefix)); + GlobalVariable.getGlobalVariablesMap().keySet().removeIf(k -> k.startsWith(prefix)); + GlobalVariable.getGlobalArraysMap().keySet().removeIf(k -> k.startsWith(prefix)); + GlobalVariable.getGlobalHashesMap().keySet().removeIf(k -> k.startsWith(prefix)); + GlobalVariable.getGlobalCodeRefsMap().keySet().removeIf(k -> k.startsWith(prefix)); + GlobalVariable.getGlobalIORefsMap().keySet().removeIf(k -> k.startsWith(prefix)); + GlobalVariable.getGlobalFormatRefsMap().keySet().removeIf(k -> k.startsWith(prefix)); InheritanceResolver.invalidateCache(); GlobalVariable.clearPackageCache(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputAutoFlushVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputAutoFlushVariable.java index eeef675e5..8a1445f93 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputAutoFlushVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputAutoFlushVariable.java @@ -7,11 +7,15 @@ */ public class OutputAutoFlushVariable extends RuntimeScalar { - private static final Stack stateStack = new Stack<>(); + // State stack is now held per-PerlRuntime. + @SuppressWarnings("unchecked") + private static Stack stateStack() { + return (Stack) (Stack) PerlRuntime.current().autoFlushStateStack; + } private static RuntimeIO currentHandle() { - RuntimeIO handle = RuntimeIO.selectedHandle; - return handle != null ? handle : RuntimeIO.stdout; + RuntimeIO handle = RuntimeIO.getSelectedHandle(); + return handle != null ? handle : RuntimeIO.getStdout(); } @Override @@ -81,14 +85,14 @@ public RuntimeScalar postAutoDecrement() { @Override public void dynamicSaveState() { RuntimeIO handle = currentHandle(); - stateStack.push(new State(handle, handle.isAutoFlush())); + stateStack().push(new State(handle, handle.isAutoFlush())); handle.setAutoFlush(false); } @Override public void dynamicRestoreState() { - if (!stateStack.isEmpty()) { - State previous = stateStack.pop(); + if (!stateStack().isEmpty()) { + State previous = stateStack().pop(); if (previous.handle != null) { previous.handle.setAutoFlush(previous.autoFlush); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java index 747982fee..bc40c7ef5 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputFieldSeparator.java @@ -15,16 +15,13 @@ */ public class OutputFieldSeparator extends RuntimeScalar { - /** - * The internal OFS value that print reads. - * Only updated by OutputFieldSeparator.set() calls. - */ - private static String internalOFS = ""; - /** * Stack for save/restore during local $, and for $, (list). + * Now held per-PerlRuntime. */ - private static final Stack ofsStack = new Stack<>(); + private static Stack ofsStack() { + return PerlRuntime.current().ofsStack; + } public OutputFieldSeparator() { super(); @@ -32,9 +29,10 @@ public OutputFieldSeparator() { /** * Returns the internal OFS value for use by print. + * Now per-PerlRuntime for multiplicity thread-safety. */ public static String getInternalOFS() { - return internalOFS; + return PerlRuntime.current().internalOFS; } /** @@ -42,7 +40,8 @@ public static String getInternalOFS() { * Called from GlobalRuntimeScalar.dynamicSaveState() when localizing $,. */ public static void saveInternalOFS() { - ofsStack.push(internalOFS); + PerlRuntime rt = PerlRuntime.current(); + ofsStack().push(rt.internalOFS); } /** @@ -50,50 +49,50 @@ public static void saveInternalOFS() { * Called from GlobalRuntimeScalar.dynamicRestoreState() when restoring $,. */ public static void restoreInternalOFS() { - if (!ofsStack.isEmpty()) { - internalOFS = ofsStack.pop(); + if (!ofsStack().isEmpty()) { + PerlRuntime.current().internalOFS = ofsStack().pop(); } } @Override public RuntimeScalar set(RuntimeScalar value) { super.set(value); - internalOFS = this.toString(); + PerlRuntime.current().internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(String value) { super.set(value); - internalOFS = this.toString(); + PerlRuntime.current().internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(int value) { super.set(value); - internalOFS = this.toString(); + PerlRuntime.current().internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(long value) { super.set(value); - internalOFS = this.toString(); + PerlRuntime.current().internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(boolean value) { super.set(value); - internalOFS = this.toString(); + PerlRuntime.current().internalOFS = this.toString(); return this; } @Override public RuntimeScalar set(Object value) { super.set(value); - internalOFS = this.toString(); + PerlRuntime.current().internalOFS = this.toString(); return this; } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java index f35fe8991..e8cd691ed 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/OutputRecordSeparator.java @@ -20,16 +20,13 @@ */ public class OutputRecordSeparator extends RuntimeScalar { - /** - * The internal ORS value that print reads. - * Only updated by OutputRecordSeparator.set() calls. - */ - private static String internalORS = ""; - /** * Stack for save/restore during local $\ and for $\ (list). + * Now held per-PerlRuntime. */ - private static final Stack orsStack = new Stack<>(); + private static Stack orsStack() { + return PerlRuntime.current().orsStack; + } public OutputRecordSeparator() { super(); @@ -37,9 +34,10 @@ public OutputRecordSeparator() { /** * Returns the internal ORS value for use by print. + * Now per-PerlRuntime for multiplicity thread-safety. */ public static String getInternalORS() { - return internalORS; + return PerlRuntime.current().internalORS; } /** @@ -47,7 +45,8 @@ public static String getInternalORS() { * Called from GlobalRuntimeScalar.dynamicSaveState() when localizing $\. */ public static void saveInternalORS() { - orsStack.push(internalORS); + PerlRuntime rt = PerlRuntime.current(); + orsStack().push(rt.internalORS); } /** @@ -55,50 +54,50 @@ public static void saveInternalORS() { * Called from GlobalRuntimeScalar.dynamicRestoreState() when restoring $\. */ public static void restoreInternalORS() { - if (!orsStack.isEmpty()) { - internalORS = orsStack.pop(); + if (!orsStack().isEmpty()) { + PerlRuntime.current().internalORS = orsStack().pop(); } } @Override public RuntimeScalar set(RuntimeScalar value) { super.set(value); - internalORS = this.toString(); + PerlRuntime.current().internalORS = this.toString(); return this; } @Override public RuntimeScalar set(String value) { super.set(value); - internalORS = this.toString(); + PerlRuntime.current().internalORS = this.toString(); return this; } @Override public RuntimeScalar set(int value) { super.set(value); - internalORS = this.toString(); + PerlRuntime.current().internalORS = this.toString(); return this; } @Override public RuntimeScalar set(long value) { super.set(value); - internalORS = this.toString(); + PerlRuntime.current().internalORS = this.toString(); return this; } @Override public RuntimeScalar set(boolean value) { super.set(value); - internalORS = this.toString(); + PerlRuntime.current().internalORS = this.toString(); return this; } @Override public RuntimeScalar set(Object value) { super.set(value); - internalORS = this.toString(); + PerlRuntime.current().internalORS = this.toString(); return this; } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java new file mode 100644 index 000000000..3fa4fa437 --- /dev/null +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/PerlRuntime.java @@ -0,0 +1,644 @@ +package org.perlonjava.runtime.runtimetypes; + +import org.perlonjava.backend.jvm.CustomClassLoader; +import org.perlonjava.runtime.io.IOHandle; +import org.perlonjava.runtime.io.StandardIO; +import org.perlonjava.runtime.mro.InheritanceResolver; +import org.perlonjava.runtime.regex.RuntimeRegex; + +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Deque; +import java.util.HashMap; +import java.util.HashSet; +import java.util.List; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Set; +import java.util.Stack; +import java.util.concurrent.atomic.AtomicLong; +import java.util.regex.Matcher; + +/** + * PerlRuntime represents an independent Perl interpreter instance. + * Each PerlRuntime holds its own copy of all mutable runtime state, + * enabling multiple Perl interpreters to coexist within the same JVM + * (multiplicity). + * + *

    The current runtime is stored in a ThreadLocal, so each thread + * can be bound to a different PerlRuntime. Access the current runtime + * via {@link #current()}.

    + * + *

    Migration strategy: Subsystems are migrated incrementally from + * static fields to PerlRuntime instance fields. During migration, the + * original classes (e.g., CallerStack, DynamicVariableManager) retain + * their static method signatures but delegate to PerlRuntime.current() + * internally, so callers don't need to change.

    + * + * @see Concurrency Design Document + */ +public final class PerlRuntime { + + private static final ThreadLocal CURRENT = new ThreadLocal<>(); + + /** + * Counter for generating unique per-runtime PIDs. + * Starts at the real JVM PID so the first runtime gets the actual PID, + * subsequent runtimes get incrementing values (realPid+1, realPid+2, ...). + * This ensures $$ is unique per interpreter for temp file isolation. + */ + private static final AtomicLong PID_COUNTER = + new AtomicLong(ProcessHandle.current().pid()); + + /** + * Per-runtime synthetic PID, used as Perl's $$. + * First runtime gets the real JVM PID; subsequent runtimes get unique values. + */ + public final long pid = PID_COUNTER.getAndIncrement(); + + // ---- Per-runtime state (migrated from static fields) ---- + + /** + * Caller stack for caller() function — migrated from CallerStack.callerStack. + * Stores CallerInfo and LazyCallerInfo objects. + */ + final List callerStack = new ArrayList<>(); + + /** + * Dynamic variable stack for Perl's "local" — migrated from DynamicVariableManager.variableStack. + * Using ArrayDeque for performance (no synchronization overhead). + */ + final Deque dynamicVariableStack = new ArrayDeque<>(); + + /** + * Dynamic state stack for RuntimeScalar "local" save/restore — + * migrated from RuntimeScalar.dynamicStateStack. + */ + final Stack dynamicStateStack = new Stack<>(); + + /** + * Dynamic state stack for RuntimeArray "local" save/restore — + * migrated from RuntimeArray.dynamicStateStack. + */ + final Stack arrayDynamicStateStack = new Stack<>(); + + /** + * Dynamic state stack for RuntimeHash "local" save/restore — + * migrated from RuntimeHash.dynamicStateStack. + */ + final Stack hashDynamicStateStack = new Stack<>(); + + /** + * Dynamic state stack for RuntimeStash "local" save/restore — + * migrated from RuntimeStash.dynamicStateStack. + */ + final Stack stashDynamicStateStack = new Stack<>(); + + /** + * Glob slot stack for RuntimeGlob "local" save/restore — + * migrated from RuntimeGlob.globSlotStack. + * Elements are RuntimeGlob.GlobSlotSnapshot (package-private inner type). + */ + final Stack globSlotStack = new Stack<>(); + + /** + * Localized stack for GlobalRuntimeScalar "local" save/restore — + * migrated from GlobalRuntimeScalar.localizedStack. + * Elements are GlobalRuntimeScalar.SavedGlobalState (package-private inner type). + */ + final Stack globalScalarLocalizedStack = new Stack<>(); + + /** + * Localized stack for GlobalRuntimeArray "local" save/restore — + * migrated from GlobalRuntimeArray.localizedStack. + * Elements are GlobalRuntimeArray.SavedGlobalArrayState (package-private inner type). + */ + final Stack globalArrayLocalizedStack = new Stack<>(); + + /** + * Localized stack for GlobalRuntimeHash "local" save/restore — + * migrated from GlobalRuntimeHash.localizedStack. + * Elements are GlobalRuntimeHash.SavedGlobalHashState (package-private inner type). + */ + final Stack globalHashLocalizedStack = new Stack<>(); + + /** + * Dynamic state stack for RuntimeHashProxyEntry "local" save/restore — + * migrated from RuntimeHashProxyEntry.dynamicStateStack. + */ + final Stack hashProxyDynamicStateStack = new Stack<>(); + + /** + * Dynamic state stacks for RuntimeArrayProxyEntry "local" save/restore — + * migrated from RuntimeArrayProxyEntry.dynamicStateStackInt and dynamicStateStack. + */ + final Stack arrayProxyDynamicStateStackInt = new Stack<>(); + final Stack arrayProxyDynamicStateStack = new Stack<>(); + + /** + * Input line state stack for ScalarSpecialVariable "local" save/restore — + * migrated from ScalarSpecialVariable.inputLineStateStack. + * Elements are ScalarSpecialVariable.InputLineState (package-private inner type). + */ + final Stack inputLineStateStack = new Stack<>(); + + /** + * State stack for OutputAutoFlushVariable "local" save/restore — + * migrated from OutputAutoFlushVariable.stateStack. + * Elements are OutputAutoFlushVariable.State (package-private inner type). + */ + final Stack autoFlushStateStack = new Stack<>(); + + /** + * ORS stack for OutputRecordSeparator "local $\" save/restore — + * migrated from OutputRecordSeparator.orsStack. + */ + final Stack orsStack = new Stack<>(); + + /** + * Internal ORS value that print reads — migrated from OutputRecordSeparator.internalORS + * for multiplicity thread-safety. + */ + public String internalORS = ""; + + /** + * OFS stack for OutputFieldSeparator "local $," save/restore — + * migrated from OutputFieldSeparator.ofsStack. + */ + final Stack ofsStack = new Stack<>(); + + /** + * Internal OFS value that print reads — migrated from OutputFieldSeparator.internalOFS + * for multiplicity thread-safety. + */ + public String internalOFS = ""; + + /** + * Errno stacks for ErrnoVariable "local $!" save/restore — + * migrated from ErrnoVariable.errnoStack and messageStack. + */ + final Stack errnoStack = new Stack<>(); + final Stack errnoMessageStack = new Stack<>(); + + /** + * Special block arrays (END, INIT, CHECK) — migrated from SpecialBlock. + */ + final RuntimeArray endBlocks = new RuntimeArray(); + final RuntimeArray initBlocks = new RuntimeArray(); + final RuntimeArray checkBlocks = new RuntimeArray(); + + // ---- I/O state — migrated from RuntimeIO static fields ---- + + /** + * Standard output stream handle (STDOUT) — migrated from RuntimeIO.stdout. + */ + RuntimeIO ioStdout; + + /** + * Standard error stream handle (STDERR) — migrated from RuntimeIO.stderr. + */ + RuntimeIO ioStderr; + + /** + * Standard input stream handle (STDIN) — migrated from RuntimeIO.stdin. + */ + RuntimeIO ioStdin; + + /** + * The currently selected filehandle for output operations (Perl's select()). + * Used by print/printf when no filehandle is specified. + */ + RuntimeIO ioSelectedHandle; + + /** + * The last handle used for output writes (print/say/etc). + */ + RuntimeIO ioLastWrittenHandle; + + /** + * The last accessed filehandle, used for Perl's ${^LAST_FH} special variable. + */ + RuntimeIO ioLastAccessedHandle; + + /** + * The variable/handle name used in the last readline operation. + */ + String ioLastReadlineHandleName; + + // ---- Inheritance / MRO state — migrated from InheritanceResolver static fields ---- + + /** + * Cache for linearized class hierarchies (C3/DFS results). + */ + public final Map> linearizedClassesCache = new HashMap<>(); + + /** + * Per-package MRO algorithm settings. + */ + public final Map packageMRO = new HashMap<>(); + + /** + * Method resolution cache (method name -> code ref). + */ + public final Map methodCache = new HashMap<>(); + + /** + * Cache for OverloadContext instances by blessing ID. + */ + public final Map overloadContextCache = new HashMap<>(); + + /** + * Tracks ISA array states for change detection. + */ + public final Map> isaStateCache = new HashMap<>(); + + /** + * Whether AUTOLOAD is enabled for method resolution. + */ + public boolean autoloadEnabled = true; + + /** + * Default MRO algorithm (DFS by default, matching Perl 5). + */ + public InheritanceResolver.MROAlgorithm currentMRO = InheritanceResolver.MROAlgorithm.DFS; + + // ---- MRO state — migrated from Mro static fields for multiplicity ---- + + /** Package generation counters for mro::get_pkg_gen(). */ + public final Map mroPackageGenerations = new HashMap<>(); + + /** Reverse ISA cache (which classes inherit from a given class). */ + public final Map> mroIsaRevCache = new HashMap<>(); + + /** Cached @ISA state per package — used to detect @ISA changes. */ + public final Map> mroPkgGenIsaState = new HashMap<>(); + + // ---- IO state — migrated from RuntimeIO static fields for multiplicity ---- + + /** Maximum number of file handles to keep in the LRU cache. */ + private static final int MAX_OPEN_HANDLES = 100; + + /** LRU cache of open file handles — per-runtime for multiplicity. */ + public final Map openHandles = + new LinkedHashMap(MAX_OPEN_HANDLES, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry eldest) { + if (size() > MAX_OPEN_HANDLES) { + try { + eldest.getKey().flush(); + } catch (Exception e) { + // Handle exception if needed + } + return true; + } + return false; + } + }; + + // ---- Symbol table state — migrated from GlobalVariable static fields ---- + + /** Global scalar variables (%main:: scalar namespace). */ + public final Map globalVariables = new HashMap<>(); + + /** Global array variables. */ + public final Map globalArrays = new HashMap<>(); + + /** Global hash variables. */ + public final Map globalHashes = new HashMap<>(); + + /** Cache for package existence checks. */ + public final Map packageExistsCache = new HashMap<>(); + + /** Tracks subroutines declared via 'use subs' pragma. */ + public final Map isSubs = new HashMap<>(); + + /** Global code references (subroutine namespace). */ + public final Map globalCodeRefs = new HashMap<>(); + + /** Global IO references (filehandle globs). */ + public final Map globalIORefs = new HashMap<>(); + + /** Global format references. */ + public final Map globalFormatRefs = new HashMap<>(); + + /** Pinned code references that survive stash deletion. */ + public final Map pinnedCodeRefs = new HashMap<>(); + + /** Stash aliasing: *Dst:: = *Src:: makes Dst symbol table redirect to Src. */ + public final Map stashAliases = new HashMap<>(); + + /** Glob aliasing: *a = *b makes a and b share the same glob. */ + public final Map globAliases = new HashMap<>(); + + /** Flags for typeglob assignments (operator override detection). */ + public final Map globalGlobs = new HashMap<>(); + + /** Global class loader for generated classes. Not final so it can be replaced. */ + public CustomClassLoader globalClassLoader = + new CustomClassLoader(GlobalVariable.class.getClassLoader()); + + /** Track explicitly declared global variables (via use vars, our, Exporter). */ + public final Set declaredGlobalVariables = new HashSet<>(); + public final Set declaredGlobalArrays = new HashSet<>(); + public final Set declaredGlobalHashes = new HashSet<>(); + + // ---- Regex match state — migrated from RuntimeRegex static fields ---- + + /** Java Matcher object; provides %+, %-, @-, @+ group info. */ + public Matcher regexGlobalMatcher; + + /** Full input string being matched; used by $&, $`, $'. */ + public String regexGlobalMatchString; + + /** The matched substring ($&). */ + public String regexLastMatchedString = null; + + /** Start offset of match (for $`/@-[0]). */ + public int regexLastMatchStart = -1; + + /** End offset of match (for $'/@+[0]). */ + public int regexLastMatchEnd = -1; + + /** Persists across failed matches — matched string. */ + public String regexLastSuccessfulMatchedString = null; + + /** Persists across failed matches — start offset. */ + public int regexLastSuccessfulMatchStart = -1; + + /** Persists across failed matches — end offset. */ + public int regexLastSuccessfulMatchEnd = -1; + + /** Full input string from last successful match. */ + public String regexLastSuccessfulMatchString = null; + + /** ${^LAST_SUCCESSFUL_PATTERN} and $^R via getLastCodeBlockResult(). */ + public RuntimeRegex regexLastSuccessfulPattern = null; + + /** Tracks if /p was used (for ${^PREMATCH}, ${^MATCH}, ${^POSTMATCH}). */ + public boolean regexLastMatchUsedPFlag = false; + + /** Tracks if \K was used; adjusts group offsets. */ + public boolean regexLastMatchUsedBackslashK = false; + + /** Capture groups $1, $2, ...; persists across non-capturing matches. */ + public String[] regexLastCaptureGroups = null; + + /** Preserves BYTE_STRING type on captures. */ + public boolean regexLastMatchWasByteString = false; + + /** Cache for /o modifier — maps callsite ID to compiled regex (only first compilation is used). */ + public final Map regexOptimizedCache = new HashMap<>(); + + // ---- ByteCodeSourceMapper state — migrated for multiplicity ---- + + /** Per-runtime source-mapper state (file→line→package mappings for stack traces). */ + public final org.perlonjava.backend.jvm.ByteCodeSourceMapper.State sourceMapperState = + new org.perlonjava.backend.jvm.ByteCodeSourceMapper.State(); + + // ---- RuntimeCode compilation state — migrated from RuntimeCode static fields ---- + + /** Tracks eval BEGIN block IDs during compilation. */ + public final java.util.IdentityHashMap evalBeginIds = new java.util.IdentityHashMap<>(); + + /** LRU cache for compiled eval STRING results. */ + public final Map> evalCache = new java.util.LinkedHashMap>(100, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry> eldest) { + return size() > 100; + } + }; + + /** LRU cache for method handles. */ + public final Map, java.lang.invoke.MethodHandle> methodHandleCache = new java.util.LinkedHashMap, java.lang.invoke.MethodHandle>(100, 0.75f, true) { + @Override + protected boolean removeEldestEntry(Map.Entry, java.lang.invoke.MethodHandle> eldest) { + return size() > 100; + } + }; + + /** Temporary storage for anonymous subroutines during compilation. */ + public final HashMap> anonSubs = new HashMap<>(); + + /** Storage for interpreter fallback closures. */ + public final HashMap interpretedSubs = new HashMap<>(); + + /** Storage for eval string compiler context (values are EmitterContext but stored as Object to avoid circular deps). */ + public final HashMap evalContext = new HashMap<>(); + + /** Current eval nesting depth for $^S support. */ + public int evalDepth = 0; + + /** + * Whether GlobalContext.initializeGlobals() has been called for this runtime. + * Each PerlRuntime needs its own initialization of global variables, @INC, %ENV, + * built-in modules, etc. Previously this was a shared static boolean in + * PerlLanguageProvider, which caused threads 2-N to skip initialization. + */ + public boolean globalInitialized = false; + + /** + * Per-runtime current working directory. + * Initialized from System.getProperty("user.dir") at construction time. + * Updated by Directory.chdir(). All path resolution in RuntimeIO.resolvePath() + * reads from this field instead of the JVM-global "user.dir" property, + * ensuring each interpreter has its own isolated CWD. + */ + public String cwd = System.getProperty("user.dir"); + + /** Inline method cache for fast method dispatch. */ + public static final int METHOD_CALL_CACHE_SIZE = 4096; + public final int[] inlineCacheBlessId = new int[METHOD_CALL_CACHE_SIZE]; + public final int[] inlineCacheMethodHash = new int[METHOD_CALL_CACHE_SIZE]; + public final RuntimeCode[] inlineCacheCode = new RuntimeCode[METHOD_CALL_CACHE_SIZE]; + + // ---- Warning/Hints stacks — migrated from WarningBitsRegistry ThreadLocals ---- + + /** Stack of warning bits for the current execution context. */ + public final Deque warningCurrentBitsStack = new ArrayDeque<>(); + + /** Warning bits at the current call site. */ + public String warningCallSiteBits = null; + + /** Stack saving caller's call-site warning bits across subroutine calls. */ + public final Deque warningCallerBitsStack = new ArrayDeque<>(); + + /** Compile-time $^H (hints) at the current call site. */ + public int warningCallSiteHints = 0; + + /** Stack saving caller's $^H hints across subroutine calls. */ + public final Deque warningCallerHintsStack = new ArrayDeque<>(); + + /** Compile-time %^H (hints hash) snapshot at the current call site. */ + public Map warningCallSiteHintHash = new HashMap<>(); + + /** Stack saving caller's %^H across subroutine calls. */ + public final Deque> warningCallerHintHashStack = new ArrayDeque<>(); + + // ---- HintHashRegistry stacks — migrated from HintHashRegistry ThreadLocals ---- + + /** Current call site's hint hash snapshot ID. */ + public int hintCallSiteSnapshotId = 0; + + /** Stack saving caller's hint hash snapshot ID across subroutine calls. */ + public final Deque hintCallerSnapshotIdStack = new ArrayDeque<>(); + + // ---- RuntimeCode stacks — migrated from RuntimeCode ThreadLocals ---- + + /** Eval runtime context (used during eval STRING compilation). */ + public Object evalRuntimeContext = null; + + /** Stack of @_ argument arrays across subroutine calls. */ + public final Deque argsStack = new ArrayDeque<>(); + + // ---- Static accessors ---- + + /** + * Returns the PerlRuntime bound to the current thread. + * This is the primary entry point for all runtime state access. + * + * @return the current PerlRuntime, never null during normal execution + * @throws IllegalStateException if no runtime is bound to this thread + */ + public static PerlRuntime current() { + PerlRuntime rt = CURRENT.get(); + if (rt == null) { + throw new IllegalStateException( + "No PerlRuntime bound to current thread. " + + "Call PerlRuntime.initialize() or PerlRuntime.setCurrent() first."); + } + return rt; + } + + /** + * Returns the current working directory for the current runtime. + * Falls back to System.getProperty("user.dir") if no runtime is bound. + */ + public static String getCwd() { + PerlRuntime rt = CURRENT.get(); + return rt != null ? rt.cwd : System.getProperty("user.dir"); + } + + /** + * Returns the PerlRuntime bound to the current thread, or null if none. + * Use this for checks where missing runtime is expected (e.g., initialization). + */ + public static PerlRuntime currentOrNull() { + return CURRENT.get(); + } + + /** + * Binds the given PerlRuntime to the current thread. + * + * @param rt the runtime to bind, or null to unbind + */ + public static void setCurrent(PerlRuntime rt) { + if (rt == null) { + CURRENT.remove(); + } else { + CURRENT.set(rt); + } + } + + /** + * Creates a new PerlRuntime and binds it to the current thread. + * If a runtime is already bound, it is replaced. + * + * @return the newly created runtime + */ + public static PerlRuntime initialize() { + PerlRuntime rt = new PerlRuntime(); + CURRENT.set(rt); + return rt; + } + + // ---- Batch push/pop methods for subroutine call hot path ---- + + /** + * Pushes all caller state in one shot for the static apply() dispatch path. + * Replaces 4 separate PerlRuntime.current() lookups with 1. + * Called from RuntimeCode's static apply() methods. + * + * @param warningBits warning bits for the code being called, or null + */ + public void pushCallerState(String warningBits) { + if (warningBits != null) { + warningCurrentBitsStack.push(warningBits); + } + // Save caller's call-site warning bits (for caller()[9]) + warningCallerBitsStack.push(warningCallSiteBits != null ? warningCallSiteBits : ""); + // Save caller's $^H (for caller()[8]) + warningCallerHintsStack.push(warningCallSiteHints); + // Save caller's hint hash snapshot ID and reset for callee + hintCallerSnapshotIdStack.push(hintCallSiteSnapshotId); + hintCallSiteSnapshotId = 0; + } + + /** + * Pops all caller state in one shot for the static apply() dispatch path. + * Replaces 4 separate PerlRuntime.current() lookups with 1. + * + * @param hadWarningBits true if warningBits was non-null on the matching push + */ + public void popCallerState(boolean hadWarningBits) { + // Restore hint hash snapshot ID + if (!hintCallerSnapshotIdStack.isEmpty()) { + hintCallSiteSnapshotId = hintCallerSnapshotIdStack.pop(); + } + // Restore caller hints + if (!warningCallerHintsStack.isEmpty()) { + warningCallerHintsStack.pop(); + } + // Restore caller bits + if (!warningCallerBitsStack.isEmpty()) { + warningCallerBitsStack.pop(); + } + // Restore warning bits + if (hadWarningBits && !warningCurrentBitsStack.isEmpty()) { + warningCurrentBitsStack.pop(); + } + } + + /** + * Pushes subroutine entry state (args + warning bits) in one shot. + * Replaces 2 separate PerlRuntime.current() lookups with 1. + * Called from RuntimeCode's instance apply() methods. + * + * @param args the @_ arguments array + * @param warningBits warning bits for the code being called, or null + */ + public void pushSubState(RuntimeArray args, String warningBits) { + argsStack.push(args); + if (warningBits != null) { + warningCurrentBitsStack.push(warningBits); + } + } + + /** + * Pops subroutine exit state (args + warning bits) in one shot. + * Replaces 2 separate PerlRuntime.current() lookups with 1. + * + * @param hadWarningBits true if warningBits was non-null on the matching push + */ + public void popSubState(boolean hadWarningBits) { + if (hadWarningBits && !warningCurrentBitsStack.isEmpty()) { + warningCurrentBitsStack.pop(); + } + if (!argsStack.isEmpty()) { + argsStack.pop(); + } + } + + /** + * Creates a new independent PerlRuntime (not bound to any thread). + * Call {@link #setCurrent(PerlRuntime)} to bind it to a thread before use. + */ + public PerlRuntime() { + // Initialize standard I/O handles + this.ioStdout = new RuntimeIO(new StandardIO(System.out, true)); + this.ioStderr = new RuntimeIO(new StandardIO(System.err, false)); + this.ioStderr.autoFlush = true; // STDERR is unbuffered by default, like in Perl + this.ioStdin = new RuntimeIO(new StandardIO(System.in)); + this.ioSelectedHandle = this.ioStdout; + this.ioLastWrittenHandle = this.ioStdout; + } +} diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java index e4e9a4455..fe3783b5f 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RegexState.java @@ -4,6 +4,7 @@ import java.util.regex.Matcher; + /** * Snapshot of regex-related global state (Perl's $1, $&, $`, $', etc.). * @@ -28,19 +29,21 @@ public class RegexState implements DynamicState { private final boolean lastMatchWasByteString; public RegexState() { - this.globalMatcher = RuntimeRegex.globalMatcher; - this.globalMatchString = RuntimeRegex.globalMatchString; - this.lastMatchedString = RuntimeRegex.lastMatchedString; - this.lastMatchStart = RuntimeRegex.lastMatchStart; - this.lastMatchEnd = RuntimeRegex.lastMatchEnd; - this.lastSuccessfulMatchedString = RuntimeRegex.lastSuccessfulMatchedString; - this.lastSuccessfulMatchStart = RuntimeRegex.lastSuccessfulMatchStart; - this.lastSuccessfulMatchEnd = RuntimeRegex.lastSuccessfulMatchEnd; - this.lastSuccessfulMatchString = RuntimeRegex.lastSuccessfulMatchString; - this.lastSuccessfulPattern = RuntimeRegex.lastSuccessfulPattern; - this.lastMatchUsedPFlag = RuntimeRegex.lastMatchUsedPFlag; - this.lastCaptureGroups = RuntimeRegex.lastCaptureGroups; - this.lastMatchWasByteString = RuntimeRegex.lastMatchWasByteString; + // Single PerlRuntime.current() lookup instead of 13 separate ones + PerlRuntime rt = PerlRuntime.current(); + this.globalMatcher = rt.regexGlobalMatcher; + this.globalMatchString = rt.regexGlobalMatchString; + this.lastMatchedString = rt.regexLastMatchedString; + this.lastMatchStart = rt.regexLastMatchStart; + this.lastMatchEnd = rt.regexLastMatchEnd; + this.lastSuccessfulMatchedString = rt.regexLastSuccessfulMatchedString; + this.lastSuccessfulMatchStart = rt.regexLastSuccessfulMatchStart; + this.lastSuccessfulMatchEnd = rt.regexLastSuccessfulMatchEnd; + this.lastSuccessfulMatchString = rt.regexLastSuccessfulMatchString; + this.lastSuccessfulPattern = rt.regexLastSuccessfulPattern; + this.lastMatchUsedPFlag = rt.regexLastMatchUsedPFlag; + this.lastCaptureGroups = rt.regexLastCaptureGroups; + this.lastMatchWasByteString = rt.regexLastMatchWasByteString; } public static void save() { @@ -57,18 +60,20 @@ public void restore() { @Override public void dynamicRestoreState() { - RuntimeRegex.globalMatcher = this.globalMatcher; - RuntimeRegex.globalMatchString = this.globalMatchString; - RuntimeRegex.lastMatchedString = this.lastMatchedString; - RuntimeRegex.lastMatchStart = this.lastMatchStart; - RuntimeRegex.lastMatchEnd = this.lastMatchEnd; - RuntimeRegex.lastSuccessfulMatchedString = this.lastSuccessfulMatchedString; - RuntimeRegex.lastSuccessfulMatchStart = this.lastSuccessfulMatchStart; - RuntimeRegex.lastSuccessfulMatchEnd = this.lastSuccessfulMatchEnd; - RuntimeRegex.lastSuccessfulMatchString = this.lastSuccessfulMatchString; - RuntimeRegex.lastSuccessfulPattern = this.lastSuccessfulPattern; - RuntimeRegex.lastMatchUsedPFlag = this.lastMatchUsedPFlag; - RuntimeRegex.lastCaptureGroups = this.lastCaptureGroups; - RuntimeRegex.lastMatchWasByteString = this.lastMatchWasByteString; + // Single PerlRuntime.current() lookup instead of 13 separate ones + PerlRuntime rt = PerlRuntime.current(); + rt.regexGlobalMatcher = this.globalMatcher; + rt.regexGlobalMatchString = this.globalMatchString; + rt.regexLastMatchedString = this.lastMatchedString; + rt.regexLastMatchStart = this.lastMatchStart; + rt.regexLastMatchEnd = this.lastMatchEnd; + rt.regexLastSuccessfulMatchedString = this.lastSuccessfulMatchedString; + rt.regexLastSuccessfulMatchStart = this.lastSuccessfulMatchStart; + rt.regexLastSuccessfulMatchEnd = this.lastSuccessfulMatchEnd; + rt.regexLastSuccessfulMatchString = this.lastSuccessfulMatchString; + rt.regexLastSuccessfulPattern = this.lastSuccessfulPattern; + rt.regexLastMatchUsedPFlag = this.lastMatchUsedPFlag; + rt.regexLastCaptureGroups = this.lastCaptureGroups; + rt.regexLastMatchWasByteString = this.lastMatchWasByteString; } } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java index 081e60e4b..c0a7fa977 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArray.java @@ -20,8 +20,10 @@ public class RuntimeArray extends RuntimeBase implements RuntimeScalarReference, public static final int AUTOVIVIFY_ARRAY = 1; public static final int TIED_ARRAY = 2; public static final int READONLY_ARRAY = 3; - // Static stack to store saved "local" states of RuntimeArray instances - private static final Stack dynamicStateStack = new Stack<>(); + // Dynamic state stack is now held per-PerlRuntime. + private static Stack dynamicStateStack() { + return PerlRuntime.current().arrayDynamicStateStack; + } // Internal type of array - PLAIN_ARRAY, AUTOVIVIFY_ARRAY, TIED_ARRAY, or READONLY_ARRAY public int type; public boolean strictAutovivify; @@ -1172,7 +1174,7 @@ public void dynamicSaveState() { // Copy the current blessId to the new state currentState.blessId = this.blessId; // Push the current state onto the stack - dynamicStateStack.push(currentState); + dynamicStateStack().push(currentState); // Clear the array elements (for tied arrays, this calls CLEAR) if (this.type == TIED_ARRAY) { TieArray.tiedClear(this); @@ -1191,9 +1193,9 @@ public void dynamicSaveState() { */ @Override public void dynamicRestoreState() { - if (!dynamicStateStack.isEmpty()) { + if (!dynamicStateStack().isEmpty()) { // Pop the most recent saved state from the stack - RuntimeArray previousState = dynamicStateStack.pop(); + RuntimeArray previousState = dynamicStateStack().pop(); // Restore the elements from the saved state this.elements = previousState.elements; // Restore the type from the saved state (important for tied arrays) diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArrayProxyEntry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArrayProxyEntry.java index 196d0dd75..584ef53dd 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArrayProxyEntry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeArrayProxyEntry.java @@ -9,8 +9,13 @@ * when they are accessed. */ public class RuntimeArrayProxyEntry extends RuntimeBaseProxy { - private static final Stack dynamicStateStackInt = new Stack<>(); - private static final Stack dynamicStateStack = new Stack<>(); + // Dynamic state stacks are now held per-PerlRuntime. + private static Stack dynamicStateStackInt() { + return PerlRuntime.current().arrayProxyDynamicStateStackInt; + } + private static Stack dynamicStateStack() { + return PerlRuntime.current().arrayProxyDynamicStateStack; + } // Reference to the parent RuntimeArray private final RuntimeArray parent; @@ -91,10 +96,10 @@ void vivify() { */ @Override public void dynamicSaveState() { - dynamicStateStackInt.push(parent.elements.size()); + dynamicStateStackInt().push(parent.elements.size()); // Create a new RuntimeScalar to save the current state if (this.lvalue == null) { - dynamicStateStack.push(null); + dynamicStateStack().push(null); vivify(); } else { RuntimeScalar currentState = new RuntimeScalar(); @@ -102,7 +107,7 @@ public void dynamicSaveState() { currentState.type = this.lvalue.type; currentState.value = this.lvalue.value; currentState.blessId = this.lvalue.blessId; - dynamicStateStack.push(currentState); + dynamicStateStack().push(currentState); // Clear the current type and value this.undefine(); } @@ -116,9 +121,9 @@ public void dynamicSaveState() { */ @Override public void dynamicRestoreState() { - if (!dynamicStateStack.isEmpty()) { + if (!dynamicStateStack().isEmpty()) { // Pop the most recent saved state from the stack - RuntimeScalar previousState = dynamicStateStack.pop(); + RuntimeScalar previousState = dynamicStateStack().pop(); if (previousState == null) { // Element didn't exist before. // Decrement refCount of the current value being displaced. @@ -139,7 +144,7 @@ public void dynamicRestoreState() { this.lvalue.blessId = previousState.blessId; this.blessId = previousState.blessId; } - int previousSize = dynamicStateStackInt.pop(); + int previousSize = dynamicStateStackInt().pop(); while (parent.elements.size() > previousSize) { parent.elements.removeLast(); } diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java index 387a73ff6..f57fc5d34 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeCode.java @@ -1,6 +1,7 @@ package org.perlonjava.runtime.runtimetypes; import org.perlonjava.app.cli.CompilerOptions; +import org.perlonjava.app.scriptengine.PerlLanguageProvider; import org.perlonjava.backend.bytecode.BytecodeCompiler; import org.perlonjava.backend.bytecode.Disassemble; import org.perlonjava.backend.bytecode.InterpretedCode; @@ -52,7 +53,10 @@ public class RuntimeCode extends RuntimeBase implements RuntimeScalarReference { // Lookup object for performing method handle operations public static final MethodHandles.Lookup lookup = MethodHandles.lookup(); - public static final IdentityHashMap evalBeginIds = new IdentityHashMap<>(); + // evalBeginIds migrated to PerlRuntime; access via getEvalBeginIds() + public static IdentityHashMap getEvalBeginIds() { + return PerlRuntime.current().evalBeginIds; + } /** * Flag to control whether eval STRING should use the interpreter backend. @@ -97,26 +101,18 @@ public class RuntimeCode extends RuntimeBase implements RuntimeScalarReference { * - runtimeValues: Object[] of captured variable values * - capturedEnv: String[] of captured variable names (matching array indices) *

    - * Thread-safety: Each thread's eval compilation uses its own ThreadLocal storage, so parallel + * Thread-safety: Each thread's eval compilation uses its own PerlRuntime storage, so parallel * eval compilations don't interfere with each other. */ - private static final ThreadLocal evalRuntimeContext = new ThreadLocal<>(); - // Cache for memoization of evalStringHelper results - private static final int CLASS_CACHE_SIZE = 100; - private static final Map> evalCache = new LinkedHashMap>(CLASS_CACHE_SIZE, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry> eldest) { - return size() > CLASS_CACHE_SIZE; - } - }; - // Cache for method handles with eviction policy - private static final int METHOD_HANDLE_CACHE_SIZE = 100; - private static final Map, MethodHandle> methodHandleCache = new LinkedHashMap, MethodHandle>(METHOD_HANDLE_CACHE_SIZE, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry, MethodHandle> eldest) { - return size() > METHOD_HANDLE_CACHE_SIZE; - } - }; + // evalRuntimeContext migrated to PerlRuntime; access via getEvalRuntimeContext() + // evalCache migrated to PerlRuntime; access via getEvalCache() + private static Map> getEvalCache() { + return PerlRuntime.current().evalCache; + } + // methodHandleCache migrated to PerlRuntime; access via getMethodHandleCache() + private static Map, MethodHandle> getMethodHandleCache() { + return PerlRuntime.current().methodHandleCache; + } /** * Flag to enable disassembly of eval STRING bytecode. * When set, prints the interpreter bytecode for each eval STRING compilation. @@ -135,9 +131,17 @@ protected boolean removeEldestEntry(Map.Entry, MethodHandle> eldest) { /** * Tracks the current eval nesting depth for $^S support. * 0 = not inside any eval, >0 = inside eval (eval STRING or eval BLOCK). - * Incremented on eval entry, decremented on eval exit (success or failure). + * Migrated to PerlRuntime; access via getEvalDepth()/incrementEvalDepth()/decrementEvalDepth(). */ - public static int evalDepth = 0; + public static int getEvalDepth() { + return PerlRuntime.current().evalDepth; + } + public static void incrementEvalDepth() { + PerlRuntime.current().evalDepth++; + } + public static void decrementEvalDepth() { + PerlRuntime.current().evalDepth--; + } /** * Thread-local stack of @_ arrays for each active subroutine call. @@ -146,9 +150,9 @@ protected boolean removeEldestEntry(Map.Entry, MethodHandle> eldest) { * * Push/pop is handled by RuntimeCode.apply() methods. * Access via getCurrentArgs() for Java-implemented functions that need caller's @_. + * + * Migrated to PerlRuntime.argsStack for reduced ThreadLocal overhead. */ - private static final ThreadLocal> argsStack = - ThreadLocal.withInitial(ArrayDeque::new); /** * Get the current subroutine's @_ array. @@ -158,7 +162,7 @@ protected boolean removeEldestEntry(Map.Entry, MethodHandle> eldest) { * @return The current @_ array, or null if not in a subroutine */ public static RuntimeArray getCurrentArgs() { - Deque stack = argsStack.get(); + Deque stack = PerlRuntime.current().argsStack; return stack.isEmpty() ? null : stack.peek(); } @@ -174,7 +178,7 @@ public static RuntimeArray getCurrentArgs() { * @return The caller's @_ array, or null if not available */ public static RuntimeArray getCallerArgs() { - Deque stack = argsStack.get(); + Deque stack = PerlRuntime.current().argsStack; if (stack.size() < 2) { return null; } @@ -188,7 +192,7 @@ public static RuntimeArray getCallerArgs() { * Public so BytecodeInterpreter can use it when calling InterpretedCode directly. */ public static void pushArgs(RuntimeArray args) { - argsStack.get().push(args); + PerlRuntime.current().argsStack.push(args); } /** @@ -196,7 +200,7 @@ public static void pushArgs(RuntimeArray args) { * Public so BytecodeInterpreter can use it when calling InterpretedCode directly. */ public static void popArgs() { - Deque stack = argsStack.get(); + Deque stack = PerlRuntime.current().argsStack; if (!stack.isEmpty()) { stack.pop(); } @@ -229,29 +233,28 @@ public static void popArgs() { * This optimization provides ~50% speedup for method-heavy code like: * while ($i < 10000) { $obj->method($arg); $i++ } */ - private static final int METHOD_CALL_CACHE_SIZE = 4096; - private static final int[] inlineCacheBlessId = new int[METHOD_CALL_CACHE_SIZE]; - private static final int[] inlineCacheMethodHash = new int[METHOD_CALL_CACHE_SIZE]; - private static final RuntimeCode[] inlineCacheCode = new RuntimeCode[METHOD_CALL_CACHE_SIZE]; + // Inline cache arrays migrated to PerlRuntime; access via PerlRuntime.current() private static int nextCallsiteId = 0; public static int allocateMethodCallsiteId() { - return nextCallsiteId++ % METHOD_CALL_CACHE_SIZE; + return nextCallsiteId++ % PerlRuntime.METHOD_CALL_CACHE_SIZE; } /** * Clear the inline method cache. Should be called when method definitions change. */ public static void clearInlineMethodCache() { - java.util.Arrays.fill(inlineCacheBlessId, 0); - java.util.Arrays.fill(inlineCacheMethodHash, 0); - java.util.Arrays.fill(inlineCacheCode, null); + PerlRuntime rt = PerlRuntime.current(); + java.util.Arrays.fill(rt.inlineCacheBlessId, 0); + java.util.Arrays.fill(rt.inlineCacheMethodHash, 0); + java.util.Arrays.fill(rt.inlineCacheCode, null); } - // Temporary storage for anonymous subroutines and eval string compiler context - public static HashMap> anonSubs = new HashMap<>(); // temp storage for makeCodeObject() - public static HashMap interpretedSubs = new HashMap<>(); // storage for interpreter fallback closures - public static HashMap evalContext = new HashMap<>(); // storage for eval string compiler context + // anonSubs, interpretedSubs, evalContext migrated to PerlRuntime; access via getters + public static HashMap> getAnonSubs() { return PerlRuntime.current().anonSubs; } + public static HashMap getInterpretedSubs() { return PerlRuntime.current().interpretedSubs; } + @SuppressWarnings("unchecked") + public static HashMap getEvalContext() { return (HashMap) (HashMap) PerlRuntime.current().evalContext; } // Runtime eval counter for generating unique filenames when $^P is set private static int runtimeEvalCounter = 1; // Method object representing the compiled subroutine (legacy - used by PerlModuleBase) @@ -471,7 +474,7 @@ public static boolean hasAutoload(RuntimeCode code) { * @return The current eval runtime context, or null if not in eval STRING compilation */ public static EvalRuntimeContext getEvalRuntimeContext() { - return evalRuntimeContext.get(); + return (EvalRuntimeContext) PerlRuntime.current().evalRuntimeContext; } /** @@ -482,9 +485,10 @@ public static EvalRuntimeContext getEvalRuntimeContext() { * @return The saved eval runtime context (may be null) */ public static EvalRuntimeContext saveAndClearEvalRuntimeContext() { - EvalRuntimeContext saved = evalRuntimeContext.get(); + PerlRuntime rt = PerlRuntime.current(); + EvalRuntimeContext saved = (EvalRuntimeContext) rt.evalRuntimeContext; if (saved != null) { - evalRuntimeContext.remove(); + rt.evalRuntimeContext = null; } return saved; } @@ -496,7 +500,7 @@ public static EvalRuntimeContext saveAndClearEvalRuntimeContext() { */ public static void restoreEvalRuntimeContext(EvalRuntimeContext saved) { if (saved != null) { - evalRuntimeContext.set(saved); + PerlRuntime.current().evalRuntimeContext = saved; } } @@ -512,12 +516,13 @@ public static synchronized String getNextEvalFilename() { // Add a method to clear caches when globals are reset public static void clearCaches() { - evalCache.clear(); - methodHandleCache.clear(); - anonSubs.clear(); - interpretedSubs.clear(); - evalContext.clear(); - evalRuntimeContext.remove(); + PerlRuntime rt = PerlRuntime.current(); + rt.evalCache.clear(); + rt.methodHandleCache.clear(); + rt.anonSubs.clear(); + rt.interpretedSubs.clear(); + rt.evalContext.clear(); + rt.evalRuntimeContext = null; } public static void copy(RuntimeCode code, RuntimeCode codeFrom) { @@ -571,8 +576,13 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag) thro */ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Object[] runtimeValues) throws Exception { + // Acquire the global compile lock — the parser and emitter have shared mutable + // static state that is not thread-safe for concurrent compilation. + PerlLanguageProvider.COMPILE_LOCK.lock(); + try { + // Retrieve the eval context that was saved at program compile-time - EmitterContext ctx = RuntimeCode.evalContext.get(evalTag); + EmitterContext ctx = RuntimeCode.getEvalContext().get(evalTag); // Handle missing eval context - this can happen when compiled code (e.g., INIT blocks // with eval) is executed after the runtime has been reset. In JUnit parallel tests, @@ -605,7 +615,7 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje ctx.capturedEnv, // Variable names in same order as runtimeValues evalTag ); - evalRuntimeContext.set(runtimeCtx); + PerlRuntime.current().evalRuntimeContext = runtimeCtx; try { // Check if the eval string contains non-ASCII characters @@ -661,9 +671,10 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje String cacheKey = code.toString() + '\0' + evalTag + '\0' + hasUnicode + '\0' + ctx.isEvalbytes + '\0' + isByteStringSource + '\0' + featureFlags + '\0' + currentPackage; Class cachedClass = null; if (!isDebugging) { - synchronized (evalCache) { - if (evalCache.containsKey(cacheKey)) { - cachedClass = evalCache.get(cacheKey); + Map> cache = getEvalCache(); + synchronized (cache) { + if (cache.containsKey(cacheKey)) { + cachedClass = cache.get(cacheKey); } } @@ -730,19 +741,19 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // variable reinitialization in loops. OperatorNode ast = entry.ast(); if (ast != null) { - int beginId = evalBeginIds.computeIfAbsent( + int beginId = getEvalBeginIds().computeIfAbsent( ast, - k -> EmitterMethodCreator.classCounter++); + k -> EmitterMethodCreator.classCounter.getAndIncrement()); String packageName = PersistentVariable.beginPackage(beginId); String varNameWithoutSigil = entry.name().substring(1); String fullName = packageName + "::" + varNameWithoutSigil; if (runtimeValue instanceof RuntimeArray) { - GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue); + GlobalVariable.getGlobalArraysMap().put(fullName, (RuntimeArray) runtimeValue); } else if (runtimeValue instanceof RuntimeHash) { - GlobalVariable.globalHashes.put(fullName, (RuntimeHash) runtimeValue); + GlobalVariable.getGlobalHashesMap().put(fullName, (RuntimeHash) runtimeValue); } else if (runtimeValue instanceof RuntimeScalar) { - GlobalVariable.globalVariables.put(fullName, (RuntimeScalar) runtimeValue); + GlobalVariable.getGlobalVariablesMap().put(fullName, (RuntimeScalar) runtimeValue); } evalAliasKeys.add(entry.name().charAt(0) + fullName); } @@ -885,9 +896,9 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje for (String key : evalAliasKeys) { String fullName = key.substring(1); switch (key.charAt(0)) { - case '$' -> GlobalVariable.globalVariables.remove(fullName); - case '@' -> GlobalVariable.globalArrays.remove(fullName); - case '%' -> GlobalVariable.globalHashes.remove(fullName); + case '$' -> GlobalVariable.getGlobalVariablesMap().remove(fullName); + case '@' -> GlobalVariable.getGlobalArraysMap().remove(fullName); + case '%' -> GlobalVariable.getGlobalHashesMap().remove(fullName); } } @@ -899,8 +910,9 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // Cache the result (unless debugging is enabled) if (!isDebugging) { - synchronized (evalCache) { - evalCache.put(cacheKey, generatedClass); + Map> cache = getEvalCache(); + synchronized (cache) { + cache.put(cacheKey, generatedClass); } } @@ -915,7 +927,11 @@ public static Class evalStringHelper(RuntimeScalar code, String evalTag, Obje // IMPORTANT: Always clean up ThreadLocal in finally block to ensure it's removed // even if compilation fails. Failure to do so could cause memory leaks in // long-running applications with thread pools. - evalRuntimeContext.remove(); + PerlRuntime.current().evalRuntimeContext = null; + } + + } finally { + PerlLanguageProvider.COMPILE_LOCK.unlock(); } } @@ -1055,7 +1071,7 @@ public static RuntimeList evalStringWithInterpreter( " codeType=" + code.type + " codeLen=" + (code.toString() != null ? code.toString().length() : -1)); // Retrieve the eval context that was saved at program compile-time - EmitterContext ctx = RuntimeCode.evalContext.get(evalTag); + EmitterContext ctx = RuntimeCode.getEvalContext().get(evalTag); // Handle missing eval context - this can happen when compiled code (e.g., INIT blocks // with eval) is executed after the runtime has been reset. In JUnit parallel tests, @@ -1070,6 +1086,11 @@ public static RuntimeList evalStringWithInterpreter( "If this occurs in tests, ensure module caches are cleared along with eval contexts."); } + // Acquire the global compile lock for the parsing/compilation phase. + // The parser and emitter have shared mutable static state that is not thread-safe. + PerlLanguageProvider.COMPILE_LOCK.lock(); + boolean compileLockReleased = false; + // Save the current scope so we can restore it after eval execution. // This is critical because eval may be called from code compiled with different // warning/feature flags than the caller, and we must not leak the eval's scope. @@ -1081,7 +1102,7 @@ public static RuntimeList evalStringWithInterpreter( ctx.capturedEnv, evalTag ); - evalRuntimeContext.set(runtimeCtx); + PerlRuntime.current().evalRuntimeContext = runtimeCtx; InterpretedCode interpretedCode = null; RuntimeList result; @@ -1148,19 +1169,19 @@ public static RuntimeList evalStringWithInterpreter( if (runtimeValue != null) { OperatorNode operatorAst = entry.ast(); if (operatorAst != null) { - int beginId = evalBeginIds.computeIfAbsent( + int beginId = getEvalBeginIds().computeIfAbsent( operatorAst, - k -> EmitterMethodCreator.classCounter++); + k -> EmitterMethodCreator.classCounter.getAndIncrement()); String packageName = PersistentVariable.beginPackage(beginId); String varNameWithoutSigil = entry.name().substring(1); String fullName = packageName + "::" + varNameWithoutSigil; if (runtimeValue instanceof RuntimeArray) { - GlobalVariable.globalArrays.put(fullName, (RuntimeArray) runtimeValue); + GlobalVariable.getGlobalArraysMap().put(fullName, (RuntimeArray) runtimeValue); } else if (runtimeValue instanceof RuntimeHash) { - GlobalVariable.globalHashes.put(fullName, (RuntimeHash) runtimeValue); + GlobalVariable.getGlobalHashesMap().put(fullName, (RuntimeHash) runtimeValue); } else if (runtimeValue instanceof RuntimeScalar) { - GlobalVariable.globalVariables.put(fullName, (RuntimeScalar) runtimeValue); + GlobalVariable.getGlobalVariablesMap().put(fullName, (RuntimeScalar) runtimeValue); } evalAliasKeys.add(entry.name().charAt(0) + fullName); } @@ -1323,16 +1344,24 @@ public static RuntimeList evalStringWithInterpreter( for (String key : evalAliasKeys) { String fullName = key.substring(1); switch (key.charAt(0)) { - case '$' -> GlobalVariable.globalVariables.remove(fullName); - case '@' -> GlobalVariable.globalArrays.remove(fullName); - case '%' -> GlobalVariable.globalHashes.remove(fullName); + case '$' -> GlobalVariable.getGlobalVariablesMap().remove(fullName); + case '@' -> GlobalVariable.getGlobalArraysMap().remove(fullName); + case '%' -> GlobalVariable.getGlobalHashesMap().remove(fullName); } } evalAliasKeys.clear(); + // Restore the current scope under the compile lock, before releasing it. + // setCurrentScope touches shared parser state (SpecialBlockParser.symbolTable). + setCurrentScope(savedCurrentScope); + + // Release the compile lock — execution is thread-safe and doesn't need it. + PerlLanguageProvider.COMPILE_LOCK.unlock(); + compileLockReleased = true; + // Execute the interpreted code // Track eval depth for $^S support - evalDepth++; + incrementEvalDepth(); try { result = interpretedCode.apply(args, callContext); @@ -1390,9 +1419,8 @@ public static RuntimeList evalStringWithInterpreter( return new RuntimeList(new RuntimeScalar()); } } finally { - evalDepth--; + decrementEvalDepth(); } - } finally { evalTrace("evalStringWithInterpreter exit tag=" + evalTag + " ctx=" + callContext + " $@=" + GlobalVariable.getGlobalVariable("main::@")); @@ -1401,6 +1429,9 @@ public static RuntimeList evalStringWithInterpreter( // Restore the original current scope, not the captured symbol table. // This prevents eval from leaking its compile-time scope to the caller. + // On the success path, setCurrentScope was already called before the lock was + // released; this is a no-op. On the error path, the lock is still held and + // this restores the scope before we release it below. setCurrentScope(savedCurrentScope); // Store source lines in debugger symbol table if $^P flags are set @@ -1412,8 +1443,16 @@ public static RuntimeList evalStringWithInterpreter( storeSourceLines(code.toString(), evalFilename, ast, tokens); } - // Clean up ThreadLocal - evalRuntimeContext.remove(); + // Clean up eval runtime context + PerlRuntime.current().evalRuntimeContext = null; + + // Release the compile lock if still held (error path — success path releases it earlier). + // Use a boolean flag instead of isHeldByCurrentThread() to avoid over-decrementing + // the ReentrantLock hold count in nested scenarios (e.g., BEGIN block triggers + // inner evalStringWithInterpreter while outer compilation holds the lock). + if (!compileLockReleased) { + PerlLanguageProvider.COMPILE_LOCK.unlock(); + } } } @@ -1563,12 +1602,13 @@ public static RuntimeList callCached(int callsiteId, int blessId = ((RuntimeBase) invocant.value).blessId; if (blessId != 0) { int methodHash = System.identityHashCode(method.value); - int cacheIndex = callsiteId & (METHOD_CALL_CACHE_SIZE - 1); + int cacheIndex = callsiteId & (PerlRuntime.METHOD_CALL_CACHE_SIZE - 1); + PerlRuntime rt = PerlRuntime.current(); // Check if cache hit - if (inlineCacheBlessId[cacheIndex] == blessId && - inlineCacheMethodHash[cacheIndex] == methodHash) { - RuntimeCode cachedCode = inlineCacheCode[cacheIndex]; + if (rt.inlineCacheBlessId[cacheIndex] == blessId && + rt.inlineCacheMethodHash[cacheIndex] == methodHash) { + RuntimeCode cachedCode = rt.inlineCacheCode[cacheIndex]; if (cachedCode != null && (cachedCode.subroutine != null || cachedCode.methodHandle != null)) { // Cache hit - ultra fast path: directly invoke method try { @@ -1627,9 +1667,9 @@ public static RuntimeList callCached(int callsiteId, // Only cache if method is defined and has a subroutine or method handle if (code.subroutine != null || code.methodHandle != null) { // Update cache - inlineCacheBlessId[cacheIndex] = blessId; - inlineCacheMethodHash[cacheIndex] = methodHash; - inlineCacheCode[cacheIndex] = code; + rt.inlineCacheBlessId[cacheIndex] = blessId; + rt.inlineCacheMethodHash[cacheIndex] = methodHash; + rt.inlineCacheCode[cacheIndex] = code; } // Call the method @@ -2222,15 +2262,9 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int // Look up warning bits for the code's class and push to context stack // This enables FATAL warnings to work even at top-level (no caller frame) String warningBits = getWarningBitsForCode(code); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } - // Save caller's call-site warning bits so caller()[9] can retrieve them - WarningBitsRegistry.pushCallerBits(); - // Save caller's $^H so caller()[8] can retrieve them - WarningBitsRegistry.pushCallerHints(); - // Save caller's call-site hint hash so caller()[10] can retrieve them - HintHashRegistry.pushCallerHintHash(); + // Batch push: caller bits, hints, hint hash, and warning bits in one PerlRuntime.current() call + PerlRuntime rt = PerlRuntime.current(); + rt.pushCallerState(warningBits); try { // Cast the value to RuntimeCode and call apply() RuntimeList result = code.apply(a, callContext); @@ -2257,12 +2291,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int // Consume at normal subroutine boundary return e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); } finally { - HintHashRegistry.popCallerHintHash(); - WarningBitsRegistry.popCallerHints(); - WarningBitsRegistry.popCallerBits(); - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } + rt.popCallerState(warningBits != null); // eval BLOCK is compiled as an immediately-invoked anonymous sub // (sub { ... }->()) that captures outer lexicals, incrementing their // captureCount. Unlike a normal closure that may be stored and reused, @@ -2317,7 +2346,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, RuntimeArray a, int // Eval STRING must allow next/last/redo to propagate to the enclosing scope. // The caller is responsible for handling RuntimeControlFlowList markers. public static RuntimeList applyEval(RuntimeScalar runtimeScalar, RuntimeArray a, int callContext) { - evalDepth++; + incrementEvalDepth(); try { RuntimeList result = apply(runtimeScalar, a, callContext); // Perl clears $@ on successful eval (even if nested evals previously set it). @@ -2350,7 +2379,7 @@ public static RuntimeList applyEval(RuntimeScalar runtimeScalar, RuntimeArray a, } return new RuntimeList(new RuntimeScalar()); } finally { - evalDepth--; + decrementEvalDepth(); // Release captured variable references from the eval's code object. // After eval STRING finishes executing, its captures are no longer needed. if (runtimeScalar.type == RuntimeScalarType.CODE && runtimeScalar.value instanceof RuntimeCode code) { @@ -2476,15 +2505,9 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa if (code.defined()) { // Look up warning bits for the code's class and push to context stack String warningBits = getWarningBitsForCode(code); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } - // Save caller's call-site warning bits so caller()[9] can retrieve them - WarningBitsRegistry.pushCallerBits(); - // Save caller's $^H so caller()[8] can retrieve them - WarningBitsRegistry.pushCallerHints(); - // Save caller's call-site hint hash so caller()[10] can retrieve them - HintHashRegistry.pushCallerHintHash(); + // Batch push: caller bits, hints, hint hash, and warning bits in one PerlRuntime.current() call + PerlRuntime rt = PerlRuntime.current(); + rt.pushCallerState(warningBits); try { // Cast the value to RuntimeCode and call apply() return code.apply(subroutineName, a, callContext); @@ -2496,12 +2519,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa // Consume at normal subroutine boundary return e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); } finally { - HintHashRegistry.popCallerHintHash(); - WarningBitsRegistry.popCallerHints(); - WarningBitsRegistry.popCallerBits(); - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } + rt.popCallerState(warningBits != null); } } @@ -2642,15 +2660,9 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa if (code.defined()) { // Look up warning bits for the code's class and push to context stack String warningBits = getWarningBitsForCode(code); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } - // Save caller's call-site warning bits so caller()[9] can retrieve them - WarningBitsRegistry.pushCallerBits(); - // Save caller's $^H so caller()[8] can retrieve them - WarningBitsRegistry.pushCallerHints(); - // Save caller's call-site hint hash so caller()[10] can retrieve them - HintHashRegistry.pushCallerHintHash(); + // Batch push: caller bits, hints, hint hash, and warning bits in one PerlRuntime.current() call + PerlRuntime rt = PerlRuntime.current(); + rt.pushCallerState(warningBits); try { // Cast the value to RuntimeCode and call apply() return code.apply(subroutineName, a, callContext); @@ -2662,12 +2674,7 @@ public static RuntimeList apply(RuntimeScalar runtimeScalar, String subroutineNa // Consume at normal subroutine boundary return e.returnValue != null ? e.returnValue.getList() : new RuntimeList(); } finally { - HintHashRegistry.popCallerHintHash(); - WarningBitsRegistry.popCallerHints(); - WarningBitsRegistry.popCallerBits(); - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } + rt.popCallerState(warningBits != null); } } @@ -3063,13 +3070,11 @@ public RuntimeList apply(RuntimeArray a, int callContext) { DebugHooks.enterSubroutine(debugSubName); } // Always push args for getCurrentArgs() support (used by List::Util::any/all/etc.) - pushArgs(a); - // Push warning bits for FATAL warnings support + // Batch push: args + warning bits in one PerlRuntime.current() call String warningBits = getWarningBitsForCode(this); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } + PerlRuntime rt = PerlRuntime.current(); + rt.pushSubState(a, warningBits); try { RuntimeList result; // Prefer functional interface over MethodHandle for better performance @@ -3082,10 +3087,7 @@ public RuntimeList apply(RuntimeArray a, int callContext) { } return result; } finally { - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } - popArgs(); + rt.popSubState(warningBits != null); if (DebugState.debugMode) { DebugHooks.exitSubroutine(); DebugState.popArgs(); @@ -3160,13 +3162,11 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) DebugHooks.enterSubroutine(debugSubName); } // Always push args for getCurrentArgs() support (used by List::Util::any/all/etc.) - pushArgs(a); - // Push warning bits for FATAL warnings support + // Batch push: args + warning bits in one PerlRuntime.current() call String warningBits = getWarningBitsForCode(this); - if (warningBits != null) { - WarningBitsRegistry.pushCurrent(warningBits); - } + PerlRuntime rt = PerlRuntime.current(); + rt.pushSubState(a, warningBits); try { RuntimeList result; // Prefer functional interface over MethodHandle for better performance @@ -3179,10 +3179,7 @@ public RuntimeList apply(String subroutineName, RuntimeArray a, int callContext) } return result; } finally { - if (warningBits != null) { - WarningBitsRegistry.popCurrent(); - } - popArgs(); + rt.popSubState(warningBits != null); if (DebugState.debugMode) { DebugHooks.exitSubroutine(); DebugState.popArgs(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java index 9accc7559..0945d2182 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeGlob.java @@ -15,7 +15,11 @@ */ public class RuntimeGlob extends RuntimeScalar implements RuntimeScalarReference { - private static final Stack globSlotStack = new Stack<>(); + // Glob slot stack is now held per-PerlRuntime. + @SuppressWarnings("unchecked") + private static Stack globSlotStack() { + return (Stack) (Stack) PerlRuntime.current().globSlotStack; + } // The name of the typeglob public String globName; public RuntimeScalar IO; @@ -151,7 +155,7 @@ public boolean equals(Object obj) { } public static boolean isGlobAssigned(String globName) { - return GlobalVariable.globalGlobs.getOrDefault(globName, false); + return GlobalVariable.getGlobalGlobsMap().getOrDefault(globName, false); } /** @@ -163,27 +167,27 @@ public static boolean isGlobAssigned(String globName) { */ public RuntimeScalar defined() { // Check if the glob has been assigned (any slot has content) - if (GlobalVariable.globalGlobs.getOrDefault(this.globName, false)) { + if (GlobalVariable.getGlobalGlobsMap().getOrDefault(this.globName, false)) { return RuntimeScalarCache.scalarTrue; } // Check scalar slot - must have defined value - if (GlobalVariable.globalVariables.containsKey(this.globName)) { - RuntimeScalar scalar = GlobalVariable.globalVariables.get(this.globName); + if (GlobalVariable.getGlobalVariablesMap().containsKey(this.globName)) { + RuntimeScalar scalar = GlobalVariable.getGlobalVariablesMap().get(this.globName); if (scalar != null && scalar.getDefinedBoolean()) { return RuntimeScalarCache.scalarTrue; } } // Check array slot - exists = defined (even if empty) - if (GlobalVariable.globalArrays.containsKey(this.globName)) { + if (GlobalVariable.getGlobalArraysMap().containsKey(this.globName)) { return RuntimeScalarCache.scalarTrue; } // Check hash slot - exists = defined (even if empty) - if (GlobalVariable.globalHashes.containsKey(this.globName)) { + if (GlobalVariable.getGlobalHashesMap().containsKey(this.globName)) { return RuntimeScalarCache.scalarTrue; } // Check code slot - must have defined value - if (GlobalVariable.globalCodeRefs.containsKey(this.globName)) { - RuntimeScalar code = GlobalVariable.globalCodeRefs.get(this.globName); + if (GlobalVariable.getGlobalCodeRefsMap().containsKey(this.globName)) { + RuntimeScalar code = GlobalVariable.getGlobalCodeRefsMap().get(this.globName); if (code != null && code.getDefinedBoolean()) { return RuntimeScalarCache.scalarTrue; } @@ -235,7 +239,7 @@ public RuntimeScalar set(RuntimeScalar value) { // isSubs for any CODE typeglob assignment — the parser only checks // isSubs for names in the OVERRIDABLE_OP set, so marking non-overridable // names has no effect. - GlobalVariable.isSubs.put(this.globName, true); + GlobalVariable.getIsSubsMap().put(this.globName, true); // Increment package generation counter for mro::get_pkg_gen int lastColonIdx = this.globName.lastIndexOf("::"); @@ -260,7 +264,7 @@ public RuntimeScalar set(RuntimeScalar value) { // Also update all glob aliases if (value.value instanceof RuntimeArray arr) { for (String aliasedName : GlobalVariable.getGlobAliasGroup(this.globName)) { - GlobalVariable.globalArrays.put(aliasedName, arr); + GlobalVariable.getGlobalArraysMap().put(aliasedName, arr); } // Mark as explicitly declared for strict vars (e.g., Exporter imports) GlobalVariable.declareGlobalArray(this.globName); @@ -271,7 +275,7 @@ public RuntimeScalar set(RuntimeScalar value) { // Also update all glob aliases if (value.value instanceof RuntimeHash hash) { for (String aliasedName : GlobalVariable.getGlobAliasGroup(this.globName)) { - GlobalVariable.globalHashes.put(aliasedName, hash); + GlobalVariable.getGlobalHashesMap().put(aliasedName, hash); } // Mark as explicitly declared for strict vars (e.g., Exporter imports) GlobalVariable.declareGlobalHash(this.globName); @@ -361,9 +365,9 @@ public RuntimeScalar set(RuntimeGlob value) { // Update selectedHandle if the old IO was the currently selected output handle. // This ensures that `local *STDOUT = $fh` redirects bare `print` (no filehandle) // to the new handle, not just explicit `print STDOUT`. - if (oldRuntimeIO != null && oldRuntimeIO == RuntimeIO.selectedHandle + if (oldRuntimeIO != null && oldRuntimeIO == RuntimeIO.getSelectedHandle() && value.IO != null && value.IO.value instanceof RuntimeIO newRIO) { - RuntimeIO.selectedHandle = newRIO; + RuntimeIO.setSelectedHandle(newRIO); } return value.scalar(); @@ -414,22 +418,22 @@ public RuntimeScalar set(RuntimeGlob value) { targetIO.IO = sourceIO.IO; // Update selectedHandle if the old IO was the currently selected output handle - if (oldRuntimeIO != null && oldRuntimeIO == RuntimeIO.selectedHandle + if (oldRuntimeIO != null && oldRuntimeIO == RuntimeIO.getSelectedHandle() && sourceIO.IO != null && sourceIO.IO.value instanceof RuntimeIO newRIO) { - RuntimeIO.selectedHandle = newRIO; + RuntimeIO.setSelectedHandle(newRIO); } // Alias the ARRAY slot: both names point to the same RuntimeArray object RuntimeArray sourceArray = GlobalVariable.getGlobalArray(globName); - GlobalVariable.globalArrays.put(this.globName, sourceArray); + GlobalVariable.getGlobalArraysMap().put(this.globName, sourceArray); // Alias the HASH slot: both names point to the same RuntimeHash object RuntimeHash sourceHash = GlobalVariable.getGlobalHash(globName); - GlobalVariable.globalHashes.put(this.globName, sourceHash); + GlobalVariable.getGlobalHashesMap().put(this.globName, sourceHash); // Alias the SCALAR slot: both names point to the same RuntimeScalar object RuntimeScalar sourceScalar = GlobalVariable.getGlobalVariable(globName); - GlobalVariable.globalVariables.put(this.globName, sourceScalar); + GlobalVariable.getGlobalVariablesMap().put(this.globName, sourceScalar); // Alias the FORMAT slot: both names point to the same RuntimeFormat object RuntimeFormat sourceFormat = GlobalVariable.getGlobalFormatRef(globName); @@ -452,7 +456,7 @@ private void markGlobAsAssigned() { // // Later, during compilation of built-in operators (like 'do EXPR'), we can consult // this map to determine whether to check for an override in CORE::GLOBAL. - GlobalVariable.globalGlobs.put(globName, true); + GlobalVariable.getGlobalGlobsMap().put(globName, true); } /** @@ -478,7 +482,7 @@ public RuntimeScalar getGlobSlot(RuntimeScalar index) { // we want to see if the sub is actually in the stash, not if it was // ever defined and pinned. This is critical for Moo's bootstrap // mechanism where a sub deletes itself from the stash. - RuntimeScalar codeRef = GlobalVariable.globalCodeRefs.get(this.globName); + RuntimeScalar codeRef = GlobalVariable.getGlobalCodeRefsMap().get(this.globName); if (codeRef != null && codeRef.type == RuntimeScalarType.CODE && codeRef.value instanceof RuntimeCode code) { if (code.defined() || code.isDeclared) { yield codeRef; @@ -614,8 +618,8 @@ public RuntimeGlob setIO(RuntimeScalar io) { if (io.value instanceof RuntimeIO runtimeIO) { runtimeIO.globName = this.globName; // Update selectedHandle if the old IO was the selected handle - if (oldIO != null && oldIO == RuntimeIO.selectedHandle) { - RuntimeIO.selectedHandle = runtimeIO; + if (oldIO != null && oldIO == RuntimeIO.getSelectedHandle()) { + RuntimeIO.setSelectedHandle(runtimeIO); } } return this; @@ -640,8 +644,8 @@ public RuntimeGlob setIO(RuntimeIO io) { // Update selectedHandle if the old IO was the selected handle // This ensures that when STDOUT is redirected, print without explicit // filehandle uses the new handle - if (oldIO != null && oldIO == RuntimeIO.selectedHandle) { - RuntimeIO.selectedHandle = io; + if (oldIO != null && oldIO == RuntimeIO.getSelectedHandle()) { + RuntimeIO.setSelectedHandle(io); } return this; } @@ -867,10 +871,10 @@ public RuntimeGlob undefine() { GlobalVariable.getGlobalVariable(this.globName).set(new RuntimeScalar()); // Undefine ARRAY - create empty array - GlobalVariable.globalArrays.put(this.globName, new RuntimeArray()); + GlobalVariable.getGlobalArraysMap().put(this.globName, new RuntimeArray()); // Undefine HASH - create empty hash - GlobalVariable.globalHashes.put(this.globName, new RuntimeHash()); + GlobalVariable.getGlobalHashesMap().put(this.globName, new RuntimeHash()); return this; } @@ -889,24 +893,24 @@ public void dynamicSaveState() { // after Capture::Tiny or similar modules localize STDOUT. RuntimeIO savedSelectedHandle = null; boolean isSelectedHandle = false; - if (this.IO != null && this.IO.value instanceof RuntimeIO rio && rio == RuntimeIO.selectedHandle) { - savedSelectedHandle = RuntimeIO.selectedHandle; + if (this.IO != null && this.IO.value instanceof RuntimeIO rio && rio == RuntimeIO.getSelectedHandle()) { + savedSelectedHandle = RuntimeIO.getSelectedHandle(); isSelectedHandle = true; - } else if (this.IO != null && this.IO.type == TIED_SCALAR && this.IO.value == RuntimeIO.selectedHandle) { - savedSelectedHandle = RuntimeIO.selectedHandle; + } else if (this.IO != null && this.IO.type == TIED_SCALAR && this.IO.value == RuntimeIO.getSelectedHandle()) { + savedSelectedHandle = RuntimeIO.getSelectedHandle(); isSelectedHandle = true; } - globSlotStack.push(new GlobSlotSnapshot(this.globName, savedScalar, savedArray, savedHash, savedCode, savedIO, savedSelectedHandle)); + globSlotStack().push(new GlobSlotSnapshot(this.globName, savedScalar, savedArray, savedHash, savedCode, savedIO, savedSelectedHandle)); // Replace global table entries with NEW empty objects instead of mutating the // existing ones in-place. This is critical because the existing objects may be // aliased (e.g., via *glob = $blessed_ref), and calling dynamicSaveState() on // them would clear/corrupt the original blessed reference's data. - GlobalVariable.globalVariables.put(this.globName, new RuntimeScalar()); - GlobalVariable.globalArrays.put(this.globName, new RuntimeArray()); - GlobalVariable.globalHashes.put(this.globName, new RuntimeHash()); + GlobalVariable.getGlobalVariablesMap().put(this.globName, new RuntimeScalar()); + GlobalVariable.getGlobalArraysMap().put(this.globName, new RuntimeArray()); + GlobalVariable.getGlobalHashesMap().put(this.globName, new RuntimeHash()); RuntimeScalar newCode = new RuntimeScalar(); - GlobalVariable.globalCodeRefs.put(this.globName, newCode); + GlobalVariable.getGlobalCodeRefsMap().put(this.globName, newCode); // Also redirect pinnedCodeRefs to the new empty code for the local scope. // Without this, getGlobalCodeRef() returns the saved (pinned) object, and // assignments during the local scope would mutate the saved snapshot instead @@ -934,15 +938,15 @@ public void dynamicSaveState() { RuntimeIO stubIO = new RuntimeIO(); stubIO.globName = this.globName; newGlob.IO = new RuntimeScalar(stubIO); - RuntimeIO.selectedHandle = stubIO; + RuntimeIO.setSelectedHandle(stubIO); } - GlobalVariable.globalIORefs.put(this.globName, newGlob); + GlobalVariable.getGlobalIORefsMap().put(this.globName, newGlob); } @Override public void dynamicRestoreState() { - GlobSlotSnapshot snap = globSlotStack.pop(); + GlobSlotSnapshot snap = globSlotStack().pop(); // Restore the saved IO object reference on this (old) glob. this.IO = snap.io; @@ -951,19 +955,19 @@ public void dynamicRestoreState() { // This ensures that after local(*STDOUT) + restore, print without explicit // filehandle goes through the correct (possibly tied) handle. if (snap.savedSelectedHandle != null) { - RuntimeIO.selectedHandle = snap.savedSelectedHandle; + RuntimeIO.setSelectedHandle(snap.savedSelectedHandle); } // Put this (old) glob back in globalIORefs, replacing the local scope's glob. // Any references captured during the local scope still point to the local glob, // which is now an independent orphaned glob (matching Perl 5 GV behavior). - GlobalVariable.globalIORefs.put(snap.globName, this); + GlobalVariable.getGlobalIORefsMap().put(snap.globName, this); // Restore saved objects directly - they were never mutated, so no // dynamicRestoreState() call is needed. - GlobalVariable.globalVariables.put(snap.globName, snap.scalar); - GlobalVariable.globalHashes.put(snap.globName, snap.hash); - GlobalVariable.globalArrays.put(snap.globName, snap.array); + GlobalVariable.getGlobalVariablesMap().put(snap.globName, snap.scalar); + GlobalVariable.getGlobalHashesMap().put(snap.globName, snap.hash); + GlobalVariable.getGlobalArraysMap().put(snap.globName, snap.array); // Before replacing the code ref, decrement the refCount of the CODE // that was installed during the local scope. The local scope's code @@ -973,14 +977,14 @@ public void dynamicRestoreState() { // `local *Foo::bar; sub bar { ... }` in Sub::Quote's unquote_sub) // have permanently overcounted refCount, preventing releaseCaptures // from firing at the right time. - RuntimeScalar localCode = GlobalVariable.globalCodeRefs.get(snap.globName); + RuntimeScalar localCode = GlobalVariable.getGlobalCodeRefsMap().get(snap.globName); if (localCode != null && (localCode.type & REFERENCE_BIT) != 0 && localCode.value instanceof RuntimeBase localBase) { if (localBase.refCount > 0 && --localBase.refCount == 0) { localBase.refCount = Integer.MIN_VALUE; DestroyDispatch.callDestroy(localBase); } } - GlobalVariable.globalCodeRefs.put(snap.globName, snap.code); + GlobalVariable.getGlobalCodeRefsMap().put(snap.globName, snap.code); // Also restore the pinned code ref so getGlobalCodeRef() returns the // original code object again. GlobalVariable.replacePinnedCodeRef(snap.globName, snap.code); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java index df1085a3a..b15d59a11 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHash.java @@ -18,8 +18,10 @@ public class RuntimeHash extends RuntimeBase implements RuntimeScalarReference, public static final int PLAIN_HASH = 0; public static final int AUTOVIVIFY_HASH = 1; public static final int TIED_HASH = 2; - // Static stack to store saved "local" states of RuntimeHash instances - private static final Stack dynamicStateStack = new Stack<>(); + // Dynamic state stack is now held per-PerlRuntime. + private static Stack dynamicStateStack() { + return PerlRuntime.current().hashDynamicStateStack; + } private static final RuntimeArray EMPTY_KEYS = new RuntimeArray(); static { @@ -1030,7 +1032,7 @@ public void dynamicSaveState() { currentState.elements = new StableHashMap<>(this.elements); currentState.blessId = this.blessId; currentState.byteKeys = this.byteKeys != null ? new HashSet<>(this.byteKeys) : null; - dynamicStateStack.push(currentState); + dynamicStateStack().push(currentState); // Clear the hash this.elements.clear(); this.byteKeys = null; @@ -1043,9 +1045,9 @@ public void dynamicSaveState() { */ @Override public void dynamicRestoreState() { - if (!dynamicStateStack.isEmpty()) { + if (!dynamicStateStack().isEmpty()) { // Restore the elements map and blessId from the most recent saved state - RuntimeHash previousState = dynamicStateStack.pop(); + RuntimeHash previousState = dynamicStateStack().pop(); this.elements = previousState.elements; this.blessId = previousState.blessId; this.byteKeys = previousState.byteKeys; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHashProxyEntry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHashProxyEntry.java index 14ac502d9..1df693096 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHashProxyEntry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeHashProxyEntry.java @@ -8,7 +8,10 @@ * when they are accessed. */ public class RuntimeHashProxyEntry extends RuntimeBaseProxy { - private static final Stack dynamicStateStack = new Stack<>(); + // Dynamic state stack is now held per-PerlRuntime. + private static Stack dynamicStateStack() { + return PerlRuntime.current().hashProxyDynamicStateStack; + } // Reference to the parent RuntimeHash private final RuntimeHash parent; @@ -87,7 +90,7 @@ void vivify() { public void dynamicSaveState() { // Create a new RuntimeScalar to save the current state if (this.lvalue == null) { - dynamicStateStack.push(null); + dynamicStateStack().push(null); vivify(); } else { RuntimeScalar currentState = new RuntimeScalar(); @@ -95,7 +98,7 @@ public void dynamicSaveState() { currentState.type = this.lvalue.type; currentState.value = this.lvalue.value; currentState.blessId = this.lvalue.blessId; - dynamicStateStack.push(currentState); + dynamicStateStack().push(currentState); // Clear the current type and value this.undefine(); } @@ -109,9 +112,9 @@ public void dynamicSaveState() { */ @Override public void dynamicRestoreState() { - if (!dynamicStateStack.isEmpty()) { + if (!dynamicStateStack().isEmpty()) { // Pop the most recent saved state from the stack - RuntimeScalar previousState = dynamicStateStack.pop(); + RuntimeScalar previousState = dynamicStateStack().pop(); if (previousState == null) { // Key didn't exist before — remove it. // Decrement refCount of the current value being displaced. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java index 609fde661..b6d6d0201 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeIO.java @@ -91,71 +91,51 @@ public class RuntimeIO extends RuntimeScalar { private static final Map> MODE_OPTIONS = new HashMap<>(); /** - * Maximum number of file handles to keep in the LRU cache. - * Older handles are flushed (not closed) when this limit is exceeded. + * Returns the per-runtime LRU cache of open file handles. + * Migrated from a static field for multiplicity thread-safety. */ - private static final int MAX_OPEN_HANDLES = 100; - - /** - * LRU (Least Recently Used) cache for managing open file handles. - * This helps prevent resource exhaustion by limiting open handles and - * automatically flushing less recently used ones. - */ - private static final Map openHandles = new LinkedHashMap(MAX_OPEN_HANDLES, 0.75f, true) { - @Override - protected boolean removeEldestEntry(Map.Entry eldest) { - if (size() > MAX_OPEN_HANDLES) { - try { - // Flush but don't close the eldest handle - eldest.getKey().flush(); - } catch (Exception e) { - // Handle exception if needed - } - return true; - } - return false; - } - }; + private static Map openHandles() { + return PerlRuntime.current().openHandles; + } private static final Map childProcesses = new java.util.concurrent.ConcurrentHashMap<>(); - /** - * Standard output stream handle (STDOUT) - */ - public static RuntimeIO stdout = new RuntimeIO(new StandardIO(System.out, true)); - /** - * Standard error stream handle (STDERR) - * Note: autoFlush is set to true to match Perl's unbuffered stderr behavior - */ - public static RuntimeIO stderr = new RuntimeIO(new StandardIO(System.err, false)); - - static { - // STDERR should be unbuffered (autoFlush) by default, like in Perl - stderr.autoFlush = true; - } - - /** - * Standard input stream handle (STDIN) - */ - public static RuntimeIO stdin = new RuntimeIO(new StandardIO(System.in)); - /** - * The last accessed filehandle, used for Perl's ${^LAST_FH} special variable. - * Updated whenever a filehandle is used for I/O operations. - */ - public static RuntimeIO lastAccesseddHandle; - /** - * The variable/handle name used in the last readline operation (e.g., "$f", "STDIN"). - * Set by the JVM backend before calling readline, used by WarnDie for error messages - * when the handle's globName is null (e.g., lexical filehandles). - */ - public static String lastReadlineHandleName; - // Tracks the last handle used for output writes (print/say/etc). This must not - // clobber lastAccesseddHandle, which is used for ${^LAST_FH} and $. - public static RuntimeIO lastWrittenHandle; - /** - * The currently selected filehandle for output operations. - * Used by print/printf when no filehandle is specified. - */ - public static RuntimeIO selectedHandle; + + // ---- I/O state is now per-PerlRuntime. These static accessors delegate to current runtime. ---- + + /** Returns the standard output handle for the current runtime. */ + public static RuntimeIO getStdout() { return PerlRuntime.current().ioStdout; } + /** Sets the standard output handle for the current runtime. */ + public static void setStdout(RuntimeIO io) { PerlRuntime.current().ioStdout = io; } + + /** Returns the standard error handle for the current runtime. */ + public static RuntimeIO getStderr() { return PerlRuntime.current().ioStderr; } + /** Sets the standard error handle for the current runtime. */ + public static void setStderr(RuntimeIO io) { PerlRuntime.current().ioStderr = io; } + + /** Returns the standard input handle for the current runtime. */ + public static RuntimeIO getStdin() { return PerlRuntime.current().ioStdin; } + /** Sets the standard input handle for the current runtime. */ + public static void setStdin(RuntimeIO io) { PerlRuntime.current().ioStdin = io; } + + /** Returns the last accessed handle for the current runtime. */ + public static RuntimeIO getLastAccessedHandle() { return PerlRuntime.current().ioLastAccessedHandle; } + /** Sets the last accessed handle for the current runtime. */ + public static void setLastAccessedHandle(RuntimeIO io) { PerlRuntime.current().ioLastAccessedHandle = io; } + + /** Returns the last readline handle name for the current runtime. */ + public static String getLastReadlineHandleName() { return PerlRuntime.current().ioLastReadlineHandleName; } + /** Sets the last readline handle name for the current runtime. */ + public static void setLastReadlineHandleName(String name) { PerlRuntime.current().ioLastReadlineHandleName = name; } + + /** Returns the last written handle for the current runtime. */ + public static RuntimeIO getLastWrittenHandle() { return PerlRuntime.current().ioLastWrittenHandle; } + /** Sets the last written handle for the current runtime. */ + public static void setLastWrittenHandle(RuntimeIO io) { PerlRuntime.current().ioLastWrittenHandle = io; } + + /** Returns the currently selected output handle for the current runtime. */ + public static RuntimeIO getSelectedHandle() { return PerlRuntime.current().ioSelectedHandle; } + /** Sets the currently selected output handle for the current runtime. */ + public static void setSelectedHandle(RuntimeIO io) { PerlRuntime.current().ioSelectedHandle = io; } /** * Fileno registry for select() support. @@ -459,7 +439,7 @@ public static Process removeChildProcess(long pid) { * @param out the OutputStream to wrap */ public static void setCustomOutputStream(OutputStream out) { - lastWrittenHandle = new RuntimeIO(new CustomOutputStreamHandle(out)); + setLastWrittenHandle(new RuntimeIO(new CustomOutputStreamHandle(out))); } /** @@ -545,12 +525,12 @@ public static RuntimeScalar handleIOException(Exception e, String message, int d */ public static void initStdHandles() { // Initialize STDOUT, STDERR, STDIN in the main package - getGlobalIO("main::STDOUT").setIO(stdout); - getGlobalIO("main::STDERR").setIO(stderr); - getGlobalIO("main::STDIN").setIO(stdin); - lastAccesseddHandle = null; - lastWrittenHandle = stdout; - selectedHandle = stdout; + getGlobalIO("main::STDOUT").setIO(getStdout()); + getGlobalIO("main::STDERR").setIO(getStderr()); + getGlobalIO("main::STDIN").setIO(getStdin()); + setLastAccessedHandle(null); + setLastWrittenHandle(getStdout()); + setSelectedHandle(getStdout()); } /** @@ -956,8 +936,8 @@ public static Path resolvePath(String fileName, String opName) { return path.toAbsolutePath(); } - // For relative paths, resolve against current directory - return Paths.get(System.getProperty("user.dir")).resolve(sanitized).toAbsolutePath(); + // For relative paths, resolve against per-runtime current directory + return Paths.get(PerlRuntime.getCwd()).resolve(sanitized).toAbsolutePath(); } /** @@ -967,11 +947,13 @@ public static Path resolvePath(String fileName, String opName) { */ public static void flushFileHandles() { // Flush stdout and stderr before sleep, in case we are displaying a prompt - if (stdout.needFlush) { - stdout.flush(); + RuntimeIO out = getStdout(); + RuntimeIO err = getStderr(); + if (out.needFlush) { + out.flush(); } - if (stderr.needFlush) { - stderr.flush(); + if (err.needFlush) { + err.flush(); } } @@ -981,8 +963,9 @@ public static void flushFileHandles() { * @param handle the IOHandle to cache */ public static void addHandle(IOHandle handle) { - synchronized (openHandles) { - openHandles.put(handle, Boolean.TRUE); + Map handles = openHandles(); + synchronized (handles) { + handles.put(handle, Boolean.TRUE); } } @@ -992,8 +975,9 @@ public static void addHandle(IOHandle handle) { * @param handle the IOHandle to remove */ public static void removeHandle(IOHandle handle) { - synchronized (openHandles) { - openHandles.remove(handle); + Map handles = openHandles(); + synchronized (handles) { + handles.remove(handle); } } @@ -1002,8 +986,9 @@ public static void removeHandle(IOHandle handle) { * This ensures all buffered data is written without closing files. */ public static void flushAllHandles() { - synchronized (openHandles) { - for (IOHandle handle : openHandles.keySet()) { + Map handles = openHandles(); + synchronized (handles) { + for (IOHandle handle : handles.keySet()) { handle.flush(); } } @@ -1016,8 +1001,9 @@ public static void flushAllHandles() { */ public static void closeAllHandles() { flushAllHandles(); - synchronized (openHandles) { - for (IOHandle handle : openHandles.keySet()) { + Map handles = openHandles(); + synchronized (handles) { + for (IOHandle handle : handles.keySet()) { try { handle.close(); handle = new ClosedIOHandle(); @@ -1025,7 +1011,7 @@ public static void closeAllHandles() { // Handle exception if needed } } - openHandles.clear(); // Clear the cache after closing all handles + handles.clear(); // Clear the cache after closing all handles } } @@ -1400,7 +1386,7 @@ public RuntimeScalar close() { * @return RuntimeScalar with true if at EOF */ public RuntimeScalar eof() { - lastAccesseddHandle = this; + setLastAccessedHandle(this); return ioHandle.eof(); } @@ -1411,7 +1397,7 @@ public RuntimeScalar eof() { * @return RuntimeScalar with the current position */ public RuntimeScalar tell() { - lastAccesseddHandle = this; + setLastAccessedHandle(this); return ioHandle.tell(); } @@ -1423,7 +1409,7 @@ public RuntimeScalar tell() { * @return RuntimeScalar indicating success/failure */ public RuntimeScalar seek(long pos) { - lastAccesseddHandle = this; + setLastAccessedHandle(this); return ioHandle.seek(pos); } @@ -1460,14 +1446,15 @@ public RuntimeScalar write(String data) { needFlush = true; // Only flush lastAccessedHandle if it's a different handle AND doesn't share the same ioHandle // (duplicated handles share the same ioHandle, so flushing would be redundant and could cause deadlocks) - if (lastWrittenHandle != null && - lastWrittenHandle != this && - lastWrittenHandle.needFlush && - lastWrittenHandle.ioHandle != this.ioHandle) { + RuntimeIO lastWritten = getLastWrittenHandle(); + if (lastWritten != null && + lastWritten != this && + lastWritten.needFlush && + lastWritten.ioHandle != this.ioHandle) { // Synchronize terminal output for stdout and stderr - lastWrittenHandle.flush(); + lastWritten.flush(); } - lastWrittenHandle = this; + setLastWrittenHandle(this); // When no encoding layer is active, check for wide characters (> 0xFF). // Perl 5 warns and outputs UTF-8 encoding of the entire string in this case. diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java index 88c7ef55a..8f86afef4 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeScalar.java @@ -31,8 +31,10 @@ */ public class RuntimeScalar extends RuntimeBase implements RuntimeScalarReference, DynamicState { - // Static stack to store saved "local" states of RuntimeScalar instances - private static final Stack dynamicStateStack = new Stack<>(); + // Dynamic state stack is now held per-PerlRuntime. + private static Stack dynamicStateStack() { + return PerlRuntime.current().dynamicStateStack; + } // Pre-compiled regex pattern for decimal numification fast-path // INTEGER_PATTERN replaced with isIntegerString() for better performance @@ -2701,7 +2703,7 @@ public void dynamicSaveState() { currentState.value = this.value; currentState.blessId = this.blessId; // Push the current state onto the stack - dynamicStateStack.push(currentState); + dynamicStateStack().push(currentState); // Clear the current type and value this.type = UNDEF; this.value = null; @@ -2716,9 +2718,10 @@ public void dynamicSaveState() { */ @Override public void dynamicRestoreState() { - if (!dynamicStateStack.isEmpty()) { + Stack stack = dynamicStateStack(); + if (!stack.isEmpty()) { // Pop the most recent saved state from the stack - RuntimeScalar previousState = dynamicStateStack.pop(); + RuntimeScalar previousState = stack.pop(); // Decrement refCount of the CURRENT value being displaced. // Do NOT increment the restored value — it already has the correct diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java index d7162184c..afa527f03 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStash.java @@ -8,8 +8,10 @@ * The RuntimeStash class simulates Perl stash hashes. */ public class RuntimeStash extends RuntimeHash { - // Static stack to store saved "local" states of RuntimeStash instances - private static final Stack dynamicStateStack = new Stack<>(); + // Dynamic state stack is now held per-PerlRuntime. + private static Stack dynamicStateStack() { + return PerlRuntime.current().stashDynamicStateStack; + } // Map to store the elements of the hash public Map elements; public String namespace; @@ -163,12 +165,12 @@ private RuntimeScalar deleteGlob(String k) { String fullKey = namespace + k; // Check if the glob exists - boolean exists = GlobalVariable.globalCodeRefs.containsKey(fullKey) || - GlobalVariable.globalVariables.containsKey(fullKey) || - GlobalVariable.globalArrays.containsKey(fullKey) || - GlobalVariable.globalHashes.containsKey(fullKey) || - GlobalVariable.globalIORefs.containsKey(fullKey) || - GlobalVariable.globalFormatRefs.containsKey(fullKey); + boolean exists = GlobalVariable.getGlobalCodeRefsMap().containsKey(fullKey) || + GlobalVariable.getGlobalVariablesMap().containsKey(fullKey) || + GlobalVariable.getGlobalArraysMap().containsKey(fullKey) || + GlobalVariable.getGlobalHashesMap().containsKey(fullKey) || + GlobalVariable.getGlobalIORefsMap().containsKey(fullKey) || + GlobalVariable.getGlobalFormatRefsMap().containsKey(fullKey); if (!exists) { return new RuntimeScalar(); @@ -176,21 +178,21 @@ private RuntimeScalar deleteGlob(String k) { // Save all slot values BEFORE deleting so they can be accessed // on the returned glob (e.g., *{$old}{SCALAR} in namespace::clean) - RuntimeScalar savedScalar = GlobalVariable.globalVariables.get(fullKey); - RuntimeArray savedArray = GlobalVariable.globalArrays.get(fullKey); - RuntimeHash savedHash = GlobalVariable.globalHashes.get(fullKey); - RuntimeGlob savedIO = GlobalVariable.globalIORefs.get(fullKey); - RuntimeScalar savedCode = GlobalVariable.globalCodeRefs.get(fullKey); + RuntimeScalar savedScalar = GlobalVariable.getGlobalVariablesMap().get(fullKey); + RuntimeArray savedArray = GlobalVariable.getGlobalArraysMap().get(fullKey); + RuntimeHash savedHash = GlobalVariable.getGlobalHashesMap().get(fullKey); + RuntimeGlob savedIO = GlobalVariable.getGlobalIORefsMap().get(fullKey); + RuntimeScalar savedCode = GlobalVariable.getGlobalCodeRefsMap().get(fullKey); // Delete all slots from GlobalVariable // Only remove from globalCodeRefs, NOT pinnedCodeRefs, to allow compiled code // to continue calling the subroutine (Perl caches CVs at compile time) - GlobalVariable.globalCodeRefs.remove(fullKey); - GlobalVariable.globalVariables.remove(fullKey); - GlobalVariable.globalArrays.remove(fullKey); - GlobalVariable.globalHashes.remove(fullKey); - GlobalVariable.globalIORefs.remove(fullKey); - GlobalVariable.globalFormatRefs.remove(fullKey); + GlobalVariable.getGlobalCodeRefsMap().remove(fullKey); + GlobalVariable.getGlobalVariablesMap().remove(fullKey); + GlobalVariable.getGlobalArraysMap().remove(fullKey); + GlobalVariable.getGlobalHashesMap().remove(fullKey); + GlobalVariable.getGlobalIORefsMap().remove(fullKey); + GlobalVariable.getGlobalFormatRefsMap().remove(fullKey); // Removing symbols from a stash can affect method lookup. InheritanceResolver.invalidateCache(); @@ -219,12 +221,12 @@ private RuntimeScalar deleteNamespace(String k) { String childPrefix = "main::".equals(namespace) ? k : namespace + k; // Remove all symbols with this prefix from all global maps (prefix-based removal) - GlobalVariable.globalCodeRefs.keySet().removeIf(key -> key.startsWith(childPrefix)); - GlobalVariable.globalVariables.keySet().removeIf(key -> key.startsWith(childPrefix)); - GlobalVariable.globalArrays.keySet().removeIf(key -> key.startsWith(childPrefix)); - GlobalVariable.globalHashes.keySet().removeIf(key -> key.startsWith(childPrefix)); - GlobalVariable.globalIORefs.keySet().removeIf(key -> key.startsWith(childPrefix)); - GlobalVariable.globalFormatRefs.keySet().removeIf(key -> key.startsWith(childPrefix)); + GlobalVariable.getGlobalCodeRefsMap().keySet().removeIf(key -> key.startsWith(childPrefix)); + GlobalVariable.getGlobalVariablesMap().keySet().removeIf(key -> key.startsWith(childPrefix)); + GlobalVariable.getGlobalArraysMap().keySet().removeIf(key -> key.startsWith(childPrefix)); + GlobalVariable.getGlobalHashesMap().keySet().removeIf(key -> key.startsWith(childPrefix)); + GlobalVariable.getGlobalIORefsMap().keySet().removeIf(key -> key.startsWith(childPrefix)); + GlobalVariable.getGlobalFormatRefsMap().keySet().removeIf(key -> key.startsWith(childPrefix)); // Clear pinned code refs so deleted subs don't get resurrected // by getGlobalCodeRef() lookups (e.g., in SubroutineParser redefinition check) @@ -389,12 +391,12 @@ public RuntimeStash undefine() { GlobalVariable.clearStashAlias(prefix); - GlobalVariable.globalVariables.keySet().removeIf(k -> k.startsWith(prefix)); - GlobalVariable.globalArrays.keySet().removeIf(k -> k.startsWith(prefix)); - GlobalVariable.globalHashes.keySet().removeIf(k -> k.startsWith(prefix)); - GlobalVariable.globalCodeRefs.keySet().removeIf(k -> k.startsWith(prefix)); - GlobalVariable.globalIORefs.keySet().removeIf(k -> k.startsWith(prefix)); - GlobalVariable.globalFormatRefs.keySet().removeIf(k -> k.startsWith(prefix)); + GlobalVariable.getGlobalVariablesMap().keySet().removeIf(k -> k.startsWith(prefix)); + GlobalVariable.getGlobalArraysMap().keySet().removeIf(k -> k.startsWith(prefix)); + GlobalVariable.getGlobalHashesMap().keySet().removeIf(k -> k.startsWith(prefix)); + GlobalVariable.getGlobalCodeRefsMap().keySet().removeIf(k -> k.startsWith(prefix)); + GlobalVariable.getGlobalIORefsMap().keySet().removeIf(k -> k.startsWith(prefix)); + GlobalVariable.getGlobalFormatRefsMap().keySet().removeIf(k -> k.startsWith(prefix)); this.elements.clear(); @@ -449,7 +451,7 @@ public void dynamicSaveState() { currentState.elements = new HashMap<>(this.elements); ((RuntimeHash) currentState).elements = currentState.elements; currentState.blessId = this.blessId; - dynamicStateStack.push(currentState); + dynamicStateStack().push(currentState); // Clear the hash this.elements.clear(); super.elements = this.elements; @@ -462,9 +464,9 @@ public void dynamicSaveState() { */ @Override public void dynamicRestoreState() { - if (!dynamicStateStack.isEmpty()) { + if (!dynamicStateStack().isEmpty()) { // Restore the elements map and blessId from the most recent saved state - RuntimeStash previousState = dynamicStateStack.pop(); + RuntimeStash previousState = dynamicStateStack().pop(); this.elements = previousState.elements; super.elements = this.elements; this.blessId = previousState.blessId; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java index e65bcbeda..6ac898338 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/RuntimeStashEntry.java @@ -136,7 +136,7 @@ public RuntimeScalar set(RuntimeScalar value) { if (value.value instanceof RuntimeArray) { RuntimeArray targetArray = value.arrayDeref(); // Make the target array slot point to the same RuntimeArray object (aliasing) - GlobalVariable.globalArrays.put(this.globName, targetArray); + GlobalVariable.getGlobalArraysMap().put(this.globName, targetArray); // Also create a constant subroutine for bareword access RuntimeCode code = new RuntimeCode("", null); @@ -309,10 +309,10 @@ public RuntimeStashEntry undefine() { GlobalVariable.getGlobalVariable(this.globName).set(new RuntimeScalar()); // Undefine ARRAY - create empty array - GlobalVariable.globalArrays.put(this.globName, new RuntimeArray()); + GlobalVariable.getGlobalArraysMap().put(this.globName, new RuntimeArray()); // Undefine HASH - create empty hash - GlobalVariable.globalHashes.put(this.globName, new RuntimeHash()); + GlobalVariable.getGlobalHashesMap().put(this.globName, new RuntimeHash()); // Invalidate the method resolution cache InheritanceResolver.invalidateCache(); diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java index b6bd44db2..749cf5457 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/ScalarSpecialVariable.java @@ -23,7 +23,11 @@ */ public class ScalarSpecialVariable extends RuntimeBaseProxy { - private static final Stack inputLineStateStack = new Stack<>(); + // Input line state stack is now held per-PerlRuntime. + @SuppressWarnings("unchecked") + private static Stack inputLineStateStack() { + return (Stack) (Stack) PerlRuntime.current().inputLineStateStack; + } // The type of special variable, represented by an enum. final Id variableId; // The position of the capture group, used only for CAPTURE type variables. @@ -84,9 +88,9 @@ void vivify() { public RuntimeScalar set(RuntimeScalar value) { if (variableId == Id.INPUT_LINE_NUMBER) { vivify(); - if (RuntimeIO.lastAccesseddHandle != null) { - RuntimeIO.lastAccesseddHandle.currentLineNumber = value.getInt(); - lvalue.set(RuntimeIO.lastAccesseddHandle.currentLineNumber); + if (RuntimeIO.getLastAccessedHandle() != null) { + RuntimeIO.getLastAccessedHandle().currentLineNumber = value.getInt(); + lvalue.set(RuntimeIO.getLastAccessedHandle().currentLineNumber); } else { lvalue.set(value); } @@ -158,25 +162,25 @@ public RuntimeScalar getValueAsScalar() { yield postmatch != null ? makeRegexResultScalar(postmatch) : scalarUndef; } case P_PREMATCH -> { - if (!RuntimeRegex.lastMatchUsedPFlag) yield scalarUndef; + if (!RuntimeRegex.getLastMatchUsedPFlag()) yield scalarUndef; String prematch = RuntimeRegex.preMatchString(); yield prematch != null ? makeRegexResultScalar(prematch) : scalarUndef; } case P_MATCH -> { - if (!RuntimeRegex.lastMatchUsedPFlag) yield scalarUndef; + if (!RuntimeRegex.getLastMatchUsedPFlag()) yield scalarUndef; String match = RuntimeRegex.matchString(); yield match != null ? makeRegexResultScalar(match) : scalarUndef; } case P_POSTMATCH -> { - if (!RuntimeRegex.lastMatchUsedPFlag) yield scalarUndef; + if (!RuntimeRegex.getLastMatchUsedPFlag()) yield scalarUndef; String postmatch = RuntimeRegex.postMatchString(); yield postmatch != null ? makeRegexResultScalar(postmatch) : scalarUndef; } case LAST_FH -> { - if (RuntimeIO.lastAccesseddHandle == null) { + if (RuntimeIO.getLastAccessedHandle() == null) { yield scalarUndef; } - String globName = RuntimeIO.lastAccesseddHandle.globName; + String globName = RuntimeIO.getLastAccessedHandle().globName; if (globName != null) { // Extract package and name from the glob name String packageName; @@ -202,28 +206,28 @@ public RuntimeScalar getValueAsScalar() { } } // Fallback to the RuntimeIO object if no glob name is available - yield new RuntimeScalar(RuntimeIO.lastAccesseddHandle); + yield new RuntimeScalar(RuntimeIO.getLastAccessedHandle()); } case INPUT_LINE_NUMBER -> { - if (RuntimeIO.lastAccesseddHandle == null) { + if (RuntimeIO.getLastAccessedHandle() == null) { if (lvalue != null) { yield lvalue; } yield scalarUndef; } - yield getScalarInt(RuntimeIO.lastAccesseddHandle.currentLineNumber); + yield getScalarInt(RuntimeIO.getLastAccessedHandle().currentLineNumber); } case LAST_PAREN_MATCH -> { String lastCapture = RuntimeRegex.lastCaptureString(); yield lastCapture != null ? new RuntimeScalar(lastCapture) : scalarUndef; } - case LAST_SUCCESSFUL_PATTERN -> RuntimeRegex.lastSuccessfulPattern != null - ? new RuntimeScalar(RuntimeRegex.lastSuccessfulPattern) : scalarUndef; + case LAST_SUCCESSFUL_PATTERN -> RuntimeRegex.getLastSuccessfulPattern() != null + ? new RuntimeScalar(RuntimeRegex.getLastSuccessfulPattern()) : scalarUndef; case LAST_REGEXP_CODE_RESULT -> { // $^R - Result of last (?{...}) code block // Get the last matched regex and retrieve its code block result - if (RuntimeRegex.lastSuccessfulPattern != null) { - RuntimeScalar codeBlockResult = RuntimeRegex.lastSuccessfulPattern.getLastCodeBlockResult(); + if (RuntimeRegex.getLastSuccessfulPattern() != null) { + RuntimeScalar codeBlockResult = RuntimeRegex.getLastSuccessfulPattern().getLastCodeBlockResult(); yield codeBlockResult != null ? codeBlockResult : scalarUndef; } yield scalarUndef; @@ -273,7 +277,7 @@ public RuntimeScalar getValueAsScalar() { // During BEGIN/UNITCHECK blocks = compilation phase yield scalarUndef; } - yield getScalarInt(RuntimeCode.evalDepth > 0 ? 1 : 0); + yield getScalarInt(RuntimeCode.getEvalDepth() > 0 ? 1 : 0); } }; return result; @@ -424,10 +428,10 @@ public RuntimeList getList() { @Override public void dynamicSaveState() { if (variableId == Id.INPUT_LINE_NUMBER) { - RuntimeIO handle = RuntimeIO.lastAccesseddHandle; + RuntimeIO handle = RuntimeIO.getLastAccessedHandle(); int lineNumber = handle != null ? handle.currentLineNumber : (lvalue != null ? lvalue.getInt() : 0); RuntimeScalar localValue = lvalue != null ? new RuntimeScalar(lvalue) : null; - inputLineStateStack.push(new InputLineState(handle, lineNumber, localValue)); + inputLineStateStack().push(new InputLineState(handle, lineNumber, localValue)); return; } super.dynamicSaveState(); @@ -442,9 +446,9 @@ public void dynamicSaveState() { @Override public void dynamicRestoreState() { if (variableId == Id.INPUT_LINE_NUMBER) { - if (!inputLineStateStack.isEmpty()) { - InputLineState previous = inputLineStateStack.pop(); - RuntimeIO.lastAccesseddHandle = previous.lastHandle; + if (!inputLineStateStack().isEmpty()) { + InputLineState previous = inputLineStateStack().pop(); + RuntimeIO.setLastAccessedHandle(previous.lastHandle); if (previous.lastHandle != null) { previous.lastHandle.currentLineNumber = previous.lastLineNumber; } @@ -465,7 +469,7 @@ public void dynamicRestoreState() { */ private static RuntimeScalar makeRegexResultScalar(String value) { RuntimeScalar scalar = new RuntimeScalar(value); - if (RuntimeRegex.lastMatchWasByteString) { + if (RuntimeRegex.getLastMatchWasByteString()) { scalar.type = RuntimeScalarType.BYTE_STRING; } return scalar; diff --git a/src/main/java/org/perlonjava/runtime/runtimetypes/SpecialBlock.java b/src/main/java/org/perlonjava/runtime/runtimetypes/SpecialBlock.java index ee3bf5df0..36c8f40ec 100644 --- a/src/main/java/org/perlonjava/runtime/runtimetypes/SpecialBlock.java +++ b/src/main/java/org/perlonjava/runtime/runtimetypes/SpecialBlock.java @@ -13,10 +13,11 @@ */ public class SpecialBlock { - // Arrays to store different types of blocks - public static RuntimeArray endBlocks = new RuntimeArray(); - public static RuntimeArray initBlocks = new RuntimeArray(); - public static RuntimeArray checkBlocks = new RuntimeArray(); + // State is now held per-PerlRuntime. These accessors delegate to the current runtime. + // Public getters preserve backward compatibility for any code that reads these fields. + public static RuntimeArray getEndBlocks() { return PerlRuntime.current().endBlocks; } + public static RuntimeArray getInitBlocks() { return PerlRuntime.current().initBlocks; } + public static RuntimeArray getCheckBlocks() { return PerlRuntime.current().checkBlocks; } /** * Saves a code reference to the endBlocks array. @@ -25,7 +26,7 @@ public class SpecialBlock { * @param codeRef the code reference to be saved */ public static void saveEndBlock(RuntimeScalar codeRef) { - RuntimeArray.push(endBlocks, codeRef); + RuntimeArray.push(getEndBlocks(), codeRef); } /** @@ -35,7 +36,7 @@ public static void saveEndBlock(RuntimeScalar codeRef) { * @param codeRef the code reference to be saved */ public static void saveInitBlock(RuntimeScalar codeRef) { - RuntimeArray.unshift(initBlocks, codeRef); + RuntimeArray.unshift(getInitBlocks(), codeRef); } /** @@ -45,7 +46,7 @@ public static void saveInitBlock(RuntimeScalar codeRef) { * @param codeRef the code reference to be saved */ public static void saveCheckBlock(RuntimeScalar codeRef) { - RuntimeArray.push(checkBlocks, codeRef); + RuntimeArray.push(getCheckBlocks(), codeRef); } /** @@ -56,13 +57,11 @@ public static void saveCheckBlock(RuntimeScalar codeRef) { */ public static void runEndBlocks(boolean resetChildStatus) { if (resetChildStatus) { - // Reset $? to 0 before END blocks run (Perl semantics for normal exit) - // This ensures END blocks see $? = 0 unless they explicitly set it getGlobalVariable("main::?").set(0); } - - while (!endBlocks.isEmpty()) { - RuntimeScalar block = RuntimeArray.pop(endBlocks); + RuntimeArray blocks = getEndBlocks(); + while (!blocks.isEmpty()) { + RuntimeScalar block = RuntimeArray.pop(blocks); if (block.getDefinedBoolean()) { RuntimeCode.apply(block, new RuntimeArray(), RuntimeContextType.VOID); } @@ -81,8 +80,9 @@ public static void runEndBlocks() { * Executes all code blocks stored in the initBlocks array in FIFO order. */ public static void runInitBlocks() { - while (!initBlocks.isEmpty()) { - RuntimeScalar block = RuntimeArray.pop(initBlocks); + RuntimeArray blocks = getInitBlocks(); + while (!blocks.isEmpty()) { + RuntimeScalar block = RuntimeArray.pop(blocks); if (block.getDefinedBoolean()) { RuntimeCode.apply(block, new RuntimeArray(), RuntimeContextType.VOID); } @@ -93,8 +93,9 @@ public static void runInitBlocks() { * Executes all code blocks stored in the checkBlocks array in LIFO order. */ public static void runCheckBlocks() { - while (!checkBlocks.isEmpty()) { - RuntimeScalar block = RuntimeArray.pop(checkBlocks); + RuntimeArray blocks = getCheckBlocks(); + while (!blocks.isEmpty()) { + RuntimeScalar block = RuntimeArray.pop(blocks); if (block.getDefinedBoolean()) { RuntimeCode.apply(block, new RuntimeArray(), RuntimeContextType.VOID); } diff --git a/src/main/java/org/perlonjava/runtime/terminal/UnixTerminalHandler.java b/src/main/java/org/perlonjava/runtime/terminal/UnixTerminalHandler.java index 5757e31c9..6b202d990 100644 --- a/src/main/java/org/perlonjava/runtime/terminal/UnixTerminalHandler.java +++ b/src/main/java/org/perlonjava/runtime/terminal/UnixTerminalHandler.java @@ -97,7 +97,7 @@ public void restoreTerminal(RuntimeIO fh) { @Override public char readSingleChar(double timeoutSeconds, RuntimeIO fh) throws IOException { - if (fh != RuntimeIO.stdin) { + if (fh != RuntimeIO.getStdin()) { RuntimeScalar result = fh.ioHandle.read(1); if (!result.getDefinedBoolean()) { return 0; @@ -147,7 +147,7 @@ public char readSingleChar(double timeoutSeconds, RuntimeIO fh) throws IOExcepti @Override public String readLineWithTimeout(double timeoutSeconds, RuntimeIO fh) throws IOException { BufferedReader reader = new BufferedReader(new InputStreamReader( - fh == RuntimeIO.stdin ? System.in : new ByteArrayInputStream(new byte[0]))); + fh == RuntimeIO.getStdin() ? System.in : new ByteArrayInputStream(new byte[0]))); if (timeoutSeconds < 0) { // Non-blocking read diff --git a/src/main/java/org/perlonjava/runtime/terminal/WindowsTerminalHandler.java b/src/main/java/org/perlonjava/runtime/terminal/WindowsTerminalHandler.java index feb8addc5..4c959f6f3 100644 --- a/src/main/java/org/perlonjava/runtime/terminal/WindowsTerminalHandler.java +++ b/src/main/java/org/perlonjava/runtime/terminal/WindowsTerminalHandler.java @@ -57,7 +57,7 @@ public void restoreTerminal(RuntimeIO fh) { @Override public char readSingleChar(double timeoutSeconds, RuntimeIO fh) throws IOException { - if (fh != RuntimeIO.stdin) { + if (fh != RuntimeIO.getStdin()) { RuntimeScalar result = fh.ioHandle.read(1); if (!result.getDefinedBoolean()) { return 0; diff --git a/src/test/java/org/perlonjava/ModuleTestExecutionTest.java b/src/test/java/org/perlonjava/ModuleTestExecutionTest.java index 3b6a012ad..02637c2e7 100644 --- a/src/test/java/org/perlonjava/ModuleTestExecutionTest.java +++ b/src/test/java/org/perlonjava/ModuleTestExecutionTest.java @@ -7,6 +7,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.perlonjava.app.cli.CompilerOptions; import org.perlonjava.runtime.io.StandardIO; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import org.perlonjava.runtime.runtimetypes.RuntimeArray; import org.perlonjava.runtime.runtimetypes.RuntimeIO; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; @@ -97,23 +98,28 @@ static Stream provideModuleTestScripts() throws IOException { @BeforeEach void setUp() { + // Ensure PerlRuntime is initialized for this test thread + if (PerlRuntime.currentOrNull() == null) { + PerlRuntime.initialize(); + } + originalOut = System.out; outputStream = new ByteArrayOutputStream(); originalUserDir = System.getProperty("user.dir"); StandardIO newStdout = new StandardIO(outputStream, true); - RuntimeIO.stdout = new RuntimeIO(newStdout); - GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.stdout); - GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.stderr); + RuntimeIO.setStdout(new RuntimeIO(newStdout)); + GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.getStdout()); + GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.getStderr()); System.setOut(new PrintStream(outputStream)); } @AfterEach void tearDown() { // Restore original stdout - RuntimeIO.stdout = new RuntimeIO(new StandardIO(originalOut, true)); - GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.stdout); - GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.stderr); + RuntimeIO.setStdout(new RuntimeIO(new StandardIO(originalOut, true))); + GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.getStdout()); + GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.getStderr()); System.setOut(originalOut); // Restore original working directory diff --git a/src/test/java/org/perlonjava/PerlScriptExecutionTest.java b/src/test/java/org/perlonjava/PerlScriptExecutionTest.java index 7e3d6fd82..9f596b3ad 100644 --- a/src/test/java/org/perlonjava/PerlScriptExecutionTest.java +++ b/src/test/java/org/perlonjava/PerlScriptExecutionTest.java @@ -8,6 +8,7 @@ import org.junit.jupiter.params.provider.MethodSource; import org.perlonjava.app.cli.CompilerOptions; import org.perlonjava.runtime.io.StandardIO; +import org.perlonjava.runtime.runtimetypes.PerlRuntime; import org.perlonjava.runtime.runtimetypes.RuntimeArray; import org.perlonjava.runtime.runtimetypes.RuntimeIO; import org.perlonjava.runtime.runtimetypes.RuntimeScalar; @@ -158,6 +159,11 @@ private static Stream getPerlScripts(boolean unitOnly) throws IOExceptio */ @BeforeEach void setUp() { + // Ensure PerlRuntime is initialized for this test thread + if (PerlRuntime.currentOrNull() == null) { + PerlRuntime.initialize(); + } + originalOut = System.out; outputStream = new ByteArrayOutputStream(); @@ -165,11 +171,11 @@ void setUp() { StandardIO newStdout = new StandardIO(outputStream, true); // Replace RuntimeIO.stdout with a new instance - RuntimeIO.stdout = new RuntimeIO(newStdout); + RuntimeIO.setStdout(new RuntimeIO(newStdout)); // Keep Perl's global *STDOUT/*STDERR in sync with the RuntimeIO static fields. // Some tests call `binmode STDOUT/STDERR` and expect it to affect the real globals. - GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.stdout); - GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.stderr); + GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.getStdout()); + GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.getStderr()); // Also update System.out for any direct Java calls System.setOut(new PrintStream(outputStream)); @@ -181,9 +187,9 @@ void setUp() { @AfterEach void tearDown() { // Restore original stdout - RuntimeIO.stdout = new RuntimeIO(new StandardIO(originalOut, true)); - GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.stdout); - GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.stderr); + RuntimeIO.setStdout(new RuntimeIO(new StandardIO(originalOut, true))); + GlobalVariable.getGlobalIO("main::STDOUT").setIO(RuntimeIO.getStdout()); + GlobalVariable.getGlobalIO("main::STDERR").setIO(RuntimeIO.getStderr()); System.setOut(originalOut); }